@linter-spec/cli 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,7 @@ import generateTemplate from '../../utils/generate-template.js';
7
7
  import { CLI_NAME, PROJECT_TYPES } from '../../utils/constants.js';
8
8
  import { messages } from '../../utils/messages.js';
9
9
  import { chooseEnableMarkdownlint, chooseEnablePrettier, chooseEnableStylelint, chooseEslintType, } from './prompts.js';
10
- import { installCliDep } from './install-deps.js';
10
+ import { installProjectDeps } from './install-deps.js';
11
11
  import { setupHusky } from './setup-husky.js';
12
12
  export default async function init(options) {
13
13
  const cwd = options.cwd || process.cwd();
@@ -46,7 +46,7 @@ export default async function init(options) {
46
46
  log.success(messages.stepConflictDone(step));
47
47
  if (!disableNpmInstall) {
48
48
  log.info(messages.stepInstall(++step));
49
- await installCliDep(cwd);
49
+ await installProjectDeps(cwd, config);
50
50
  log.success(messages.stepInstallDone(step));
51
51
  }
52
52
  }
@@ -1,7 +1,25 @@
1
1
  /** True when the project carries its own lint config files. */
2
2
  export declare function hasLocalLintConfig(cwd: string): boolean;
3
- /** Install the CLI itself as a dev dependency of the target project. */
4
- export declare function installCliDep(cwd: string): Promise<void>;
3
+ export interface ProjectDepsConfig {
4
+ enableESLint: boolean;
5
+ eslintType: string;
6
+ enableStylelint: boolean;
7
+ enableMarkdownlint: boolean;
8
+ enablePrettier: boolean;
9
+ }
10
+ /**
11
+ * The npm packages to install as devDependencies of the user's project so that
12
+ * the generated `eslint.config.mjs` (and friends) can resolve everything they
13
+ * need — including peers of `@linter-spec/eslint-config` that are marked
14
+ * `optional` in its `peerDependenciesMeta` and therefore would NOT be installed
15
+ * transitively just by adding `@linter-spec/cli`.
16
+ */
17
+ export declare function projectDepsToInstall(config: ProjectDepsConfig): string[];
18
+ /**
19
+ * Install everything the chosen lint setup needs (CLI + eslint-config +
20
+ * project-type-specific plugins + selected toolchains) as devDeps of `cwd`.
21
+ */
22
+ export declare function installProjectDeps(cwd: string, config: ProjectDepsConfig): Promise<void>;
5
23
  /**
6
24
  * When a project relies on the bundled lint configs but has no `node_modules`,
7
25
  * install its dependencies first (otherwise config resolution fails).
@@ -16,10 +16,87 @@ export function hasLocalLintConfig(cwd) {
16
16
  '.markdownlint?(-cli2).@(jsonc|json|yaml|yml|cjs|mjs)',
17
17
  ], { cwd, dot: true }).length > 0);
18
18
  }
19
- /** Install the CLI itself as a dev dependency of the target project. */
20
- export async function installCliDep(cwd) {
21
- const [command, args] = addDevCommand(detectPackageManager(cwd), PKG_NAME);
22
- spawn.sync(command, args, { stdio: 'inherit', cwd });
19
+ /**
20
+ * Version ranges to pin so npm/pnpm don't pull a major that the config doesn't
21
+ * support yet. Mirrors `@linter-spec/eslint-config`'s `peerDependencies` —
22
+ * e.g., ESLint 10 changed `context.getFilename()` to `context.filename`, which
23
+ * crashes `eslint-plugin-react@7` at rule-load time. Keep this in sync with
24
+ * eslint-config's peers if those ranges bump.
25
+ */
26
+ const PINNED_VERSIONS = {
27
+ eslint: '^9.0.0',
28
+ typescript: '^5.6.2',
29
+ 'typescript-eslint': '^8.8.1',
30
+ 'eslint-plugin-react': '^7.37.1',
31
+ 'eslint-plugin-react-hooks': '^5.0.0',
32
+ 'eslint-plugin-jsx-a11y': '^6.10.0',
33
+ 'eslint-plugin-vue': '^9.28.0',
34
+ 'vue-eslint-parser': '^9.4.3',
35
+ 'eslint-plugin-n': '^17.10.3',
36
+ };
37
+ function pin(name) {
38
+ const v = PINNED_VERSIONS[name];
39
+ return v ? `${name}@${v}` : name;
40
+ }
41
+ /**
42
+ * The npm packages to install as devDependencies of the user's project so that
43
+ * the generated `eslint.config.mjs` (and friends) can resolve everything they
44
+ * need — including peers of `@linter-spec/eslint-config` that are marked
45
+ * `optional` in its `peerDependenciesMeta` and therefore would NOT be installed
46
+ * transitively just by adding `@linter-spec/cli`.
47
+ */
48
+ export function projectDepsToInstall(config) {
49
+ const deps = new Set([PKG_NAME]);
50
+ if (config.enableESLint) {
51
+ deps.add('eslint');
52
+ deps.add('@linter-spec/eslint-config');
53
+ const t = config.eslintType;
54
+ const isTs = t.startsWith('typescript');
55
+ const isReact = t === 'react' || t === 'typescript/react';
56
+ const isVue = t === 'vue' || t === 'typescript/vue';
57
+ const isNode = t === 'node' || t === 'typescript/node';
58
+ if (isTs) {
59
+ deps.add('typescript');
60
+ deps.add('typescript-eslint');
61
+ }
62
+ if (isReact) {
63
+ deps.add('eslint-plugin-react');
64
+ deps.add('eslint-plugin-react-hooks');
65
+ deps.add('eslint-plugin-jsx-a11y');
66
+ }
67
+ if (isVue) {
68
+ deps.add('eslint-plugin-vue');
69
+ deps.add('vue-eslint-parser');
70
+ }
71
+ if (isNode) {
72
+ deps.add('eslint-plugin-n');
73
+ }
74
+ }
75
+ if (config.enableStylelint) {
76
+ deps.add('stylelint');
77
+ deps.add('@linter-spec/stylelint-config');
78
+ }
79
+ if (config.enableMarkdownlint) {
80
+ deps.add('markdownlint-cli2');
81
+ deps.add('@linter-spec/markdownlint-config');
82
+ }
83
+ if (config.enablePrettier) {
84
+ deps.add('prettier');
85
+ }
86
+ return [...deps].map(pin);
87
+ }
88
+ /**
89
+ * Install everything the chosen lint setup needs (CLI + eslint-config +
90
+ * project-type-specific plugins + selected toolchains) as devDeps of `cwd`.
91
+ */
92
+ export async function installProjectDeps(cwd, config) {
93
+ const pm = detectPackageManager(cwd);
94
+ const pkgs = projectDepsToInstall(config);
95
+ const [command, args] = addDevCommand(pm, pkgs);
96
+ const result = spawn.sync(command, args, { stdio: 'inherit', cwd });
97
+ if (result.status !== 0) {
98
+ throw new Error(`Failed to install project dependencies (\`${command} ${args.join(' ')}\` exited with ${result.status}).`);
99
+ }
23
100
  }
24
101
  /**
25
102
  * When a project relies on the bundled lint configs but has no `node_modules`,
@@ -0,0 +1,30 @@
1
+ <%_
2
+ // Only emit a tsconfig for TypeScript project types. The init flow skips
3
+ // templates that render to an empty string.
4
+ if (!eslintType.startsWith('typescript')) { return ''; }
5
+
6
+ const isReact = eslintType === 'typescript/react';
7
+ const isVue = eslintType === 'typescript/vue';
8
+ const isNode = eslintType === 'typescript/node';
9
+ // Node-only projects don't need DOM types; everything else (plain TS, React, Vue) does.
10
+ const lib = isNode ? ['ES2022'] : ['ES2022', 'DOM', 'DOM.Iterable'];
11
+ _%>
12
+ {
13
+ "compilerOptions": {
14
+ "target": "ES2022",
15
+ "module": "ESNext",
16
+ "moduleResolution": "Bundler",
17
+ "lib": [<%- lib.map((l) => `"${l}"`).join(', ') %>],<% if (isReact) { %>
18
+ "jsx": "react-jsx",<% } %><% if (isVue) { %>
19
+ "jsx": "preserve",<% } %>
20
+ "strict": true,
21
+ "esModuleInterop": true,
22
+ "forceConsistentCasingInFileNames": true,
23
+ "skipLibCheck": true,
24
+ "resolveJsonModule": true,
25
+ "isolatedModules": true,
26
+ "noEmit": true
27
+ },
28
+ "include": ["**/*.ts"<% if (isReact) { %>, "**/*.tsx"<% } %><% if (isVue) { %>, "**/*.vue"<% } %>],
29
+ "exclude": ["node_modules", "dist", "build", "coverage", ".next", ".nuxt", ".turbo"]
30
+ }
@@ -6,6 +6,7 @@ import { confirm } from '@inquirer/prompts';
6
6
  import log from './log.js';
7
7
  import { messages } from './messages.js';
8
8
  import { CliAbortError } from './errors.js';
9
+ import { SKIP_IF_EXISTS } from './generate-template.js';
9
10
  const dirname = path.dirname(fileURLToPath(import.meta.url));
10
11
  // Remove these exact dependencies (they conflict or are superseded).
11
12
  const packageNamesToRemove = [
@@ -55,6 +56,7 @@ const checkUselessConfig = (cwd) => []
55
56
  const checkReWriteConfig = (cwd) => fg
56
57
  .sync('**/*.ejs', { cwd: path.resolve(dirname, '../config'), dot: true })
57
58
  .map((name) => name.replace(/^_/, '.').replace(/\.ejs$/, ''))
59
+ .filter((filename) => !SKIP_IF_EXISTS.has(path.basename(filename)))
58
60
  .filter((filename) => fs.existsSync(path.resolve(cwd, filename)));
59
61
  export default async function conflictResolve(cwd, rewriteConfig) {
60
62
  const pkgPath = path.resolve(cwd, 'package.json');
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Files we will never silently overwrite — user customisations would be lost.
3
+ * The conflict-resolve step already prompts before regenerating known lint
4
+ * configs; these are extra files we only seed on a clean project.
5
+ */
6
+ export declare const SKIP_IF_EXISTS: Set<string>;
1
7
  /**
2
8
  * Render the EJS config templates into `cwd`.
3
9
  * @param cwd target project root
@@ -39,6 +39,12 @@ const mergeVSCodeConfig = (filepath, content) => {
39
39
  return '';
40
40
  }
41
41
  };
42
+ /**
43
+ * Files we will never silently overwrite — user customisations would be lost.
44
+ * The conflict-resolve step already prompts before regenerating known lint
45
+ * configs; these are extra files we only seed on a clean project.
46
+ */
47
+ export const SKIP_IF_EXISTS = new Set(['tsconfig.json']);
42
48
  /**
43
49
  * Render the EJS config templates into `cwd`.
44
50
  * @param cwd target project root
@@ -49,7 +55,10 @@ export default function generateTemplate(cwd, data, vscode) {
49
55
  const templatePath = path.resolve(dirname, '../config');
50
56
  const templates = fg.sync(`${vscode ? '_vscode' : '**'}/*.ejs`, { cwd: templatePath, dot: true });
51
57
  for (const name of templates) {
52
- const filepath = path.resolve(cwd, name.replace(/\.ejs$/, '').replace(/^_/, '.'));
58
+ const outName = name.replace(/\.ejs$/, '').replace(/^_/, '.');
59
+ const filepath = path.resolve(cwd, outName);
60
+ if (SKIP_IF_EXISTS.has(path.basename(outName)) && fs.existsSync(filepath))
61
+ continue;
53
62
  let content = ejs.render(fs.readFileSync(path.resolve(templatePath, name), 'utf8'), {
54
63
  eslintIgnores: ESLINT_IGNORE_GLOBS,
55
64
  stylelintExt: STYLELINT_FILE_EXT,
@@ -12,8 +12,8 @@ export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'cnpm';
12
12
  * `pnpm` happened to be on PATH and so polluted lockfiles in yarn/bun projects.
13
13
  */
14
14
  export declare function detectPackageManager(cwd?: string): PackageManager;
15
- /** `[command, args]` to add a dev dependency, per package manager. */
16
- export declare function addDevCommand(pm: PackageManager, pkg: string): [string, string[]];
15
+ /** `[command, args]` to add one or more dev dependencies, per package manager. */
16
+ export declare function addDevCommand(pm: PackageManager, pkg: string | string[]): [string, string[]];
17
17
  /** `[command, args]` to add a global package, per package manager. */
18
18
  export declare function addGlobalCommand(pm: PackageManager, pkg: string): [string, string[]];
19
19
  /** `[command, args]` to install all dependencies — `<pm> install` works for all. */
package/dist/utils/npm.js CHANGED
@@ -48,15 +48,16 @@ export function detectPackageManager(cwd = process.cwd()) {
48
48
  // 4. Fallback.
49
49
  return 'npm';
50
50
  }
51
- /** `[command, args]` to add a dev dependency, per package manager. */
51
+ /** `[command, args]` to add one or more dev dependencies, per package manager. */
52
52
  export function addDevCommand(pm, pkg) {
53
+ const list = Array.isArray(pkg) ? pkg : [pkg];
53
54
  switch (pm) {
54
55
  case 'yarn':
55
- return ['yarn', ['add', '-D', pkg]];
56
+ return ['yarn', ['add', '-D', ...list]];
56
57
  case 'bun':
57
- return ['bun', ['add', '-d', pkg]];
58
+ return ['bun', ['add', '-d', ...list]];
58
59
  default: // npm / pnpm / cnpm
59
- return [pm, ['i', '-D', pkg]];
60
+ return [pm, ['i', '-D', ...list]];
60
61
  }
61
62
  }
62
63
  /** `[command, args]` to add a global package, per package manager. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linter-spec/cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "One-command lint toolchain (ESLint / Stylelint / markdownlint / Prettier / commitlint) for the linter-spec spec.",
5
5
  "type": "module",
6
6
  "author": "SotherWind",
@@ -61,9 +61,9 @@
61
61
  "stylelint": "^17.13.0",
62
62
  "terminal-link": "^5.0.0",
63
63
  "text-table": "^0.2.0",
64
- "@linter-spec/commitlint-config": "^1.0.0",
65
- "@linter-spec/eslint-config": "^1.0.0",
66
64
  "@linter-spec/markdownlint-config": "^1.0.0",
65
+ "@linter-spec/eslint-config": "^1.0.0",
66
+ "@linter-spec/commitlint-config": "^1.0.0",
67
67
  "@linter-spec/stylelint-config": "^1.0.0"
68
68
  },
69
69
  "devDependencies": {