ember.nvp 0.1.0 → 0.2.1

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.
Files changed (94) hide show
  1. package/.github/workflows/ci.yml +41 -0
  2. package/.github/workflows/plan-release.yml +2 -2
  3. package/.github/workflows/publish.yml +4 -5
  4. package/.prettierignore +2 -1
  5. package/.prettierrc.js +10 -0
  6. package/.release-plan.json +6 -6
  7. package/CHANGELOG.md +53 -0
  8. package/README.md +79 -142
  9. package/package.json +28 -5
  10. package/pnpm-workspace.yaml +3 -0
  11. package/src/bases/README.md +5 -0
  12. package/src/bases/minimal-app/app.test.ts +203 -0
  13. package/src/bases/minimal-app/files/.env.development +8 -0
  14. package/src/bases/minimal-app/files/app/app.ts +21 -0
  15. package/src/bases/minimal-app/files/app/config.ts +17 -0
  16. package/src/bases/minimal-app/files/app/router.ts +51 -0
  17. package/src/bases/minimal-app/files/app/templates/application.gts +5 -0
  18. package/src/{layers/minimal/files-ts → bases/minimal-app/files}/babel.config.js +8 -5
  19. package/src/{layers/minimal → bases/minimal-app}/files/index.html +3 -5
  20. package/src/bases/minimal-app/files/package.json +57 -0
  21. package/src/{layers/minimal → bases/minimal-app}/files/tsconfig.json +0 -6
  22. package/src/bases/minimal-app/index.js +111 -0
  23. package/src/bases/minimal-library/index.js +10 -0
  24. package/src/cli/args.js +57 -0
  25. package/src/cli/generator.js +66 -11
  26. package/src/cli/index.js +44 -13
  27. package/src/cli/questions/layers.js +39 -50
  28. package/src/cli/questions/name.js +22 -6
  29. package/src/cli/questions/ok.js +38 -0
  30. package/src/cli/questions/package-manager.js +23 -2
  31. package/src/cli/questions/path.js +59 -0
  32. package/src/cli/questions/project-type.js +55 -0
  33. package/src/consolidators/linting.js +51 -0
  34. package/src/index.js +2 -0
  35. package/src/layers/eslint-bundled-ember/files/eslint.config.js +14 -0
  36. package/src/layers/eslint-bundled-ember/index.js +91 -0
  37. package/src/layers/eslint-bundled-nvp/app.test.ts +91 -0
  38. package/src/layers/eslint-bundled-nvp/files/eslint.config.js +16 -0
  39. package/src/layers/eslint-bundled-nvp/index.js +91 -0
  40. package/src/layers/eslint-ejected/index.js +95 -0
  41. package/src/layers/git/index.js +33 -0
  42. package/src/layers/github-actions/app.test.ts +73 -0
  43. package/src/layers/github-actions/index.js +200 -0
  44. package/src/layers/index.js +47 -0
  45. package/src/layers/prettier/files/.prettierrc.js +4 -0
  46. package/src/layers/prettier/index.js +45 -18
  47. package/src/layers/qunit/app.test.ts +90 -0
  48. package/src/layers/qunit/files/testem.cjs +26 -0
  49. package/src/layers/qunit/files/tests/index.html +16 -7
  50. package/src/layers/qunit/files/tests/test-helper.ts +65 -7
  51. package/src/layers/qunit/files/tests/unit/macros-test.ts +26 -0
  52. package/src/layers/qunit/index.js +83 -34
  53. package/src/layers/release-plan/index.js +5 -0
  54. package/src/layers/renovate/files/app.json5 +14 -0
  55. package/src/layers/renovate/files/library.json5 +16 -0
  56. package/src/layers/renovate/index.js +68 -0
  57. package/src/layers/typescript/app.test.ts +93 -0
  58. package/src/layers/typescript/index.js +145 -0
  59. package/src/layers/vitest/files/vitest.config.mjs +1 -5
  60. package/src/layers/vitest/index.js +43 -26
  61. package/src/utils/babel.js +76 -0
  62. package/src/utils/buffer.js +15 -0
  63. package/src/utils/cli.js +45 -0
  64. package/src/utils/cwd.js +1 -0
  65. package/src/utils/fs.js +85 -0
  66. package/src/utils/git.js +41 -0
  67. package/src/utils/git.test.ts +36 -0
  68. package/src/utils/manifest.js +9 -0
  69. package/src/utils/npm.js +29 -0
  70. package/src/utils/object.js +10 -0
  71. package/src/utils/project.js +249 -0
  72. package/src/utils/remove-types.js +90 -0
  73. package/src/utils/types.ts +48 -0
  74. package/test/ember.nvp/cli.test.ts +12 -0
  75. package/test/ember.nvp/layers.test.ts +35 -0
  76. package/test/ember.nvp/permutations.test.ts +200 -0
  77. package/test/manual.sh +11 -0
  78. package/test/test-helpers/permutate.test.ts +70 -0
  79. package/test/test-helpers.ts +175 -0
  80. package/tsconfig.json +10 -2
  81. package/vitest.config.js +9 -0
  82. package/src/layers/eslint/index.js +0 -42
  83. package/src/layers/minimal/files/app/app.ts +0 -10
  84. package/src/layers/minimal/files/app/config.ts +0 -18
  85. package/src/layers/minimal/files/app/router.ts +0 -9
  86. package/src/layers/minimal/files/app/templates/application.gts +0 -7
  87. package/src/layers/minimal/files-js/babel.config.js +0 -38
  88. package/src/layers/minimal/index.js +0 -89
  89. package/src/layers/prettier/files/.prettierrc.cjs +0 -15
  90. package/src/layers/qunit/files/tests/unit/.gitkeep +0 -0
  91. /package/src/{layers/minimal → bases/minimal-app}/files/vite.config.mjs +0 -0
  92. /package/src/layers/{eslint/files/eslint.config.mjs → eslint-ejected/files/eslint.config.js} +0 -0
  93. /package/src/layers/{minimal/files/app/routes → qunit/files/tests/application}/.gitkeep +0 -0
  94. /package/src/layers/qunit/files/tests/{integration → rendering}/.gitkeep +0 -0
@@ -1,19 +1,74 @@
1
1
  import { mkdir } from "node:fs/promises";
2
2
 
3
+ import baseApp from "#bases/minimal-app";
4
+ import baseLibrary from "#bases/minimal-library";
5
+ import { consolidateLintingScripts } from "../consolidators/linting.js";
6
+ import { hasGit } from "#utils/git.js";
3
7
  /**
4
8
  * Generate project files by running layer functions
9
+ *
10
+ * @param {import('#utils/project.js').Project} project
5
11
  */
6
- export async function generateProject(projectPath, projectName, layers) {
7
- // Create project directory
8
- await mkdir(projectPath, { recursive: true });
9
-
10
- // Run each layer's run function in sequence
11
- for (const layer of layers) {
12
- if (typeof layer.run === "function") {
13
- await layer.run({
14
- targetDir: projectPath,
15
- projectName,
16
- });
12
+ export async function generateProject(project) {
13
+ await mkdir(project.directory, { recursive: true });
14
+
15
+ switch (project.desires.type) {
16
+ case "app":
17
+ await baseApp.run(project);
18
+ break;
19
+ case "library":
20
+ await baseLibrary.run(project);
21
+ break;
22
+ }
23
+
24
+ /**
25
+ * We could run these in a loop until there is no git diff
26
+ */
27
+ await runLap(project);
28
+
29
+ if (hasGit(project.directory)) {
30
+ await project.gitAdd();
31
+
32
+ if (await project.gitHasDiff()) {
33
+ let layerNames = project.desires.layers.map((l) => l.name).join(", ");
34
+ await project.gitCommit(
35
+ `[ember.nvp] Applied ${layerNames} to ${project.type}: ${project.name} -- Please report issues to https://github.com/NullVoxPopuli/ember.nvp/`,
36
+ );
17
37
  }
18
38
  }
39
+
40
+ await runLap(project);
41
+
42
+ if (hasGit(project.directory) && (await project.gitHasDiff())) {
43
+ await project.gitAdd();
44
+ await project.gitCommit(
45
+ `[ember.nvp] Consolidation commit -- Please report issues to https://github.com/NullVoxPopuli/ember.nvp/`,
46
+ );
47
+ }
48
+
49
+ await runLap(project);
50
+
51
+ if (hasGit(project.directory) && (await project.gitHasDiff())) {
52
+ await project.gitAdd();
53
+ await project.gitCommit(
54
+ `[ember.nvp] Consolidation commit -- Please report issues to https://github.com/NullVoxPopuli/ember.nvp/`,
55
+ );
56
+ }
57
+ }
58
+
59
+ /**
60
+ *
61
+ * @param {import('#utils/project.js').Project} project
62
+ */
63
+ async function runLap(project) {
64
+ for (const layer of project.desires.layers) {
65
+ if (typeof layer.run !== "function") {
66
+ console.warn(`${layer.name} is not implemented`);
67
+ continue;
68
+ }
69
+
70
+ await layer.run(project);
71
+ }
72
+
73
+ await consolidateLintingScripts(project);
19
74
  }
package/src/cli/index.js CHANGED
@@ -1,44 +1,75 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import * as p from "@clack/prompts";
4
- import { join } from "node:path";
5
- import pc from "picocolors";
6
4
  import { generateProject } from "./generator.js";
7
- import { askName } from './questions/name.js';
8
- import { askLayers } from './questions/layers.js';
9
- import { askPackageManager } from './questions/package-manager.js';
5
+ import { askName } from "./questions/name.js";
6
+ import { askLayers } from "./questions/layers.js";
7
+ import { askProjectType } from "./questions/project-type.js";
8
+ import { askPackageManager } from "./questions/package-manager.js";
9
+ import { askPath } from "./questions/path.js";
10
+ import { askIfOK } from "./questions/ok.js";
11
+ import { styleText } from "node:util";
12
+ import { Project } from "#utils/project.js";
10
13
 
14
+ /**
15
+ * This whole file's primary purpose is to be an interactive CLI
16
+ * for `generateProject`.
17
+ *
18
+ * Other tools can call `generateProject` themselves if they wish
19
+ * if they want to provide different TUI/GUIs.
20
+ */
11
21
  async function main() {
12
22
  console.clear();
13
23
 
14
- p.intro(pc.bgCyan(pc.black(" ember.nvp ")));
24
+ p.intro(styleText(["bgCyan", "black"], " ember.nvp "));
15
25
 
16
26
  const projectName = await askName();
27
+ const projectPath = await askPath(projectName);
28
+ const projectType = await askProjectType();
17
29
  const selectedLayers = await askLayers();
18
30
  const packageManager = await askPackageManager();
19
31
 
32
+ let project = new Project(projectPath, {
33
+ name: projectName,
34
+ type: projectType,
35
+ path: projectPath,
36
+ layers: selectedLayers,
37
+ packageManager,
38
+ });
39
+
40
+ await askIfOK();
41
+
20
42
  const s = p.spinner();
21
43
  s.start("Creating your Ember app...");
22
44
 
23
45
  try {
24
46
  // Generate the project
25
- const projectPath = join(process.cwd(), projectName);
26
- await generateProject(projectPath, projectName, selectedLayers);
47
+ await generateProject(project);
27
48
 
28
49
  s.stop("Project created!");
29
50
 
30
51
  p.note(
31
52
  `cd ${projectName}\n` +
32
- `${packageManager} install\n` +
33
- `${packageManager} ${packageManager === "npm" ? "run " : ""}start`,
53
+ `${packageManager} install\n` +
54
+ `${packageManager} ${packageManager === "npm" ? "run " : ""}start`,
34
55
  "Next steps",
35
56
  );
36
57
 
37
- p.outro(pc.green("✓") + " Project ready! " + pc.dim("Happy coding!"));
58
+ p.outro(styleText("green", "✓") + " Project ready! " + styleText("dim", "Happy coding!"));
38
59
  } catch (err) {
39
60
  s.stop("Failed to create project");
40
- p.cancel(`Error: ${err.message}`);
41
- throw err;
61
+
62
+ if (err instanceof Error) {
63
+ p.cancel(`Error: ${err.message}`);
64
+ if (err.stack) {
65
+ console.error(err.stack);
66
+ }
67
+
68
+ process.exit(1);
69
+ }
70
+
71
+ p.cancel(`Error: ${String(err)}`);
72
+ process.exit(1);
42
73
  }
43
74
  }
44
75
 
@@ -1,31 +1,53 @@
1
- import { readdir } from "node:fs/promises";
2
- import { join } from "node:path";
1
+ import { answers, printArgInUse } from "#args";
2
+ import { discoverLayers } from "#layers";
3
3
 
4
4
  import * as p from "@clack/prompts";
5
5
 
6
6
  export async function askLayers() {
7
+ const optionalLayers = (await discoverLayers()).filter(
8
+ (layer) => typeof layer.run === "function",
9
+ );
7
10
 
8
- // Discover available layers
9
- const layers = await discoverLayers();
11
+ const supported = new Set(optionalLayers.map((layer) => layer.name));
10
12
 
11
- // Separate minimal from other layers
12
- const minimalLayer = layers.find((l) => l.name === "minimal");
13
- const optionalLayers = layers.filter((l) => l.name !== "minimal");
13
+ /**
14
+ *
15
+ * @param {string[] | undefined} selected
16
+ * @returns {selected is string[]}
17
+ */
18
+ function isValid(selected) {
19
+ if (!selected) return false;
20
+ if (Array.isArray(selected) && selected.length === 0) return false;
14
21
 
15
- p.note(
16
- `${pc.cyan("minimal")} layer is always included.\n` +
17
- 'It provides: type: "module", no compat, no testing, no linting.\n' +
18
- "Perfect for demos and reproductions.",
19
- "Base Layer",
20
- );
22
+ return selected.every((name) => supported.has(name));
23
+ }
24
+
25
+ if (isValid(answers.layers)) {
26
+ let result = optionalLayers.filter((layer) => answers.layers.includes(layer.name));
27
+
28
+ printArgInUse(`layers`, result.map((layer) => layer.name).join(", "));
29
+
30
+ return result;
31
+ }
32
+
33
+ // Whenever we have a yes/no option, we use string values rather than true/false
34
+ const defaultValues = (
35
+ await Promise.all(
36
+ optionalLayers.map(async (layer) => {
37
+ let result = await layer.defaultValue?.();
38
+
39
+ return result && layer.name;
40
+ }),
41
+ )
42
+ ).filter(Boolean);
21
43
 
22
- // Let user select additional features
23
44
  const selectedFeatures = await p.multiselect({
24
45
  message: "Select additional features:",
46
+ initialValues: defaultValues,
25
47
  options: optionalLayers.map((layer) => ({
26
48
  value: layer.name,
27
- label: layer.label || layer.name,
28
- hint: layer.description,
49
+ label: layer.label ?? layer.name,
50
+ hint: layer.hint,
29
51
  })),
30
52
  required: false,
31
53
  });
@@ -36,40 +58,7 @@ export async function askLayers() {
36
58
  }
37
59
 
38
60
  // Build the final configuration
39
- const selectedLayers = [
40
- minimalLayer,
41
- ...optionalLayers.filter((l) => selectedFeatures.includes(l.name)),
42
- ].filter(Boolean);
61
+ const selectedLayers = optionalLayers.filter((layer) => selectedFeatures.includes(layer.name));
43
62
 
44
63
  return selectedLayers;
45
-
46
- }
47
-
48
- async function discoverLayers() {
49
- const layersDir = join(import.meta.dirname, '..', "..", "layers");
50
- const entries = await readdir(layersDir, { withFileTypes: true });
51
-
52
- const layers = [];
53
-
54
- for (const entry of entries) {
55
- if (entry.isDirectory()) {
56
- const layerPath = join(layersDir, entry.name, "index.js");
57
- try {
58
- const layer = await import(layerPath);
59
- layers.push({
60
- name: entry.name,
61
- ...layer.default,
62
- });
63
- } catch (err) {
64
- // Skip layers that don't have proper exports
65
- console.warn(
66
- `Warning: Could not load layer ${entry.name}:`,
67
- err.message,
68
- );
69
- }
70
- }
71
- }
72
-
73
- return layers;
74
64
  }
75
-
@@ -1,14 +1,30 @@
1
1
  import * as p from "@clack/prompts";
2
2
 
3
+ import packageNameRegex from "package-name-regex";
4
+ import { answers, printArgInUse } from "#args";
5
+
6
+ const DEFAULT = "my-app";
7
+
3
8
  export async function askName() {
9
+ if (answers.name) {
10
+ let isValid = packageNameRegex.test(answers.name);
11
+ if (isValid) {
12
+ printArgInUse("name", answers.name);
13
+
14
+ return answers.name;
15
+ }
16
+ }
4
17
  const projectName = await p.text({
5
18
  message: "What is your project name?",
6
- placeholder: "my-app",
7
- defaultValue: 'my-app',
19
+ placeholder: DEFAULT,
20
+ defaultValue: DEFAULT,
8
21
  validate(value) {
9
- if (value.length === 0) return "Project name is required";
10
- if (!/^[a-z0-9-]+$/.test(value)) {
11
- return "Project name must be lowercase and can only contain letters, numbers, and hyphens";
22
+ if (value.length === 0) return;
23
+
24
+ let isValid = packageNameRegex.test(value);
25
+
26
+ if (!isValid) {
27
+ return "Project name must be a valid npm package-name";
12
28
  }
13
29
  },
14
30
  });
@@ -18,5 +34,5 @@ export async function askName() {
18
34
  return process.exit(0);
19
35
  }
20
36
 
21
- return projectName;
37
+ return projectName ?? DEFAULT;
22
38
  }
@@ -0,0 +1,38 @@
1
+ import * as p from "@clack/prompts";
2
+ import { answers, printArgInUse } from "#args";
3
+
4
+ const SUPPORTED = new Set(["yes", "no"]);
5
+
6
+ /**
7
+ *
8
+ * @param {string | undefined} selected
9
+ * @returns {selected is 'yes' | 'no'}
10
+ */
11
+ function isValid(selected) {
12
+ if (!selected) return false;
13
+
14
+ return SUPPORTED.has(selected);
15
+ }
16
+
17
+ export async function askIfOK() {
18
+ if (isValid(answers.confirm)) {
19
+ printArgInUse("confirm", answers.confirm);
20
+
21
+ return answers.confirm;
22
+ }
23
+
24
+ const answer = await p.select({
25
+ message: "Do all the selections above look ok?",
26
+ options: [
27
+ { value: "yes", label: "yes, generate the project" },
28
+ { value: "no", label: "no, cancel" },
29
+ ],
30
+ });
31
+
32
+ if (p.isCancel(answer) || answer === "no") {
33
+ p.cancel("Operation cancelled");
34
+ return process.exit(0);
35
+ }
36
+
37
+ return answer;
38
+ }
@@ -1,13 +1,35 @@
1
1
  import * as p from "@clack/prompts";
2
+ import { answers, printArgInUse } from "#args";
2
3
 
4
+ const SUPPORTED = new Set(["pnpm", "npm"]);
5
+
6
+ /**
7
+ *
8
+ * @param {string | undefined} selected
9
+ * @returns {selected is 'pnpm' | 'npm'}
10
+ */
11
+ function isValid(selected) {
12
+ if (!selected) return false;
13
+
14
+ return SUPPORTED.has(selected);
15
+ }
16
+
17
+ /**
18
+ *
19
+ * @returns {Promise<import('#types').PackageManager>}
20
+ */
3
21
  export async function askPackageManager() {
22
+ if (isValid(answers.packageManager)) {
23
+ printArgInUse("packageManager", answers.packageManager);
24
+
25
+ return answers.packageManager;
26
+ }
4
27
 
5
28
  const packageManager = await p.select({
6
29
  message: "Which package manager?",
7
30
  options: [
8
31
  { value: "pnpm", label: "pnpm", hint: "Fast, disk space efficient" },
9
32
  { value: "npm", label: "npm", hint: "Node default" },
10
- { value: "yarn", label: "yarn", hint: "Classic alternative" },
11
33
  ],
12
34
  });
13
35
 
@@ -18,4 +40,3 @@ export async function askPackageManager() {
18
40
 
19
41
  return packageManager;
20
42
  }
21
-
@@ -0,0 +1,59 @@
1
+ import * as p from "@clack/prompts";
2
+
3
+ import { join } from "node:path";
4
+ import { answers, printArgInUse } from "#args";
5
+ import { cwd } from "#utils/cwd.js";
6
+
7
+ /**
8
+ *
9
+ * @param {string | undefined} value
10
+ * @returns
11
+ */
12
+ function isValid(value) {
13
+ if (!value) return false;
14
+
15
+ return value.startsWith(".") || value.startsWith("/");
16
+ }
17
+
18
+ /**
19
+ * @param {string} name -- the projectName
20
+ */
21
+ export async function askPath(name) {
22
+ if (answers.path) {
23
+ if (isValid(answers.path)) {
24
+ printArgInUse("path", answers.path);
25
+
26
+ return answers.path;
27
+ }
28
+ }
29
+
30
+ const defaultValue = join(cwd, name);
31
+
32
+ const answer = await p.text({
33
+ message: "Where would you like to place your project?",
34
+ placeholder: defaultValue.replace(cwd, "."),
35
+ defaultValue: defaultValue,
36
+ validate(value) {
37
+ if (value.length === 0) return;
38
+
39
+ if (!isValid(value)) {
40
+ return `Path must start with a '.' or '/' (be either a relative or absolute path)`;
41
+ }
42
+ },
43
+ });
44
+
45
+ if (p.isCancel(answer)) {
46
+ p.cancel("Operation cancelled");
47
+ return process.exit(0);
48
+ }
49
+
50
+ if (answer) {
51
+ if (answer.startsWith(".")) {
52
+ return join(cwd, answer);
53
+ }
54
+
55
+ return answer;
56
+ }
57
+
58
+ return defaultValue;
59
+ }
@@ -0,0 +1,55 @@
1
+ import * as p from "@clack/prompts";
2
+ import { answers, printArgInUse } from "#args";
3
+
4
+ /**
5
+ * addon is alias for library
6
+ */
7
+ const SUPPORTED = new Set(["app", "library", "addon"]);
8
+
9
+ /**
10
+ * @param {string | undefined} selected
11
+ * @returns {selected is "app" | "library" | "addon"}
12
+ */
13
+ function isValid(selected) {
14
+ if (!selected) return false;
15
+
16
+ return SUPPORTED.has(selected);
17
+ }
18
+
19
+ /**
20
+ *
21
+ * @return {Promise<import('#types').ProjectType>}
22
+ */
23
+ export async function askProjectType() {
24
+ if (isValid(answers.type)) {
25
+ printArgInUse("type", answers.type);
26
+
27
+ if (answers.type === "addon") return "library";
28
+
29
+ return answers.type;
30
+ }
31
+
32
+ const answer = await p.select({
33
+ message: "Which type of project?",
34
+ options: [
35
+ { value: "app", label: "web app", hint: "generates html, js, and css to deploy to the web" },
36
+ {
37
+ value: "library",
38
+ label: "library",
39
+ hint: "sharable code, usable by other libraries and apps alike",
40
+ },
41
+ ],
42
+ });
43
+
44
+ if (answer === "library") {
45
+ p.cancel("Not implemented yet, apologies!");
46
+ return process.exit(0);
47
+ }
48
+
49
+ if (p.isCancel(answer)) {
50
+ p.cancel("Operation cancelled");
51
+ return process.exit(0);
52
+ }
53
+
54
+ return answer;
55
+ }
@@ -0,0 +1,51 @@
1
+ import { getLatest } from "#utils/npm.js";
2
+ import { packageJson } from "ember-apply";
3
+
4
+ /**
5
+ * @param {import('#utils/project.js').Project} project
6
+ */
7
+ export async function consolidateLintingScripts(project) {
8
+ // todo
9
+ }
10
+
11
+ /**
12
+ * @param {import('#utils/project.js').Project} project
13
+ */
14
+ export async function maybeLintWithConcurrently(project) {
15
+ /** @type {Record<string, boolean>} */
16
+ let linting = {
17
+ prettier: false,
18
+ eslint: false,
19
+ "ember-template-lint": false,
20
+ stylelint: false,
21
+ };
22
+
23
+ let count = 0;
24
+ let manifest = await packageJson.read(project.directory);
25
+
26
+ for (let script of Object.values(manifest.scripts)) {
27
+ for (let bin of Object.keys(linting)) {
28
+ if (script.includes(bin)) {
29
+ linting[bin] = true;
30
+ count++;
31
+ }
32
+ }
33
+ }
34
+
35
+ if (count > 1) {
36
+ await packageJson.modify(async (json) => {
37
+ json.scripts ||= {};
38
+ json.devDependencies ||= {};
39
+ json.scripts.lint = 'concurrently "pnpm:lint:*(!fix)" --names "lint:" --prefixColors auto';
40
+ json.scripts["lint:fix"] =
41
+ 'concurrently "pnpm:lint:*:fix" --names "fix:" --prefixColors auto && pnpm format';
42
+
43
+ Object.assign(
44
+ json.devDependencies,
45
+ await getLatest({
46
+ concurrently: "^9.2.1",
47
+ }),
48
+ );
49
+ }, project.directory);
50
+ }
51
+ }
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { generateProject } from "./cli/generator.js";
2
+ export { Project } from "./utils/project.js";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * This tries to follow what the official blueprints are doing more closely
3
+ * but in a way that abstracts the dependencies and configuration
4
+ * out of your project.
5
+ *
6
+ * Handles: (G)TS + (G)JS, QUnit, Supporting Node files
7
+ */
8
+ import { ember } from "ember-eslint";
9
+
10
+ export default [
11
+ ...ember.recommended(import.meta.dirname),
12
+ // your modifications here
13
+ // see: https://eslint.org/docs/user-guide/configuring/configuration-files#how-do-overrides-work
14
+ ];
@@ -0,0 +1,91 @@
1
+ import { getLatest } from "#utils/npm.js";
2
+ import { files, packageJson } from "ember-apply";
3
+ import { formatLabel } from "#utils/cli.js";
4
+ import { maybeLintWithConcurrently } from "#consolidators/linting.js";
5
+ import { existsSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { hasDevDeps } from "#utils/manifest.js";
8
+
9
+ /**
10
+ * @type {import('#types').Layer}
11
+ */
12
+ export default {
13
+ label: formatLabel("ESLint", "ember's encapsulated config"),
14
+ hint: `Minimal dependencies added to package.json. Not official, but follows the official configuration`,
15
+
16
+ async run(project) {
17
+ await files.applyFolder(join(import.meta.dirname, "files"), project.directory);
18
+
19
+ await packageJson.addDevDependencies(
20
+ await getLatest({
21
+ "ember-eslint": "^0.6.1",
22
+ eslint: "^9.39.2",
23
+ }),
24
+ project.directory,
25
+ );
26
+
27
+ await packageJson.addScripts(
28
+ {
29
+ "lint:eslint": "eslint . --cache",
30
+ "lint:eslint:fix": "eslint . --fix",
31
+ },
32
+ project.directory,
33
+ );
34
+
35
+ await maybeLintWithConcurrently(project);
36
+ },
37
+
38
+ /**
39
+ * @overload
40
+ * @param {import('#utils/project.js').Project} project
41
+ * @param {true} explain
42
+ * @returns {Promise<{ isSetup: boolean; reasons: string[] }>}
43
+ */
44
+ /**
45
+ * @overload
46
+ * @param {import('#utils/project.js').Project} project
47
+ * @param {boolean | undefined} [explain]
48
+ * @returns {Promise<boolean>}
49
+ */
50
+ async isSetup(project, explain) {
51
+ const reasons = [];
52
+
53
+ if (!existsSync(join(project.directory, "eslint.config.js"))) {
54
+ reasons.push("eslint.config.js is missing");
55
+
56
+ if (!explain) return false;
57
+ }
58
+
59
+ let manifest = await packageJson.read(project.directory);
60
+
61
+ if (!hasDevDeps(manifest, ["ember-eslint", "eslint"])) {
62
+ reasons.push("missing required dependencies: ember-eslint, eslint");
63
+
64
+ if (!explain) return false;
65
+ }
66
+
67
+ let scripts = Object.values(manifest.scripts ?? {}).filter((script) =>
68
+ script.includes("eslint"),
69
+ );
70
+
71
+ if (!scripts.some((script) => script.includes("--cache"))) {
72
+ reasons.push("missing eslint script with --cache flag");
73
+
74
+ if (!explain) return false;
75
+ }
76
+ if (!scripts.some((script) => script.includes("--fix"))) {
77
+ reasons.push("missing eslint script with --fix flag");
78
+
79
+ if (!explain) return false;
80
+ }
81
+
82
+ if (explain) {
83
+ return {
84
+ isSetup: reasons.length === 0,
85
+ reasons,
86
+ };
87
+ }
88
+
89
+ return reasons.length === 0;
90
+ },
91
+ };