ember.nvp 0.0.0 → 0.2.0

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 (92) hide show
  1. package/.github/workflows/ci.yml +41 -0
  2. package/.github/workflows/plan-release.yml +61 -0
  3. package/.github/workflows/publish.yml +41 -0
  4. package/.nvmrc +1 -0
  5. package/.prettierignore +3 -0
  6. package/.prettierrc.js +10 -0
  7. package/.release-plan.json +30 -0
  8. package/CHANGELOG.md +54 -0
  9. package/README.md +159 -1
  10. package/RELEASE.md +27 -0
  11. package/package.json +51 -3
  12. package/pnpm-workspace.yaml +3 -0
  13. package/src/bases/README.md +5 -0
  14. package/src/bases/minimal-app/app.test.ts +203 -0
  15. package/src/bases/minimal-app/files/.env.development +8 -0
  16. package/src/bases/minimal-app/files/app/app.ts +21 -0
  17. package/src/bases/minimal-app/files/app/config.ts +30 -0
  18. package/src/bases/minimal-app/files/app/router.ts +51 -0
  19. package/src/bases/minimal-app/files/app/templates/application.gts +5 -0
  20. package/src/bases/minimal-app/files/babel.config.js +43 -0
  21. package/src/bases/minimal-app/files/index.html +17 -0
  22. package/src/bases/minimal-app/files/package.json +57 -0
  23. package/src/bases/minimal-app/files/tsconfig.json +12 -0
  24. package/src/bases/minimal-app/files/vite.config.mjs +13 -0
  25. package/src/bases/minimal-app/index.js +111 -0
  26. package/src/bases/minimal-library/index.js +10 -0
  27. package/src/cli/args.js +57 -0
  28. package/src/cli/generator.js +74 -0
  29. package/src/cli/index.js +79 -0
  30. package/src/cli/questions/layers.js +64 -0
  31. package/src/cli/questions/name.js +38 -0
  32. package/src/cli/questions/ok.js +38 -0
  33. package/src/cli/questions/package-manager.js +42 -0
  34. package/src/cli/questions/path.js +59 -0
  35. package/src/cli/questions/project-type.js +55 -0
  36. package/src/consolidators/linting.js +51 -0
  37. package/src/index.js +2 -0
  38. package/src/layers/eslint-bundled-ember/files/eslint.config.js +14 -0
  39. package/src/layers/eslint-bundled-ember/index.js +91 -0
  40. package/src/layers/eslint-bundled-nvp/app.test.ts +91 -0
  41. package/src/layers/eslint-bundled-nvp/files/eslint.config.js +16 -0
  42. package/src/layers/eslint-bundled-nvp/index.js +91 -0
  43. package/src/layers/eslint-ejected/files/eslint.config.js +37 -0
  44. package/src/layers/eslint-ejected/index.js +95 -0
  45. package/src/layers/git/index.js +33 -0
  46. package/src/layers/github-actions/app.test.ts +73 -0
  47. package/src/layers/github-actions/index.js +200 -0
  48. package/src/layers/index.js +47 -0
  49. package/src/layers/prettier/files/.prettierignore +18 -0
  50. package/src/layers/prettier/files/.prettierrc.js +4 -0
  51. package/src/layers/prettier/index.js +65 -0
  52. package/src/layers/qunit/app.test.ts +90 -0
  53. package/src/layers/qunit/files/run-tests.mjs +30 -0
  54. package/src/layers/qunit/files/testem.cjs +26 -0
  55. package/src/layers/qunit/files/tests/index.html +30 -0
  56. package/src/layers/qunit/files/tests/test-helper.ts +66 -0
  57. package/src/layers/qunit/files/tests/unit/.gitkeep +0 -0
  58. package/src/layers/qunit/index.js +102 -0
  59. package/src/layers/release-plan/index.js +5 -0
  60. package/src/layers/renovate/files/app.json5 +14 -0
  61. package/src/layers/renovate/files/library.json5 +16 -0
  62. package/src/layers/renovate/index.js +68 -0
  63. package/src/layers/typescript/app.test.ts +93 -0
  64. package/src/layers/typescript/index.js +145 -0
  65. package/src/layers/vitest/files/tests/integration/.gitkeep +0 -0
  66. package/src/layers/vitest/files/tests/test-helper.ts +5 -0
  67. package/src/layers/vitest/files/tests/unit/.gitkeep +0 -0
  68. package/src/layers/vitest/files/vitest.config.mjs +18 -0
  69. package/src/layers/vitest/index.js +66 -0
  70. package/src/utils/babel.js +76 -0
  71. package/src/utils/buffer.js +15 -0
  72. package/src/utils/cli.js +45 -0
  73. package/src/utils/cwd.js +1 -0
  74. package/src/utils/fs.js +85 -0
  75. package/src/utils/git.js +41 -0
  76. package/src/utils/git.test.ts +36 -0
  77. package/src/utils/manifest.js +9 -0
  78. package/src/utils/npm.js +29 -0
  79. package/src/utils/object.js +10 -0
  80. package/src/utils/project.js +249 -0
  81. package/src/utils/remove-types.js +90 -0
  82. package/src/utils/types.ts +48 -0
  83. package/test/ember.nvp/cli.test.ts +12 -0
  84. package/test/ember.nvp/layers.test.ts +35 -0
  85. package/test/ember.nvp/permutations.test.ts +200 -0
  86. package/test/manual.sh +11 -0
  87. package/test/test-helpers/permutate.test.ts +70 -0
  88. package/test/test-helpers.ts +175 -0
  89. package/tsconfig.json +15 -0
  90. package/vitest.config.js +9 -0
  91. /package/src/layers/{eslint/index.js → qunit/files/tests/application/.gitkeep} +0 -0
  92. /package/src/layers/{minimal/index.js → qunit/files/tests/rendering/.gitkeep} +0 -0
@@ -0,0 +1,74 @@
1
+ import { mkdir } from "node:fs/promises";
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";
7
+ /**
8
+ * Generate project files by running layer functions
9
+ *
10
+ * @param {import('#utils/project.js').Project} project
11
+ */
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
+ );
37
+ }
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);
74
+ }
package/src/cli/index.js CHANGED
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+
3
+ import * as p from "@clack/prompts";
4
+ import { generateProject } from "./generator.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";
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
+ */
21
+ async function main() {
22
+ console.clear();
23
+
24
+ p.intro(styleText(["bgCyan", "black"], " ember.nvp "));
25
+
26
+ const projectName = await askName();
27
+ const projectPath = await askPath(projectName);
28
+ const projectType = await askProjectType();
29
+ const selectedLayers = await askLayers();
30
+ const packageManager = await askPackageManager();
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
+
42
+ const s = p.spinner();
43
+ s.start("Creating your Ember app...");
44
+
45
+ try {
46
+ // Generate the project
47
+ await generateProject(project);
48
+
49
+ s.stop("Project created!");
50
+
51
+ p.note(
52
+ `cd ${projectName}\n` +
53
+ `${packageManager} install\n` +
54
+ `${packageManager} ${packageManager === "npm" ? "run " : ""}start`,
55
+ "Next steps",
56
+ );
57
+
58
+ p.outro(styleText("green", "✓") + " Project ready! " + styleText("dim", "Happy coding!"));
59
+ } catch (err) {
60
+ s.stop("Failed to create project");
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);
73
+ }
74
+ }
75
+
76
+ main().catch((err) => {
77
+ console.error(err);
78
+ process.exit(1);
79
+ });
@@ -0,0 +1,64 @@
1
+ import { answers, printArgInUse } from "#args";
2
+ import { discoverLayers } from "#layers";
3
+
4
+ import * as p from "@clack/prompts";
5
+
6
+ export async function askLayers() {
7
+ const optionalLayers = (await discoverLayers()).filter(
8
+ (layer) => typeof layer.run === "function",
9
+ );
10
+
11
+ const supported = new Set(optionalLayers.map((layer) => layer.name));
12
+
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;
21
+
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);
43
+
44
+ const selectedFeatures = await p.multiselect({
45
+ message: "Select additional features:",
46
+ initialValues: defaultValues,
47
+ options: optionalLayers.map((layer) => ({
48
+ value: layer.name,
49
+ label: layer.label ?? layer.name,
50
+ hint: layer.hint,
51
+ })),
52
+ required: false,
53
+ });
54
+
55
+ if (p.isCancel(selectedFeatures)) {
56
+ p.cancel("Operation cancelled");
57
+ return process.exit(0);
58
+ }
59
+
60
+ // Build the final configuration
61
+ const selectedLayers = optionalLayers.filter((layer) => selectedFeatures.includes(layer.name));
62
+
63
+ return selectedLayers;
64
+ }
@@ -0,0 +1,38 @@
1
+ import * as p from "@clack/prompts";
2
+
3
+ import packageNameRegex from "package-name-regex";
4
+ import { answers, printArgInUse } from "#args";
5
+
6
+ const DEFAULT = "my-app";
7
+
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
+ }
17
+ const projectName = await p.text({
18
+ message: "What is your project name?",
19
+ placeholder: DEFAULT,
20
+ defaultValue: DEFAULT,
21
+ validate(value) {
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";
28
+ }
29
+ },
30
+ });
31
+
32
+ if (p.isCancel(projectName)) {
33
+ p.cancel("Operation cancelled");
34
+ return process.exit(0);
35
+ }
36
+
37
+ return projectName ?? DEFAULT;
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
+ }
@@ -0,0 +1,42 @@
1
+ import * as p from "@clack/prompts";
2
+ import { answers, printArgInUse } from "#args";
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
+ */
21
+ export async function askPackageManager() {
22
+ if (isValid(answers.packageManager)) {
23
+ printArgInUse("packageManager", answers.packageManager);
24
+
25
+ return answers.packageManager;
26
+ }
27
+
28
+ const packageManager = await p.select({
29
+ message: "Which package manager?",
30
+ options: [
31
+ { value: "pnpm", label: "pnpm", hint: "Fast, disk space efficient" },
32
+ { value: "npm", label: "npm", hint: "Node default" },
33
+ ],
34
+ });
35
+
36
+ if (p.isCancel(packageManager)) {
37
+ p.cancel("Operation cancelled");
38
+ return process.exit(0);
39
+ }
40
+
41
+ return packageManager;
42
+ }
@@ -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
+ };