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.
- package/.github/workflows/ci.yml +41 -0
- package/.github/workflows/plan-release.yml +2 -2
- package/.github/workflows/publish.yml +2 -2
- package/.prettierignore +2 -1
- package/.prettierrc.js +10 -0
- package/.release-plan.json +11 -3
- package/CHANGELOG.md +37 -0
- package/README.md +79 -142
- package/package.json +28 -5
- package/pnpm-workspace.yaml +3 -0
- package/src/bases/README.md +5 -0
- package/src/bases/minimal-app/app.test.ts +203 -0
- package/src/bases/minimal-app/files/.env.development +8 -0
- package/src/bases/minimal-app/files/app/app.ts +21 -0
- package/src/bases/minimal-app/files/app/config.ts +30 -0
- package/src/bases/minimal-app/files/app/router.ts +51 -0
- package/src/bases/minimal-app/files/app/templates/application.gts +5 -0
- package/src/{layers/minimal/files-ts → bases/minimal-app/files}/babel.config.js +1 -4
- package/src/{layers/minimal → bases/minimal-app}/files/index.html +3 -5
- package/src/bases/minimal-app/files/package.json +57 -0
- package/src/{layers/minimal → bases/minimal-app}/files/tsconfig.json +0 -6
- package/src/bases/minimal-app/index.js +111 -0
- package/src/bases/minimal-library/index.js +10 -0
- package/src/cli/args.js +57 -0
- package/src/cli/generator.js +66 -11
- package/src/cli/index.js +44 -13
- package/src/cli/questions/layers.js +39 -50
- package/src/cli/questions/name.js +22 -6
- package/src/cli/questions/ok.js +38 -0
- package/src/cli/questions/package-manager.js +23 -2
- package/src/cli/questions/path.js +59 -0
- package/src/cli/questions/project-type.js +55 -0
- package/src/consolidators/linting.js +51 -0
- package/src/index.js +2 -0
- package/src/layers/eslint-bundled-ember/files/eslint.config.js +14 -0
- package/src/layers/eslint-bundled-ember/index.js +91 -0
- package/src/layers/eslint-bundled-nvp/app.test.ts +91 -0
- package/src/layers/eslint-bundled-nvp/files/eslint.config.js +16 -0
- package/src/layers/eslint-bundled-nvp/index.js +91 -0
- package/src/layers/eslint-ejected/index.js +95 -0
- package/src/layers/git/index.js +33 -0
- package/src/layers/github-actions/app.test.ts +73 -0
- package/src/layers/github-actions/index.js +200 -0
- package/src/layers/index.js +47 -0
- package/src/layers/prettier/files/.prettierrc.js +4 -0
- package/src/layers/prettier/index.js +45 -18
- package/src/layers/qunit/app.test.ts +90 -0
- package/src/layers/qunit/files/testem.cjs +26 -0
- package/src/layers/qunit/files/tests/index.html +16 -7
- package/src/layers/qunit/files/tests/test-helper.ts +61 -7
- package/src/layers/qunit/index.js +83 -34
- package/src/layers/release-plan/index.js +5 -0
- package/src/layers/renovate/files/app.json5 +14 -0
- package/src/layers/renovate/files/library.json5 +16 -0
- package/src/layers/renovate/index.js +68 -0
- package/src/layers/typescript/app.test.ts +93 -0
- package/src/layers/typescript/index.js +145 -0
- package/src/layers/vitest/files/vitest.config.mjs +1 -5
- package/src/layers/vitest/index.js +43 -26
- package/src/utils/babel.js +76 -0
- package/src/utils/buffer.js +15 -0
- package/src/utils/cli.js +45 -0
- package/src/utils/cwd.js +1 -0
- package/src/utils/fs.js +85 -0
- package/src/utils/git.js +41 -0
- package/src/utils/git.test.ts +36 -0
- package/src/utils/manifest.js +9 -0
- package/src/utils/npm.js +29 -0
- package/src/utils/object.js +10 -0
- package/src/utils/project.js +249 -0
- package/src/utils/remove-types.js +90 -0
- package/src/utils/types.ts +48 -0
- package/test/ember.nvp/cli.test.ts +12 -0
- package/test/ember.nvp/layers.test.ts +35 -0
- package/test/ember.nvp/permutations.test.ts +200 -0
- package/test/manual.sh +11 -0
- package/test/test-helpers/permutate.test.ts +70 -0
- package/test/test-helpers.ts +175 -0
- package/tsconfig.json +10 -2
- package/vitest.config.js +9 -0
- package/src/layers/eslint/index.js +0 -42
- package/src/layers/minimal/files/app/app.ts +0 -10
- package/src/layers/minimal/files/app/config.ts +0 -18
- package/src/layers/minimal/files/app/router.ts +0 -9
- package/src/layers/minimal/files/app/templates/application.gts +0 -7
- package/src/layers/minimal/files-js/babel.config.js +0 -38
- package/src/layers/minimal/index.js +0 -89
- package/src/layers/prettier/files/.prettierrc.cjs +0 -15
- /package/src/{layers/minimal → bases/minimal-app}/files/vite.config.mjs +0 -0
- /package/src/layers/{eslint/files/eslint.config.mjs → eslint-ejected/files/eslint.config.js} +0 -0
- /package/src/layers/{minimal/files/app/routes → qunit/files/tests/application}/.gitkeep +0 -0
- /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
|
+
});
|