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,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);
|