ember.nvp 0.1.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 +2 -2
  3. package/.github/workflows/publish.yml +2 -2
  4. package/.prettierignore +2 -1
  5. package/.prettierrc.js +10 -0
  6. package/.release-plan.json +11 -3
  7. package/CHANGELOG.md +37 -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 +30 -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 +1 -4
  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 +61 -7
  51. package/src/layers/qunit/index.js +83 -34
  52. package/src/layers/release-plan/index.js +5 -0
  53. package/src/layers/renovate/files/app.json5 +14 -0
  54. package/src/layers/renovate/files/library.json5 +16 -0
  55. package/src/layers/renovate/index.js +68 -0
  56. package/src/layers/typescript/app.test.ts +93 -0
  57. package/src/layers/typescript/index.js +145 -0
  58. package/src/layers/vitest/files/vitest.config.mjs +1 -5
  59. package/src/layers/vitest/index.js +43 -26
  60. package/src/utils/babel.js +76 -0
  61. package/src/utils/buffer.js +15 -0
  62. package/src/utils/cli.js +45 -0
  63. package/src/utils/cwd.js +1 -0
  64. package/src/utils/fs.js +85 -0
  65. package/src/utils/git.js +41 -0
  66. package/src/utils/git.test.ts +36 -0
  67. package/src/utils/manifest.js +9 -0
  68. package/src/utils/npm.js +29 -0
  69. package/src/utils/object.js +10 -0
  70. package/src/utils/project.js +249 -0
  71. package/src/utils/remove-types.js +90 -0
  72. package/src/utils/types.ts +48 -0
  73. package/test/ember.nvp/cli.test.ts +12 -0
  74. package/test/ember.nvp/layers.test.ts +35 -0
  75. package/test/ember.nvp/permutations.test.ts +200 -0
  76. package/test/manual.sh +11 -0
  77. package/test/test-helpers/permutate.test.ts +70 -0
  78. package/test/test-helpers.ts +175 -0
  79. package/tsconfig.json +10 -2
  80. package/vitest.config.js +9 -0
  81. package/src/layers/eslint/index.js +0 -42
  82. package/src/layers/minimal/files/app/app.ts +0 -10
  83. package/src/layers/minimal/files/app/config.ts +0 -18
  84. package/src/layers/minimal/files/app/router.ts +0 -9
  85. package/src/layers/minimal/files/app/templates/application.gts +0 -7
  86. package/src/layers/minimal/files-js/babel.config.js +0 -38
  87. package/src/layers/minimal/index.js +0 -89
  88. package/src/layers/prettier/files/.prettierrc.cjs +0 -15
  89. /package/src/{layers/minimal → bases/minimal-app}/files/vite.config.mjs +0 -0
  90. /package/src/layers/{eslint/files/eslint.config.mjs → eslint-ejected/files/eslint.config.js} +0 -0
  91. /package/src/layers/{minimal/files/app/routes → qunit/files/tests/application}/.gitkeep +0 -0
  92. /package/src/layers/qunit/files/tests/{integration → rendering}/.gitkeep +0 -0
@@ -0,0 +1,91 @@
1
+ import { beforeAll, describe, it, expect as hardExpect, afterAll } from "vitest";
2
+ import { expectIsSetup, generate, layers, reapply } from "#test-helpers";
3
+
4
+ import type { Project } from "ember.nvp";
5
+ import { rm } from "node:fs/promises";
6
+
7
+ const expect = hardExpect.soft;
8
+
9
+ let layer = layers.find((layer) => layer.name === "eslint-bundled-nvp")!;
10
+
11
+ describe("starting without eslint-bundled-nvp", () => {
12
+ let project: Project;
13
+
14
+ beforeAll(async () => {
15
+ project = await generate({
16
+ type: "app",
17
+ packageManager: "pnpm",
18
+ layers: ["git"],
19
+ });
20
+ });
21
+
22
+ afterAll(async () => {
23
+ await rm(project.directory, { recursive: true, force: true });
24
+ });
25
+
26
+ it("applying eslint-bundled-nvp", async () => {
27
+ await reapply(project, ["eslint-bundled-nvp"]);
28
+
29
+ await expectIsSetup(project, layer);
30
+ expect(await project.gitHasDiff()).toBe(false);
31
+ });
32
+ });
33
+
34
+ describe("starting with eslint-bundled-nvp", () => {
35
+ let project: Project;
36
+
37
+ beforeAll(async () => {
38
+ project = await generate({
39
+ type: "app",
40
+ packageManager: "pnpm",
41
+ layers: ["eslint-bundled-nvp", "git"],
42
+ });
43
+ });
44
+
45
+ afterAll(async () => {
46
+ await rm(project.directory, { recursive: true, force: true });
47
+ });
48
+
49
+ it("reapplying eslint-bundled-nvp causes no changes", async () => {
50
+ await reapply(project, ["eslint-bundled-nvp"]);
51
+
52
+ await expectIsSetup(project, layer);
53
+ expect(await project.gitHasDiff()).toBe(false);
54
+ });
55
+ });
56
+
57
+ describe("package.json scripts", () => {
58
+ let project: Project;
59
+
60
+ beforeAll(async () => {
61
+ project = await generate({
62
+ type: "app",
63
+ packageManager: "pnpm",
64
+ layers: ["eslint-bundled-nvp", "git"],
65
+ });
66
+
67
+ let { exitCode } = await project.run("pnpm install");
68
+
69
+ hardExpect(exitCode).toBe(0);
70
+
71
+ await expectIsSetup(project, layer);
72
+ });
73
+
74
+ afterAll(async () => {
75
+ await rm(project.directory, { recursive: true, force: true });
76
+ });
77
+
78
+ it("linting and fixing works", async () => {
79
+ {
80
+ let { exitCode } = await project.run("pnpm lint:eslint --fix");
81
+
82
+ expect(exitCode).toBe(0);
83
+ }
84
+
85
+ {
86
+ let { exitCode } = await project.run("pnpm lint:eslint");
87
+
88
+ expect(exitCode).toBe(0);
89
+ }
90
+ });
91
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * This config simultaneously supports apps, libraries, typescript, etc.
3
+ *
4
+ * but in a way that abstracts the dependencies and configuration
5
+ * out of your project.
6
+ *
7
+ * Handles: (G)TS + (G)JS, QUnit, Supporting Node files
8
+ * for apps and libraries
9
+ */
10
+ import { configs } from "@nullvoxpopuli/eslint-configs";
11
+
12
+ export default [
13
+ ...configs.ember(import.meta.dirname),
14
+ // your modifications here
15
+ // see: https://eslint.org/docs/user-guide/configuring/configuration-files#how-do-overrides-work
16
+ ];
@@ -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", "NullVoxPopuli's encapsulated config"),
14
+ hint: `Minimal dependencies added to package.json`,
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
+ "@nullvoxpopuli/eslint-configs": "^5.5.0",
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, ["@nullvoxpopuli/eslint-configs", "eslint"])) {
62
+ reasons.push("missing required dependencies: @nullvoxpopuli/eslint-configs, 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
+ };
@@ -0,0 +1,95 @@
1
+ import { getLatest } from "#utils/npm.js";
2
+ import { packageJson, files } from "ember-apply";
3
+ import { formatLabel } from "#utils/cli.js";
4
+ import { hasDevDeps } from "#utils/manifest.js";
5
+ import { maybeLintWithConcurrently } from "#consolidators/linting.js";
6
+ import { existsSync } from "node:fs";
7
+ import { join } from "node:path";
8
+
9
+ const toInstall = {
10
+ "@babel/eslint-parser": "^7.28.6",
11
+ "@eslint/js": "^9.39.2",
12
+ "eslint-config-prettier": "^10.1.8",
13
+ "eslint-plugin-ember": "^12.7.5",
14
+ "eslint-plugin-n": "^17.23.2",
15
+ "eslint-plugin-qunit": "^8.2.5",
16
+ "typescript-eslint": "^8.53.0",
17
+ eslint: "^9.39.2",
18
+ };
19
+
20
+ /**
21
+ * @type {import('#types').Layer}
22
+ */
23
+ export default {
24
+ label: formatLabel("ESLint", "fully ejected, all of the dependencies"),
25
+
26
+ async run(project) {
27
+ await files.applyFolder(join(import.meta.dirname, "files"), project.directory);
28
+
29
+ await packageJson.addDevDependencies(await getLatest(toInstall), project.directory);
30
+
31
+ await packageJson.addScripts(
32
+ {
33
+ "lint:eslint": "eslint . --cache",
34
+ "lint:eslint:fix": "eslint . --fix",
35
+ },
36
+ project.directory,
37
+ );
38
+
39
+ await maybeLintWithConcurrently(project);
40
+ },
41
+
42
+ /**
43
+ * @overload
44
+ * @param {import('#utils/project.js').Project} project
45
+ * @param {true} explain
46
+ * @returns {Promise<{ isSetup: boolean; reasons: string[] }>}
47
+ */
48
+ /**
49
+ * @overload
50
+ * @param {import('#utils/project.js').Project} project
51
+ * @param {boolean | undefined} [explain]
52
+ * @returns {Promise<boolean>}
53
+ */
54
+ async isSetup(project, explain) {
55
+ const reasons = [];
56
+
57
+ if (!existsSync(join(project.directory, "eslint.config.js"))) {
58
+ reasons.push("eslint.config.js is missing");
59
+
60
+ if (!explain) return false;
61
+ }
62
+
63
+ let manifest = await packageJson.read(project.directory);
64
+
65
+ if (!hasDevDeps(manifest, toInstall)) {
66
+ reasons.push("missing required dependencies");
67
+
68
+ if (!explain) return false;
69
+ }
70
+
71
+ let scripts = Object.values(manifest.scripts ?? {}).filter((script) =>
72
+ script.includes("eslint"),
73
+ );
74
+
75
+ if (!scripts.some((script) => script.includes("--cache"))) {
76
+ reasons.push("missing eslint script with --cache flag");
77
+
78
+ if (!explain) return false;
79
+ }
80
+ if (!scripts.some((script) => script.includes("--fix"))) {
81
+ reasons.push("missing eslint script with --fix flag");
82
+
83
+ if (!explain) return false;
84
+ }
85
+
86
+ if (explain) {
87
+ return {
88
+ isSetup: reasons.length === 0,
89
+ reasons,
90
+ };
91
+ }
92
+
93
+ return reasons.length === 0;
94
+ },
95
+ };
@@ -0,0 +1,33 @@
1
+ import * as p from "@clack/prompts";
2
+ import { hasGit, initGit, isInGit } from "#utils/git.js";
3
+ import { formatLabel } from "#utils/cli.js";
4
+
5
+ export default {
6
+ label: formatLabel("git init"),
7
+ hint: "set up this project as a fresh git repository",
8
+
9
+ /**
10
+ * @param {import('#utils/project.js').Project} project
11
+ */
12
+ async defaultValue(project) {
13
+ return !isInGit();
14
+ },
15
+
16
+ /**
17
+ * @param {import('#utils/project.js').Project} project
18
+ */
19
+ async run(project) {
20
+ if (hasGit(project.directory)) {
21
+ return;
22
+ }
23
+
24
+ return initGit(project.directory);
25
+ },
26
+
27
+ /**
28
+ * @param {import('#utils/project.js').Project} project
29
+ */
30
+ async isSetup(project) {
31
+ return hasGit(project.directory);
32
+ },
33
+ };
@@ -0,0 +1,73 @@
1
+ import { beforeAll, describe, it, expect as hardExpect, afterAll } from "vitest";
2
+ import { generate, layers, reapply } from "#test-helpers";
3
+
4
+ import type { Project } from "ember.nvp";
5
+ import { rm, readFile } from "node:fs/promises";
6
+
7
+ const expect = hardExpect.soft;
8
+
9
+ let githubActionsLayer = layers.find((layer) => layer.name === "github-actions")!;
10
+
11
+ for (let packageManager of ["pnpm", "npm"] as const) {
12
+ describe(packageManager, () => {
13
+ let project: Project;
14
+
15
+ beforeAll(async () => {
16
+ project = await generate({
17
+ type: "app",
18
+ packageManager,
19
+ layers: ["github-actions"],
20
+ });
21
+ });
22
+
23
+ afterAll(async () => {
24
+ await rm(project.directory, { recursive: true, force: true });
25
+ });
26
+
27
+ it("did not emit a CI.yml, because it wouldn't be used", async () => {
28
+ let result = await githubActionsLayer.isSetup(project);
29
+
30
+ expect(await project.read(".github/workflows/ci.yml")).toMatchInlineSnapshot(`undefined`);
31
+ expect(result).toBe(true);
32
+ });
33
+
34
+ it("after emitting with an eslint layer", async () => {
35
+ await reapply(project, ["eslint-bundled-nvp", "github-actions"]);
36
+
37
+ let result = await githubActionsLayer.isSetup(project);
38
+
39
+ expect(result).toBe(true);
40
+ });
41
+
42
+ it("does not have commands for the 'other' packageManager(s)", async () => {
43
+ let ciYamlPath = project.path(".github/workflows/ci.yml");
44
+ let ciYamlContent = await readFile(ciYamlPath, "utf-8");
45
+
46
+ if (packageManager === "pnpm") {
47
+ expect(ciYamlContent).not.toContain(" npm install");
48
+ expect(ciYamlContent).toContain("wyvox/action-setup-pnpm");
49
+ expect(ciYamlContent).toContain("pnpm lint");
50
+ expect(ciYamlContent).not.toContain(" npm lint");
51
+ expect(await githubActionsLayer.isSetup(project, true)).toMatchInlineSnapshot(`
52
+ {
53
+ "isSetup": true,
54
+ "reasons": [],
55
+ }
56
+ `);
57
+ }
58
+
59
+ if (packageManager === "npm") {
60
+ expect(ciYamlContent).not.toContain("wyvox/action-setup-pnpm");
61
+ expect(ciYamlContent).toContain(" npm install");
62
+ expect(ciYamlContent).toContain(" npm run lint");
63
+ expect(ciYamlContent).not.toContain("pnpm lint");
64
+ expect(await githubActionsLayer.isSetup(project, true)).toMatchInlineSnapshot(`
65
+ {
66
+ "isSetup": true,
67
+ "reasons": [],
68
+ }
69
+ `);
70
+ }
71
+ });
72
+ });
73
+ }
@@ -0,0 +1,200 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+
5
+ /**
6
+ * @type {import('#types').Layer}
7
+ */
8
+ export default {
9
+ label: "GitHub Actions",
10
+ async run(project) {
11
+ let result = await transform(project);
12
+
13
+ if (result.didChange) {
14
+ await mkdir(project.path(".github/workflows"), { recursive: true });
15
+ await writeFile(result.outputPath, result.content, "utf-8");
16
+ }
17
+ },
18
+ /**
19
+ * @overload
20
+ * @param {import('#utils/project.js').Project} project
21
+ * @param {true} explain
22
+ * @returns {Promise<{ isSetup: boolean; reasons: string[] }>}
23
+ */
24
+ /**
25
+ * @overload
26
+ * @param {import('#utils/project.js').Project} project
27
+ * @param {boolean | undefined} [explain]
28
+ * @returns {Promise<boolean>}
29
+ */
30
+ async isSetup(project, explain) {
31
+ const reasons = [];
32
+
33
+ let result = await transform(project);
34
+
35
+ if (result.didChange) {
36
+ reasons.push(
37
+ `GitHub Actions workflow needs updates. Was ${result.original}, now ${result.content}`,
38
+ );
39
+
40
+ if (!explain) return false;
41
+ }
42
+
43
+ if (explain) {
44
+ return {
45
+ isSetup: reasons.length === 0,
46
+ reasons,
47
+ };
48
+ }
49
+
50
+ return reasons.length === 0;
51
+ },
52
+ };
53
+
54
+ /**
55
+ * This is a very silly way to handle YAML, but 🤷
56
+ * will update if this starts getting to be troublesome.
57
+ */
58
+ const preamble = `name: CI
59
+
60
+ on:
61
+ push:
62
+ branches:
63
+ - main
64
+ - master
65
+ pull_request: {}
66
+
67
+ concurrency:
68
+ group: ci-\${{ github.head_ref || github.ref }}
69
+ cancel-in-progress: true`;
70
+
71
+ const lint = {
72
+ npm: `lint:
73
+ name: "Lints"
74
+ runs-on: ubuntu-latest
75
+ steps:
76
+ - uses: actions/checkout@v6
77
+ - uses: actions/setup-node@v6
78
+ with:
79
+ node-version: 24
80
+ - run: npm install
81
+ - run: npm run lint`,
82
+ pnpm: ` lint:
83
+ name: "Lints"
84
+ runs-on: ubuntu-latest
85
+ steps:
86
+ - uses: actions/checkout@v6
87
+ - uses: wyvox/action-setup-pnpm@v3
88
+ - run: pnpm lint`,
89
+ };
90
+
91
+ const test = {
92
+ npm: `test:
93
+ name: "Tests"
94
+ runs-on: ubuntu-latest
95
+ steps:
96
+ - uses: actions/checkout@v6
97
+ - uses: actions/setup-node@v6
98
+ with:
99
+ node-version: 24
100
+ - run: npm install
101
+ - run: npm test`,
102
+ pnpm: ` test:
103
+ name: "Tests"
104
+ runs-on: ubuntu-latest
105
+ steps:
106
+ - uses: actions/checkout@v6
107
+ - uses: wyvox/action-setup-pnpm@v3
108
+ - run: pnpm test`,
109
+ };
110
+
111
+ /**
112
+ *
113
+ * @param {import('#utils/project.js').Project} project
114
+ * @returns
115
+ */
116
+ async function transform(project) {
117
+ let outputPath = join(project.directory, ".github/workflows/ci.yml");
118
+ let content = "";
119
+ let original = "";
120
+
121
+ if (existsSync(outputPath)) {
122
+ let buffer = await readFile(outputPath, "utf-8");
123
+ content = buffer.toString();
124
+ original = content;
125
+ }
126
+
127
+ content = await addOrUpdateLints(project, content);
128
+ content = await addOrUpdateTests(project, content);
129
+
130
+ /**
131
+ * If we've still added nothing, we can't actually add the preamble
132
+ * because there are no jobs to run (we need at least one job).
133
+ */
134
+ if (content === "") {
135
+ return { content, original, didChange: original !== content, outputPath };
136
+ }
137
+
138
+ content = await addOrUpdateBase(project, content);
139
+
140
+ return { content, original, didChange: original !== content, outputPath };
141
+ }
142
+
143
+ /**
144
+ *
145
+ * @param {import('#utils/project.js').Project} project
146
+ * @param {string} file
147
+ */
148
+ async function addOrUpdateBase(project, file) {
149
+ if (file.includes("jobs:")) {
150
+ if (file.includes(preamble)) {
151
+ return file;
152
+ }
153
+
154
+ return preamble + file;
155
+ }
156
+
157
+ return file;
158
+ }
159
+
160
+ /**
161
+ *
162
+ * @param {import('#utils/project.js').Project} project
163
+ * @param {string} file
164
+ */
165
+ async function addOrUpdateLints(project, file) {
166
+ if (!(await project.hasOrWantsLayer("eslint"))) {
167
+ return file;
168
+ }
169
+
170
+ if (file.includes("lint:")) {
171
+ return file;
172
+ }
173
+
174
+ if (!file.includes("jobs:")) {
175
+ file += "\n\njobs:\n";
176
+ }
177
+
178
+ return file + "\n" + lint[project.desires.packageManager];
179
+ }
180
+
181
+ /**
182
+ *
183
+ * @param {import('#utils/project.js').Project} project
184
+ * @param {string} file
185
+ */
186
+ async function addOrUpdateTests(project, file) {
187
+ if (!(await project.hasOrWantsLayer("testing"))) {
188
+ return file;
189
+ }
190
+
191
+ if (file.includes("test:")) {
192
+ return file;
193
+ }
194
+
195
+ if (!file.includes("jobs:")) {
196
+ file += "\n\njobs:\n";
197
+ }
198
+
199
+ return file + "\n" + test[project.desires.packageManager];
200
+ }
@@ -0,0 +1,47 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ /**
5
+ * @type {Set<string>}
6
+ */
7
+ export const TODO = new Set(["vitest", "release-plan", "eslint-ejected"]);
8
+
9
+ /**
10
+ * @returns {Promise<Array<import('#types').DiscoveredLayer>>}
11
+ */
12
+ export async function discoverLayers() {
13
+ const layersDir = import.meta.dirname;
14
+ const entries = await readdir(layersDir, { withFileTypes: true });
15
+
16
+ const layers = [];
17
+
18
+ for (const entry of entries) {
19
+ if (TODO.has(entry.name)) {
20
+ continue;
21
+ }
22
+
23
+ if (entry.isDirectory()) {
24
+ const layerPath = join(layersDir, entry.name, "index.js");
25
+ try {
26
+ const layer = await import(layerPath);
27
+ layers.push({
28
+ name: entry.name,
29
+ ...layer.default,
30
+ });
31
+ } catch (error) {
32
+ if (typeof error !== "object" || error === null) {
33
+ console.warn(`Warning: Could not load layer ${entry.name}:`, error);
34
+ continue;
35
+ }
36
+ if ("message" in error) {
37
+ console.warn(`Warning: Could not load layer ${entry.name}:`, error.message);
38
+ }
39
+ }
40
+ }
41
+ }
42
+
43
+ return layers;
44
+ }
45
+
46
+ export const layers = await discoverLayers();
47
+ export const layerNames = layers.map((layer) => layer.name);
@@ -0,0 +1,4 @@
1
+ export default {
2
+ printWidth: 100,
3
+ plugins: ["prettier-plugin-ember-template-tag"],
4
+ };