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,249 @@
1
+ import { $ } from "execa";
2
+ import { existsSync } from "node:fs";
3
+ import { readFile, rm } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { hasGit } from "#utils/git.js";
6
+ import { assert } from "node:console";
7
+ import { layers, layerNames } from "#layers";
8
+
9
+ /**
10
+ * State container for the project.
11
+ *
12
+ * May eventually include information for discovering existing state
13
+ * about a project.
14
+ */
15
+ export class Project {
16
+ #directory;
17
+ #desires;
18
+
19
+ /**
20
+ *
21
+ * @param {string} atDirectory
22
+ * @param {import('./types.ts').Answers} desires
23
+ */
24
+ constructor(atDirectory, desires) {
25
+ this.#directory = atDirectory;
26
+ this.#desires = desires;
27
+ }
28
+
29
+ /**
30
+ * @type {string}
31
+ */
32
+ get name() {
33
+ return this.desires.name;
34
+ }
35
+
36
+ /**
37
+ * @type {import('#types').ProjectType}
38
+ */
39
+ get type() {
40
+ return this.desires.type;
41
+ }
42
+
43
+ /**
44
+ * @type {import('#types').PackageManager}
45
+ */
46
+ get packageManager() {
47
+ return this.desires.packageManager;
48
+ }
49
+
50
+ /**
51
+ * @type {string}
52
+ */
53
+ get directory() {
54
+ return this.#directory;
55
+ }
56
+
57
+ /**
58
+ * @type {import('./types.ts').Answers}
59
+ */
60
+ get desires() {
61
+ return this.#desires;
62
+ }
63
+
64
+ /**
65
+ * @param {string} name
66
+ * @returns {Promise<boolean>}
67
+ */
68
+ async #hasLayer(name) {
69
+ let layer = layers.find((layer) => layer.name === name);
70
+
71
+ if (!layer) return false;
72
+
73
+ return layer.isSetup(this);
74
+ }
75
+
76
+ /**
77
+ * @param {string} name
78
+ * @returns {Promise<boolean>}
79
+ */
80
+ async hasLayer(name) {
81
+ switch (name) {
82
+ case "eslint": {
83
+ let eslints = layerNames.filter((name) => name.startsWith("eslint"));
84
+ let results = await Promise.all(eslints.map((name) => this.#hasLayer(name)));
85
+
86
+ return results.some(Boolean);
87
+ }
88
+ case "testing": {
89
+ let results = await Promise.all([this.#hasLayer("qunit"), this.#hasLayer("vitest")]);
90
+
91
+ return results.some(Boolean);
92
+ }
93
+ default: {
94
+ return this.#hasLayer(name);
95
+ }
96
+ }
97
+ }
98
+
99
+ /**
100
+ * @param {string} name
101
+ * @returns {boolean}
102
+ */
103
+ wantsLayer(name) {
104
+ switch (name) {
105
+ case "eslint": {
106
+ return this.desires.layers.some((layer) => layer.name.startsWith("eslint"));
107
+ }
108
+ case "testing": {
109
+ return this.desires.layers.some(
110
+ (layer) => layer.name === "qunit" || layer.name === "vitest",
111
+ );
112
+ }
113
+ default: {
114
+ return this.desires.layers.some((layer) => layer.name === name);
115
+ }
116
+ }
117
+ }
118
+
119
+ /**
120
+ * @param {string} name
121
+ * @returns {Promise<boolean>}
122
+ */
123
+ async hasOrWantsLayer(name) {
124
+ return this.wantsLayer(name) || this.hasLayer(name);
125
+ }
126
+
127
+ /**
128
+ * @returns {boolean}
129
+ */
130
+ get wantsTypeScript() {
131
+ return this.wantsLayer("typescript");
132
+ }
133
+
134
+ /**
135
+ *
136
+ * @param {string} relativePath
137
+ * @returns {string}
138
+ */
139
+ path(relativePath) {
140
+ assert(typeof relativePath === "string");
141
+ assert(!relativePath.startsWith("/"), "relativePath must be relative");
142
+ assert(!relativePath.includes(".."), "relativePath must not traverse directories");
143
+
144
+ return join(this.directory, relativePath);
145
+ }
146
+
147
+ /**
148
+ *
149
+ * @param {string} relativePath
150
+ * @returns {boolean}
151
+ */
152
+ hasFile(relativePath) {
153
+ let path = this.path(relativePath);
154
+
155
+ return existsSync(path);
156
+ }
157
+
158
+ /**
159
+ *
160
+ * @param {string} relativePath
161
+ * @returns {Promise<string|undefined>}
162
+ */
163
+ async read(relativePath) {
164
+ let path = this.path(relativePath);
165
+
166
+ if (existsSync(path)) {
167
+ return await readFile(path, "utf-8");
168
+ }
169
+
170
+ return;
171
+ }
172
+
173
+ /**
174
+ *
175
+ * @param {string} relativePath
176
+ */
177
+ async removeFile(relativePath) {
178
+ let path = this.path(relativePath);
179
+
180
+ if (existsSync(path)) {
181
+ await rm(path);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * @param {string} command
187
+ * @returns {import('execa').ResultPromise}
188
+ */
189
+ run(command) {
190
+ return $(command, { cwd: this.directory, shell: true });
191
+ }
192
+
193
+ /**
194
+ * @returns {import('execa').ResultPromise}
195
+ */
196
+ install() {
197
+ return this.run(`${this.desires.packageManager} install`);
198
+ }
199
+
200
+ /**
201
+ * @param {string} [files='.'] files to add to git, defaults to all files
202
+ * @returns {import('execa').ResultPromise | undefined}
203
+ */
204
+ gitAdd(files = ".") {
205
+ if (!hasGit(this.directory)) return;
206
+
207
+ return this.run(`git add ${files}`);
208
+ }
209
+
210
+ /**
211
+ * @param {string} message commit message
212
+ * @returns {import('execa').ResultPromise | undefined}
213
+ */
214
+ gitCommit(message) {
215
+ if (!hasGit(this.directory)) return;
216
+
217
+ return this.run(`git commit -m ${JSON.stringify(message)}`);
218
+ }
219
+
220
+ /**
221
+ * @returns {Promise<string | undefined>}
222
+ */
223
+ async gitLastCommitMessage() {
224
+ if (!hasGit(this.directory)) return;
225
+
226
+ try {
227
+ const result = await this.run("git log -1 --pretty=%B");
228
+ const stdout = result.stdout;
229
+ return typeof stdout === "string" ? stdout.trim() : undefined;
230
+ } catch {
231
+ return;
232
+ }
233
+ }
234
+
235
+ /**
236
+ * @returns {Promise<boolean>}
237
+ */
238
+ async gitHasDiff() {
239
+ if (!hasGit(this.directory)) return false;
240
+
241
+ try {
242
+ await this.run("git diff --exit-code");
243
+ await this.run("git diff --staged --exit-code");
244
+ return false; // exit code 0 means no diff
245
+ } catch {
246
+ return true; // exit code 1 means there is a diff
247
+ }
248
+ }
249
+ }
@@ -0,0 +1,90 @@
1
+ import { Preprocessor } from "content-tag";
2
+ import { removeTypes as babelRemoveTypes } from "babel-remove-types";
3
+ import { substringBytes } from "./buffer.js";
4
+
5
+ /**
6
+ * Inspired by https://github.com/ember-cli/ember-cli/blob/8ea5b2e22d37805f5749a4917e0ef25fac3c9cda/lib/models/blueprint.js#L537
7
+ * @param {*} extension
8
+ * @param {*} code
9
+ * @returns
10
+ */
11
+ export async function removeTypes(extension, code) {
12
+ if (extension === ".gts") {
13
+ return await wrappedRemoveTypes(code, babelRemoveTypes);
14
+ }
15
+
16
+ if (extension === ".ts") {
17
+ return await babelRemoveTypes(code);
18
+ }
19
+
20
+ return code;
21
+ }
22
+
23
+ /**
24
+ *
25
+ * Support for removeTypes
26
+ *
27
+ */
28
+
29
+ const preprocessor = new Preprocessor();
30
+ /**
31
+ * Builds a class field placeholder for a template block.
32
+ * @param {number} i index of the template occurrence
33
+ */
34
+ function replacementClassMember(i = 0) {
35
+ return `template = __TEMPLATE_TAG_${i}__;`;
36
+ }
37
+
38
+ /**
39
+ * Builds an expression placeholder for a template block.
40
+ * @param {number} i index of the template occurrence
41
+ */
42
+ function replacementExpression(i = 0) {
43
+ return `__TEMPLATE_TAG_${i}__`;
44
+ }
45
+
46
+ async function wrappedRemoveTypes(code = "", callback = babelRemoveTypes) {
47
+ // Strip template tags
48
+ const templateTagMatches = preprocessor.parse(code);
49
+ let strippedCode = code;
50
+
51
+ for (const [i, match] of templateTagMatches.entries()) {
52
+ const templateTag = substringBytes(code, match.range.startByte, match.range.endByte);
53
+
54
+ if (match.type === "class-member") {
55
+ strippedCode = strippedCode.replace(templateTag, replacementClassMember(i));
56
+ } else {
57
+ strippedCode = strippedCode.replace(templateTag, replacementExpression(i));
58
+ }
59
+ }
60
+
61
+ // Remove types
62
+ const transformed = await callback(strippedCode);
63
+
64
+ // Readd stripped template tags
65
+ let transformedWithTemplateTag = transformed;
66
+
67
+ for (const [i, match] of templateTagMatches.entries()) {
68
+ const templateTag = substringBytes(code, match.range.startByte, match.range.endByte);
69
+
70
+ if (match.type === "class-member") {
71
+ transformedWithTemplateTag = transformedWithTemplateTag.replace(
72
+ replacementClassMember(i),
73
+ templateTag,
74
+ );
75
+ } else {
76
+ // babel-remove-types uses prettier under the hood, and adds trailing `;` where allowed,
77
+ // so we need to take that into account when restoring the template tags:
78
+ transformedWithTemplateTag = transformedWithTemplateTag.replace(
79
+ `${replacementExpression(i)};`,
80
+ templateTag,
81
+ );
82
+ transformedWithTemplateTag = transformedWithTemplateTag.replace(
83
+ replacementExpression(i),
84
+ templateTag,
85
+ );
86
+ }
87
+ }
88
+
89
+ return transformedWithTemplateTag;
90
+ }
@@ -0,0 +1,48 @@
1
+ import { Project } from "./project.js";
2
+ export type PackageManager = "pnpm" | "npm";
3
+ export type ProjectType = "app" | "library";
4
+
5
+ export interface Layer {
6
+ /**
7
+ * The text to show during selection
8
+ */
9
+ label: string;
10
+ hint?: string;
11
+ defaultValue?: () => unknown;
12
+ /**
13
+ * The function that applies the codemod
14
+ *
15
+ * run _may_ be invoked multiple times,
16
+ * so it's important to not require interaction here
17
+ */
18
+ run: (project: Project) => Promise<void>;
19
+ isSetup: <Explain extends boolean = false>(
20
+ project: Project,
21
+ explain?: Explain,
22
+ ) => Promise<
23
+ Explain extends true
24
+ ? {
25
+ isSetup: boolean;
26
+ reasons: string[];
27
+ }
28
+ : boolean
29
+ >;
30
+ }
31
+
32
+ export interface DiscoveredLayer extends Layer {
33
+ /**
34
+ * The unique name of the layer
35
+ * (exact match of the folder name
36
+ * not provided by the layer
37
+ * )
38
+ */
39
+ name: string;
40
+ }
41
+
42
+ export interface Answers {
43
+ type: ProjectType;
44
+ path: string;
45
+ name: string;
46
+ layers: DiscoveredLayer[];
47
+ packageManager: PackageManager;
48
+ }
@@ -0,0 +1,12 @@
1
+ import { test, expect as hardExpect } from "vitest";
2
+ import { cli } from "#test-helpers";
3
+
4
+ const expect = hardExpect.soft;
5
+
6
+ test.skip("cli works", async () => {
7
+ let { execaPromise, output, input } = cli();
8
+
9
+ await execaPromise;
10
+
11
+ expect(execaPromise.exitCode).toBe(0);
12
+ });
@@ -0,0 +1,35 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { discoverLayers } from "#layers";
3
+
4
+ const layers = await discoverLayers();
5
+
6
+ describe("discoverLayers", () => {
7
+ it("includes expected layer names", async () => {
8
+ const layers = await discoverLayers();
9
+ const layerNames = layers.map((layer) => layer.name);
10
+
11
+ expect(layerNames).toMatchInlineSnapshot(`
12
+ [
13
+ "eslint-bundled-ember",
14
+ "eslint-bundled-nvp",
15
+ "git",
16
+ "github-actions",
17
+ "prettier",
18
+ "qunit",
19
+ "renovate",
20
+ "typescript",
21
+ ]
22
+ `);
23
+ });
24
+
25
+ describe("each layer has minimum API", () => {
26
+ for (let layer of layers) {
27
+ it(layer.name, () => {
28
+ const keys = Object.keys(layer);
29
+ expect(keys).toContain("name");
30
+ expect(keys).toContain("label");
31
+ expect(keys).toContain("run");
32
+ });
33
+ }
34
+ });
35
+ });
@@ -0,0 +1,200 @@
1
+ import { beforeAll, describe, it, expect, afterAll } from "vitest";
2
+ import { generate, permutate, bases, layers, reapply } from "#test-helpers";
3
+ import { TODO } from "#layers";
4
+
5
+ import type { Project } from "ember.nvp";
6
+ import { rimraf } from "rimraf";
7
+
8
+ let permutations = permutate(layers.map((layer) => layer.name));
9
+
10
+ const baseline = "<baseline>";
11
+ permutations.push([baseline]);
12
+
13
+ const RE_APPLY_ONLY = new Set<string>([
14
+ // "typescript"
15
+ // "renovate",
16
+ // "github-actions",
17
+ ]);
18
+ const INITIAL_ONLY = new Set<string>([
19
+ // baseline,
20
+ // "eslint-bundled-nvp",
21
+ // "typescript"
22
+ ]);
23
+
24
+ const eachBase = bases
25
+ .map((base) => ({ name: base }))
26
+ .filter(({ name }) => {
27
+ if (name === "minimal-library") {
28
+ /** TODO **/
29
+ return false;
30
+ }
31
+
32
+ return true;
33
+ });
34
+ const eachPermutation = permutations
35
+ .filter((permutation) => {
36
+ if (INITIAL_ONLY.size > 0 && !permutation.some((x) => INITIAL_ONLY.has(x))) {
37
+ return;
38
+ }
39
+
40
+ // Not implemented yet
41
+ if (permutation.some((x) => TODO.has(x))) {
42
+ return;
43
+ }
44
+
45
+ return true;
46
+ })
47
+ .map((permutation) => ({ permutation, name: permutation.join(", ") }));
48
+
49
+ describe.each(eachBase)("$name", ({ name: base }) => {
50
+ describe.each(eachPermutation)("layers: $name", ({ permutation }) => {
51
+ let project: Project;
52
+ let layerNames = permutation.filter((x) => x !== baseline);
53
+ let startingLayers = layers.filter((layer) => layerNames.includes(layer.name));
54
+
55
+ beforeAll(async () => {
56
+ project = await generate({
57
+ type: base === "minimal-app" ? "app" : "library",
58
+ layers: layerNames,
59
+ });
60
+ });
61
+
62
+ afterAll(async () => {
63
+ if (process.env.CI) {
64
+ return;
65
+ }
66
+
67
+ if (project?.directory) {
68
+ await rimraf(project.directory, { maxRetries: 3, retryDelay: 100 });
69
+ }
70
+ });
71
+
72
+ it("successfully setup the layers", async () => {
73
+ if (startingLayers.length === 0) {
74
+ // TODO: test baseline stuff more?
75
+ expect(true, "No layers to verify").toBe(true);
76
+ }
77
+
78
+ for (let layer of startingLayers) {
79
+ expect(layer.isSetup, `has isSetup for ${layer.name}`).toBeInstanceOf(Function);
80
+
81
+ let result = await layer.isSetup(project);
82
+
83
+ expect(result, `${layer.name} is setup`).toBe(true);
84
+ }
85
+ });
86
+
87
+ /**
88
+ * Are these valuable? no one would invoke the layers like this
89
+ */
90
+ // describe.skip("(re)applying", () => {
91
+ // for (let layer of layers) {
92
+ // if (RE_APPLY_ONLY.size > 0 && !RE_APPLY_ONLY.has(layer.name)) {
93
+ // continue;
94
+ // }
95
+
96
+ // if (TODO.has(layer.name)) {
97
+ // continue;
98
+ // }
99
+
100
+ // describe(layer.name, () => {
101
+ // let expected = startingLayers.find((l) => l.name === layer.name);
102
+
103
+ // beforeAll(async () => {
104
+ // await layer.run(project);
105
+ // });
106
+
107
+ // if (expected) {
108
+ // it("is applied", async () => {
109
+ // expect(layer.isSetup, `has isSetup for ${layer.name}`).toBeInstanceOf(Function);
110
+
111
+ // let result = await layer.isSetup(project, true);
112
+
113
+ // if (typeof result === "object") {
114
+ // expect(result.reasons, `${layer.name} is setup`).deep.equal([]);
115
+ // expect(result.isSetup, `${layer.name} is setup`).toBe(true);
116
+ // return;
117
+ // }
118
+
119
+ // expect(result, `${layer.name} is setup`).toBe(true);
120
+
121
+ // return;
122
+ // });
123
+ // } else {
124
+ // it("is not applied", async () => {
125
+ // expect(layer.isSetup, `has isSetup for ${layer.name}`).toBeInstanceOf(Function);
126
+
127
+ // let result = await layer.isSetup(project, true);
128
+
129
+ // if (typeof result === "object") {
130
+ // expect(result.isSetup, `${layer.name} is not setup`).toBe(false);
131
+ // hardExpect(result.reasons.length, `${layer.name} is not setup`).toBeGreaterThan(0);
132
+ // return;
133
+ // }
134
+
135
+ // expect(result, `${layer.name} is not setup`).toBe(false);
136
+ // });
137
+ // }
138
+ // });
139
+ // }
140
+ // });
141
+
142
+ /**
143
+ * Simulates running the CLI again with different options selected
144
+ * on the same project
145
+ */
146
+ describe("apply anew", () => {
147
+ let newLayers = layers
148
+ .filter((layer) => {
149
+ if (RE_APPLY_ONLY.size > 0 && !RE_APPLY_ONLY.has(layer.name)) {
150
+ return false;
151
+ }
152
+
153
+ if (TODO.has(layer.name)) {
154
+ return false;
155
+ }
156
+
157
+ return true;
158
+ })
159
+ .map((layer) => ({ name: layer.name, layer }));
160
+
161
+ describe.each(newLayers)("$name", ({ layer }) => {
162
+ beforeAll(async () => {
163
+ // Simulate running the CLI again, adding one more option while
164
+ // keeping the previously-selected layers.
165
+ await reapply(project, [...new Set([...layerNames, layer.name])]);
166
+ });
167
+
168
+ it.for(startingLayers)("starting layers still present: $name", async (layer) => {
169
+ expect(layer.isSetup, `has isSetup for ${layer.name}`).toBeInstanceOf(Function);
170
+
171
+ let result = await layer.isSetup(project, true);
172
+
173
+ if (typeof result === "object") {
174
+ expect(result.reasons, `${layer.name} is setup`).deep.equal([]);
175
+ expect(result.isSetup, `${layer.name} is setup`).toBe(true);
176
+ return;
177
+ }
178
+
179
+ expect(result, `${layer.name} is setup`).toBe(true);
180
+ });
181
+
182
+ it("is applied", async () => {
183
+ expect(layer.isSetup, `has isSetup for ${layer.name}`).toBeInstanceOf(Function);
184
+
185
+ let result = await layer.isSetup(project, true);
186
+
187
+ if (typeof result === "object") {
188
+ expect(result.reasons, `${layer.name} is setup`).deep.equal([]);
189
+ expect(result.isSetup, `${layer.name} is setup`).toBe(true);
190
+ return;
191
+ }
192
+
193
+ expect(result, `${layer.name} is setup`).toBe(true);
194
+
195
+ return;
196
+ });
197
+ });
198
+ });
199
+ });
200
+ });
package/test/manual.sh ADDED
@@ -0,0 +1,11 @@
1
+ #!/bin/bash
2
+
3
+ root=$(git rev-parse --show-toplevel)
4
+ bin_path="$root/src/cli/index.js"
5
+
6
+ tmp_dir=$(mktemp -d)
7
+
8
+ cd $tmp_dir
9
+
10
+
11
+ node $bin_path