diff --git a/docs/CLI.md b/docs/CLI.md index 160e192ee..cff2b6cd3 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -56,6 +56,7 @@ Each defaults to a value based on the running system, including an repository if | `--pnpm` | `string` | pnpm version for `package.json`'s `packageManager` field | Existing value in `package.json` if it exists | | `--repository` | `string` | Name for the new repository | The same as `--directory` | | `--title` | `string` | 'Title Case' title for the repository | Title-cased `repository` | +| `--type` | `string` | package.json modules type | Existing value in `package.json` if it exists, or `"module"` | | `--version` | `string` | package version to publish as and store in `package.json` | Existing value in `package.json` if it exists, or `"0.0.0"` | | `--words` | `string[]` | additional words to add to the CSpell dictionary | Existing `words` in a `cspell.json` file if it exists, and any new words in from other options | diff --git a/eslint.config.js b/eslint.config.js index 6f9a57272..7b87e41f2 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -42,7 +42,7 @@ export default tseslint.config( tseslint.configs.strictTypeChecked, tseslint.configs.stylisticTypeChecked, ], - files: ["**/*.js", "**/*.ts"], + files: ["**/*.{js,ts}"], languageOptions: { parserOptions: { projectService: { diff --git a/src/base.test.ts b/src/base.test.ts index 45cf28466..4b092a8a9 100644 --- a/src/base.test.ts +++ b/src/base.test.ts @@ -57,6 +57,7 @@ describe("base", () => { pnpm: expect.any(String), repository: "create-typescript-app", title: "Create TypeScript App", + type: expect.any(String), version: expect.any(String), // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-require-imports words: require("../cspell.json").words, diff --git a/src/base.ts b/src/base.ts index 0ee339563..6d761b02d 100644 --- a/src/base.ts +++ b/src/base.ts @@ -155,6 +155,10 @@ export const base = createBase({ .optional() .describe("GitHub branch ruleset ID for main branch protections"), title: z.string().describe("'Title Case' title for the repository"), + type: z + .union([z.literal("commonjs"), z.literal("module")]) + .optional() + .describe("package.json modules type"), version: z .string() .optional() @@ -294,6 +298,8 @@ export const base = createBase({ async () => await readTitle(getReadme, getRepository), ); + const getType = lazyValue(async () => (await getPackageData()).type); + const getVersion = lazyValue(async () => (await getPackageData()).version); const getWords = lazyValue(async () => await readWords(take)); @@ -324,6 +330,7 @@ export const base = createBase({ repository: getRepository, rulesetId: getRulesetId, title: getTitle, + type: getType, version: getVersion, words: getWords, workflowsVersions: getWorkflowData, diff --git a/src/blocks/blockESLint.test.ts b/src/blocks/blockESLint.test.ts index 35dd6f27f..56d277d0d 100644 --- a/src/blocks/blockESLint.test.ts +++ b/src/blocks/blockESLint.test.ts @@ -110,7 +110,7 @@ describe("blockESLint", () => { { ignores: ["lib", "node_modules", "pnpm-lock.yaml"] }, { linterOptions: {"reportUnusedDisableDirectives":"error"} }, eslint.configs.recommended, - { extends: [tseslint.configs.strictTypeChecked, tseslint.configs.stylisticTypeChecked], files: ["**/*.js", "**/*.ts"], languageOptions: {"parserOptions":{"projectService":{"allowDefaultProject":["*.config.*s"]},"tsconfigRootDir":import.meta.dirname}}, } + { extends: [tseslint.configs.strictTypeChecked, tseslint.configs.stylisticTypeChecked], files: ["**/*.{js,ts}"], languageOptions: {"parserOptions":{"projectService":{"allowDefaultProject":["*.config.*s"]},"tsconfigRootDir":import.meta.dirname}}, } );", }, "scripts": [ @@ -265,7 +265,165 @@ describe("blockESLint", () => { { ignores: ["lib", "node_modules", "pnpm-lock.yaml"] }, { linterOptions: {"reportUnusedDisableDirectives":"error"} }, eslint.configs.recommended, - { extends: [tseslint.configs.strictTypeChecked, tseslint.configs.stylisticTypeChecked], files: ["**/*.js", "**/*.ts"], languageOptions: {"parserOptions":{"projectService":{"allowDefaultProject":["*.config.*s"]},"tsconfigRootDir":import.meta.dirname}}, } + { extends: [tseslint.configs.strictTypeChecked, tseslint.configs.stylisticTypeChecked], files: ["**/*.{js,ts}"], languageOptions: {"parserOptions":{"projectService":{"allowDefaultProject":["*.config.*s"]},"tsconfigRootDir":import.meta.dirname}}, } + );", + }, + "scripts": [ + { + "commands": [ + "pnpm lint --fix", + ], + "phase": 3, + }, + ], + } + `); + }); + + test("transition mode with options.type set to commonjs", () => { + const creation = testBlock(blockESLint, { + mode: "transition", + options: { + ...optionsBase, + type: "commonjs", + }, + }); + + expect(creation).toMatchInlineSnapshot(` + { + "addons": [ + { + "addons": { + "sections": { + "Linting": { + "contents": { + "after": [ + " + For example, ESLint can be run with \`--fix\` to auto-fix some lint rule complaints: + + \`\`\`shell + pnpm run lint --fix + \`\`\` + ", + ], + "before": " + This package includes several forms of linting to enforce consistent code quality and styling. + Each should be shown in VS Code, and can be run manually on the command-line: + ", + "items": [ + "- \`pnpm lint\` ([ESLint](https://eslint.org) with [typescript-eslint](https://typescript-eslint.io)): Lints JavaScript and TypeScript source files", + ], + "plural": "Read the individual documentation for each linter to understand how it can be configured and used best.", + }, + }, + }, + }, + "block": [Function], + }, + { + "addons": { + "jobs": [ + { + "name": "Lint", + "steps": [ + { + "run": "pnpm lint", + }, + ], + }, + ], + }, + "block": [Function], + }, + { + "addons": { + "properties": { + "devDependencies": { + "@eslint/js": "9.22.0", + "@types/node": "22.13.10", + "eslint": "9.22.0", + "typescript-eslint": "8.26.1", + }, + "scripts": { + "lint": "eslint . --max-warnings 0", + }, + }, + }, + "block": [Function], + }, + { + "addons": { + "extensions": [ + "dbaeumer.vscode-eslint", + ], + "settings": { + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + }, + "eslint.probe": [ + "javascript", + "javascriptreact", + "json", + "jsonc", + "markdown", + "typescript", + "typescriptreact", + "yaml", + ], + "eslint.rules.customizations": [ + { + "rule": "*", + "severity": "warn", + }, + ], + }, + }, + "block": [Function], + }, + { + "addons": { + "dependencies": [ + "@types/eslint", + "@typescript-eslint/eslint-plugin", + "@typescript-eslint/parser", + "eslint-plugin-deprecation", + "eslint-plugin-eslint-comments", + "eslint-plugin-no-only-tests", + "jsonc-eslint-parser", + "yaml-eslint-parser", + ], + }, + "block": [Function], + }, + { + "addons": { + "files": [ + ".eslintrc*", + ".eslintignore", + "eslint.config.{cjs,js}", + ], + }, + "block": [Function], + }, + { + "addons": { + "workflows": [ + "eslint", + "lint", + ], + }, + "block": [Function], + }, + ], + "files": { + "eslint.config.mjs": "import eslint from "@eslint/js"; + import tseslint from "typescript-eslint"; + + export default tseslint.config( + { ignores: ["lib", "node_modules", "pnpm-lock.yaml"] }, + { linterOptions: {"reportUnusedDisableDirectives":"error"} }, + eslint.configs.recommended, + { extends: [tseslint.configs.strictTypeChecked, tseslint.configs.stylisticTypeChecked], files: ["**/*.{js,mjs,ts}"], languageOptions: {"parserOptions":{"projectService":{"allowDefaultProject":["*.config.*s"]},"tsconfigRootDir":import.meta.dirname}}, },{ files: ["*.mjs"], languageOptions: {"sourceType":"module"}, } );", }, "scripts": [ @@ -434,7 +592,7 @@ describe("blockESLint", () => { { ignores: ["generated", "lib", "node_modules", "pnpm-lock.yaml"] }, { linterOptions: {"reportUnusedDisableDirectives":"error"} }, eslint.configs.recommended, - a.configs.recommended,{ extends: [b.configs.recommended], files: ["**/*.b"], rules: {"b/c":"error","b/d":["error",{"e":"f"}]}, },{ extends: [c.configs.recommended], rules: {"c/d":"error","c/e":["error",{"f":"g"}]}, },{ extends: [tseslint.configs.strictTypeChecked, tseslint.configs.stylisticTypeChecked], files: ["**/*.js", "**/*.ts"], languageOptions: {"parserOptions":{"projectService":{"allowDefaultProject":["*.config.*s"]},"tsconfigRootDir":import.meta.dirname}}, rules: {"a/b":"error","a/c":["error",{"d":"e"}]}, settings: {"react":{"version":"detect"}}, } + a.configs.recommended,{ extends: [b.configs.recommended], files: ["**/*.b"], rules: {"b/c":"error","b/d":["error",{"e":"f"}]}, },{ extends: [c.configs.recommended], rules: {"c/d":"error","c/e":["error",{"f":"g"}]}, },{ extends: [tseslint.configs.strictTypeChecked, tseslint.configs.stylisticTypeChecked], files: ["**/*.{js,ts}"], languageOptions: {"parserOptions":{"projectService":{"allowDefaultProject":["*.config.*s"]},"tsconfigRootDir":import.meta.dirname}}, rules: {"a/b":"error","a/c":["error",{"d":"e"}]}, settings: {"react":{"version":"detect"}}, } );", }, "scripts": [ @@ -570,7 +728,7 @@ describe("blockESLint", () => { { ignores: ["lib", "node_modules", "pnpm-lock.yaml"] }, { linterOptions: {"reportUnusedDisableDirectives":"error"} }, eslint.configs.recommended, - { extends: [tseslint.configs.strictTypeChecked, tseslint.configs.stylisticTypeChecked], files: ["**/*.js", "**/*.ts"], languageOptions: {"parserOptions":{"projectService":{"allowDefaultProject":["*.config.*s"]},"tsconfigRootDir":import.meta.dirname}}, rules: { + { extends: [tseslint.configs.strictTypeChecked, tseslint.configs.stylisticTypeChecked], files: ["**/*.{js,ts}"], languageOptions: {"parserOptions":{"projectService":{"allowDefaultProject":["*.config.*s"]},"tsconfigRootDir":import.meta.dirname}}, rules: { // One line "a": "error", @@ -708,7 +866,7 @@ describe("blockESLint", () => { { ignores: ["lib", "node_modules", "pnpm-lock.yaml"] }, { linterOptions: {"reportUnusedDisableDirectives":"error"} }, eslint.configs.recommended, - { extends: [tseslint.configs.strictTypeChecked, tseslint.configs.stylisticTypeChecked], files: ["**/*.js", "**/*.ts"], languageOptions: {"parserOptions":{"projectService":{"allowDefaultProject":["*.config.*s","bin/index.js"]},"tsconfigRootDir":import.meta.dirname}}, } + { extends: [tseslint.configs.strictTypeChecked, tseslint.configs.stylisticTypeChecked], files: ["**/*.{js,ts}"], languageOptions: {"parserOptions":{"projectService":{"allowDefaultProject":["*.config.*s","bin/index.js"]},"tsconfigRootDir":import.meta.dirname}}, } );", }, "scripts": [ @@ -834,7 +992,253 @@ describe("blockESLint", () => { { ignores: ["lib", "node_modules", "pnpm-lock.yaml"] }, { linterOptions: {"reportUnusedDisableDirectives":"error"} }, eslint.configs.recommended, - { extends: [tseslint.configs.strictTypeChecked, tseslint.configs.stylisticTypeChecked], files: ["**/*.js", "**/*.ts"], languageOptions: {"parserOptions":{"projectService":{"allowDefaultProject":["*.config.*s","bin/index.js"]},"tsconfigRootDir":import.meta.dirname}}, } + { extends: [tseslint.configs.strictTypeChecked, tseslint.configs.stylisticTypeChecked], files: ["**/*.{js,ts}"], languageOptions: {"parserOptions":{"projectService":{"allowDefaultProject":["*.config.*s","bin/index.js"]},"tsconfigRootDir":import.meta.dirname}}, } + );", + }, + "scripts": [ + { + "commands": [ + "pnpm lint --fix", + ], + "phase": 3, + }, + ], + } + `); + }); + + test("with options.type set to commonjs", () => { + const creation = testBlock(blockESLint, { + options: { + ...optionsBase, + type: "commonjs", + }, + }); + + expect(creation).toMatchInlineSnapshot(` + { + "addons": [ + { + "addons": { + "sections": { + "Linting": { + "contents": { + "after": [ + " + For example, ESLint can be run with \`--fix\` to auto-fix some lint rule complaints: + + \`\`\`shell + pnpm run lint --fix + \`\`\` + ", + ], + "before": " + This package includes several forms of linting to enforce consistent code quality and styling. + Each should be shown in VS Code, and can be run manually on the command-line: + ", + "items": [ + "- \`pnpm lint\` ([ESLint](https://eslint.org) with [typescript-eslint](https://typescript-eslint.io)): Lints JavaScript and TypeScript source files", + ], + "plural": "Read the individual documentation for each linter to understand how it can be configured and used best.", + }, + }, + }, + }, + "block": [Function], + }, + { + "addons": { + "jobs": [ + { + "name": "Lint", + "steps": [ + { + "run": "pnpm lint", + }, + ], + }, + ], + }, + "block": [Function], + }, + { + "addons": { + "properties": { + "devDependencies": { + "@eslint/js": "9.22.0", + "@types/node": "22.13.10", + "eslint": "9.22.0", + "typescript-eslint": "8.26.1", + }, + "scripts": { + "lint": "eslint . --max-warnings 0", + }, + }, + }, + "block": [Function], + }, + { + "addons": { + "extensions": [ + "dbaeumer.vscode-eslint", + ], + "settings": { + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + }, + "eslint.probe": [ + "javascript", + "javascriptreact", + "json", + "jsonc", + "markdown", + "typescript", + "typescriptreact", + "yaml", + ], + "eslint.rules.customizations": [ + { + "rule": "*", + "severity": "warn", + }, + ], + }, + }, + "block": [Function], + }, + ], + "files": { + "eslint.config.mjs": "import eslint from "@eslint/js"; + import tseslint from "typescript-eslint"; + + export default tseslint.config( + { ignores: ["lib", "node_modules", "pnpm-lock.yaml"] }, + { linterOptions: {"reportUnusedDisableDirectives":"error"} }, + eslint.configs.recommended, + { extends: [tseslint.configs.strictTypeChecked, tseslint.configs.stylisticTypeChecked], files: ["**/*.{js,mjs,ts}"], languageOptions: {"parserOptions":{"projectService":{"allowDefaultProject":["*.config.*s"]},"tsconfigRootDir":import.meta.dirname}}, },{ files: ["*.mjs"], languageOptions: {"sourceType":"module"}, } + );", + }, + "scripts": [ + { + "commands": [ + "pnpm lint --fix", + ], + "phase": 3, + }, + ], + } + `); + }); + + test("with options.type set to module", () => { + const creation = testBlock(blockESLint, { + options: { + ...optionsBase, + type: "module", + }, + }); + + expect(creation).toMatchInlineSnapshot(` + { + "addons": [ + { + "addons": { + "sections": { + "Linting": { + "contents": { + "after": [ + " + For example, ESLint can be run with \`--fix\` to auto-fix some lint rule complaints: + + \`\`\`shell + pnpm run lint --fix + \`\`\` + ", + ], + "before": " + This package includes several forms of linting to enforce consistent code quality and styling. + Each should be shown in VS Code, and can be run manually on the command-line: + ", + "items": [ + "- \`pnpm lint\` ([ESLint](https://eslint.org) with [typescript-eslint](https://typescript-eslint.io)): Lints JavaScript and TypeScript source files", + ], + "plural": "Read the individual documentation for each linter to understand how it can be configured and used best.", + }, + }, + }, + }, + "block": [Function], + }, + { + "addons": { + "jobs": [ + { + "name": "Lint", + "steps": [ + { + "run": "pnpm lint", + }, + ], + }, + ], + }, + "block": [Function], + }, + { + "addons": { + "properties": { + "devDependencies": { + "@eslint/js": "9.22.0", + "@types/node": "22.13.10", + "eslint": "9.22.0", + "typescript-eslint": "8.26.1", + }, + "scripts": { + "lint": "eslint . --max-warnings 0", + }, + }, + }, + "block": [Function], + }, + { + "addons": { + "extensions": [ + "dbaeumer.vscode-eslint", + ], + "settings": { + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + }, + "eslint.probe": [ + "javascript", + "javascriptreact", + "json", + "jsonc", + "markdown", + "typescript", + "typescriptreact", + "yaml", + ], + "eslint.rules.customizations": [ + { + "rule": "*", + "severity": "warn", + }, + ], + }, + }, + "block": [Function], + }, + ], + "files": { + "eslint.config.js": "import eslint from "@eslint/js"; + import tseslint from "typescript-eslint"; + + export default tseslint.config( + { ignores: ["lib", "node_modules", "pnpm-lock.yaml"] }, + { linterOptions: {"reportUnusedDisableDirectives":"error"} }, + eslint.configs.recommended, + { extends: [tseslint.configs.strictTypeChecked, tseslint.configs.stylisticTypeChecked], files: ["**/*.{js,ts}"], languageOptions: {"parserOptions":{"projectService":{"allowDefaultProject":["*.config.*s"]},"tsconfigRootDir":import.meta.dirname}}, } );", }, "scripts": [ diff --git a/src/blocks/blockESLint.ts b/src/blocks/blockESLint.ts index 020623cd8..757c92483 100644 --- a/src/blocks/blockESLint.ts +++ b/src/blocks/blockESLint.ts @@ -71,6 +71,11 @@ export const blockESLint = base.createBlock({ const { explanations, extensions, ignores, imports, rules, settings } = addons; + const [configFileName, fileExtensions] = + options.type === "commonjs" + ? ["eslint.config.mjs", "js,mjs,ts"] + : ["eslint.config.js", "js,ts"]; + const explanation = explanations.length > 0 ? `${explanations @@ -99,7 +104,7 @@ export const blockESLint = base.createBlock({ "tseslint.configs.strictTypeChecked", "tseslint.configs.stylisticTypeChecked", ], - files: ["**/*.js", "**/*.ts"], + files: [`**/*.{${fileExtensions}}`], languageOptions: { parserOptions: { projectService: { @@ -122,6 +127,14 @@ export const blockESLint = base.createBlock({ ...(rules && { rules }), ...(settings && { settings }), }), + ...(options.type === "commonjs" + ? [ + printExtension({ + files: ["*.mjs"], + languageOptions: { sourceType: "module" }, + }), + ] + : []), ...extensions.map((extension) => typeof extension === "string" ? extension : printExtension(extension), ), @@ -207,7 +220,7 @@ Each should be shown in VS Code, and can be run manually on the command-line: }), ], files: { - "eslint.config.js": `${explanation}${importLines.join("\n")} + [configFileName]: `${explanation}${importLines.join("\n")} export default tseslint.config( { ignores: [${ignoreLines.join(", ")}] }, @@ -226,7 +239,7 @@ export default tseslint.config( ], }; }, - transition() { + transition({ options }) { return { addons: [ blockRemoveDependencies({ @@ -242,7 +255,13 @@ export default tseslint.config( ], }), blockRemoveFiles({ - files: [".eslintrc*", ".eslintignore", "eslint.config.{cjs,mjs}"], + files: [ + ".eslintrc*", + ".eslintignore", + options.type === "commonjs" + ? "eslint.config.{cjs,js}" + : "eslint.config.{cjs,mjs}", + ], }), blockRemoveWorkflows({ workflows: ["eslint", "lint"], diff --git a/src/blocks/blockPackageJson.test.ts b/src/blocks/blockPackageJson.test.ts index 793244ea9..2e79798f3 100644 --- a/src/blocks/blockPackageJson.test.ts +++ b/src/blocks/blockPackageJson.test.ts @@ -131,14 +131,12 @@ describe("blockPackageJson", () => { `); }); - test("with addons adding type", () => { + test("with options.type set to commonjs", () => { const creation = testBlock(blockPackageJson, { - addons: { - properties: { - type: "commonjs", - }, + options: { + ...options, + type: "commonjs", }, - options, }); expect(creation).toMatchInlineSnapshot(` diff --git a/src/blocks/blockPackageJson.ts b/src/blocks/blockPackageJson.ts index 72c49adc3..637aba13e 100644 --- a/src/blocks/blockPackageJson.ts +++ b/src/blocks/blockPackageJson.ts @@ -42,7 +42,6 @@ export const blockPackageJson = base.createBlock({ "package.json": sortPackageJson( JSON.stringify( removeUndefinedObjects({ - type: "module", ...options.packageData, ...addons.properties, author: { email: options.email.npm, name: options.author }, @@ -76,6 +75,7 @@ export const blockPackageJson = base.createBlock({ ...options.packageData?.scripts, ...addons.properties.scripts, }, + type: options.type ?? "module", version: options.version ?? "0.0.0", }), ), diff --git a/src/types.ts b/src/types.ts index ea0349ea1..0483a8469 100644 --- a/src/types.ts +++ b/src/types.ts @@ -24,6 +24,7 @@ export interface PartialPackageData { publishConfig?: PartialPublishConfig; repository?: string | { type: string; url: string }; scripts?: Record; + type?: "commonjs" | "module"; version?: string; }