ember.nvp 0.1.0 → 0.2.1

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 (94) 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 +4 -5
  4. package/.prettierignore +2 -1
  5. package/.prettierrc.js +10 -0
  6. package/.release-plan.json +6 -6
  7. package/CHANGELOG.md +53 -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 +17 -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 +8 -5
  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 +65 -7
  51. package/src/layers/qunit/files/tests/unit/macros-test.ts +26 -0
  52. package/src/layers/qunit/index.js +83 -34
  53. package/src/layers/release-plan/index.js +5 -0
  54. package/src/layers/renovate/files/app.json5 +14 -0
  55. package/src/layers/renovate/files/library.json5 +16 -0
  56. package/src/layers/renovate/index.js +68 -0
  57. package/src/layers/typescript/app.test.ts +93 -0
  58. package/src/layers/typescript/index.js +145 -0
  59. package/src/layers/vitest/files/vitest.config.mjs +1 -5
  60. package/src/layers/vitest/index.js +43 -26
  61. package/src/utils/babel.js +76 -0
  62. package/src/utils/buffer.js +15 -0
  63. package/src/utils/cli.js +45 -0
  64. package/src/utils/cwd.js +1 -0
  65. package/src/utils/fs.js +85 -0
  66. package/src/utils/git.js +41 -0
  67. package/src/utils/git.test.ts +36 -0
  68. package/src/utils/manifest.js +9 -0
  69. package/src/utils/npm.js +29 -0
  70. package/src/utils/object.js +10 -0
  71. package/src/utils/project.js +249 -0
  72. package/src/utils/remove-types.js +90 -0
  73. package/src/utils/types.ts +48 -0
  74. package/test/ember.nvp/cli.test.ts +12 -0
  75. package/test/ember.nvp/layers.test.ts +35 -0
  76. package/test/ember.nvp/permutations.test.ts +200 -0
  77. package/test/manual.sh +11 -0
  78. package/test/test-helpers/permutate.test.ts +70 -0
  79. package/test/test-helpers.ts +175 -0
  80. package/tsconfig.json +10 -2
  81. package/vitest.config.js +9 -0
  82. package/src/layers/eslint/index.js +0 -42
  83. package/src/layers/minimal/files/app/app.ts +0 -10
  84. package/src/layers/minimal/files/app/config.ts +0 -18
  85. package/src/layers/minimal/files/app/router.ts +0 -9
  86. package/src/layers/minimal/files/app/templates/application.gts +0 -7
  87. package/src/layers/minimal/files-js/babel.config.js +0 -38
  88. package/src/layers/minimal/index.js +0 -89
  89. package/src/layers/prettier/files/.prettierrc.cjs +0 -15
  90. package/src/layers/qunit/files/tests/unit/.gitkeep +0 -0
  91. /package/src/{layers/minimal → bases/minimal-app}/files/vite.config.mjs +0 -0
  92. /package/src/layers/{eslint/files/eslint.config.mjs → eslint-ejected/files/eslint.config.js} +0 -0
  93. /package/src/layers/{minimal/files/app/routes → qunit/files/tests/application}/.gitkeep +0 -0
  94. /package/src/layers/qunit/files/tests/{integration → rendering}/.gitkeep +0 -0
@@ -1,8 +1,9 @@
1
1
  import { packageJson, files } from "ember-apply";
2
- import { dirname, join } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
-
5
- const __dirname = dirname(fileURLToPath(import.meta.url));
2
+ import { join } from "node:path";
3
+ import { existsSync } from "node:fs";
4
+ import { getLatest } from "#utils/npm.js";
5
+ import { formatLabel } from "#utils/cli.js";
6
+ import { maybeLintWithConcurrently } from "#consolidators/linting.js";
6
7
 
7
8
  /**
8
9
  * Prettier Layer
@@ -10,29 +11,55 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
10
11
  * Adds Prettier for code formatting
11
12
  */
12
13
  export default {
13
- label: "Prettier",
14
- description: "Code formatting with Prettier",
14
+ label: formatLabel("Prettier", "code formatting"),
15
+ hint: "the defaults",
15
16
 
16
- async run({ targetDir }) {
17
- // Apply config files
18
- await files.applyFolder(join(__dirname, "files"), targetDir);
17
+ /**
18
+ * @param {import('#utils/project.js').Project} project
19
+ */
20
+ async run(project) {
21
+ await files.applyFolder(join(import.meta.dirname, "files"), project.directory);
19
22
 
20
- // Add devDependencies
21
23
  await packageJson.addDevDependencies(
22
- {
23
- prettier: "^3.4.2",
24
- "prettier-plugin-ember-template-tag": "^2.0.2",
25
- },
26
- targetDir,
24
+ await getLatest({
25
+ prettier: "^3.8.1",
26
+ "prettier-plugin-ember-template-tag": "^2.1.2",
27
+ }),
28
+ project.directory,
27
29
  );
28
30
 
29
- // Add scripts
30
31
  await packageJson.addScripts(
31
32
  {
32
33
  format: "prettier --write .",
33
- "format:check": "prettier --check .",
34
+ "lint:prettier": "prettier --check .",
34
35
  },
35
- targetDir,
36
+ project.directory,
36
37
  );
38
+
39
+ await maybeLintWithConcurrently(project);
40
+ },
41
+
42
+ /**
43
+ * @param {import('#utils/project.js').Project} project
44
+ */
45
+ async isSetup(project) {
46
+ let manifest = await packageJson.read(project.directory);
47
+
48
+ let scripts = Object.values(manifest.scripts ?? {}).filter((script) =>
49
+ script.includes("prettier"),
50
+ );
51
+
52
+ if (!scripts.some((script) => script.includes("--write"))) {
53
+ return false;
54
+ }
55
+ if (!scripts.some((script) => script.includes("--check"))) {
56
+ return false;
57
+ }
58
+
59
+ if (!existsSync(join(project.directory, ".prettierrc.js"))) {
60
+ return false;
61
+ }
62
+
63
+ return true;
37
64
  },
38
65
  };
@@ -0,0 +1,90 @@
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 === "qunit")!;
10
+
11
+ describe("starting without qunit", () => {
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 qunit", async () => {
27
+ await reapply(project, ["qunit"]);
28
+
29
+ await expectIsSetup(project, layer);
30
+ expect(await project.gitHasDiff()).toBe(false);
31
+ });
32
+ });
33
+
34
+ describe("starting with qunit", () => {
35
+ let project: Project;
36
+
37
+ beforeAll(async () => {
38
+ project = await generate({
39
+ type: "app",
40
+ packageManager: "pnpm",
41
+ layers: ["qunit", "git"],
42
+ });
43
+ });
44
+
45
+ afterAll(async () => {
46
+ await rm(project.directory, { recursive: true, force: true });
47
+ });
48
+
49
+ it("reapplying causes no changes", async () => {
50
+ await reapply(project, ["qunit"]);
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: ["qunit", "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("running tests works", async () => {
79
+ {
80
+ let { exitCode, stderr, stdout } = await project.run("pnpm test");
81
+
82
+ if (exitCode !== 0) {
83
+ console.log(stderr);
84
+ console.log(stdout);
85
+ }
86
+
87
+ expect(exitCode).toBe(0);
88
+ }
89
+ });
90
+ });
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+
3
+ if (typeof module !== "undefined") {
4
+ module.exports = {
5
+ test_page: "tests/index.html?hidepassed",
6
+ cwd: "dist",
7
+ disable_watching: true,
8
+ launch_in_ci: ["Chrome"],
9
+ launch_in_dev: ["Chrome"],
10
+ browser_start_timeout: 120,
11
+ browser_args: {
12
+ Chrome: {
13
+ ci: [
14
+ // --no-sandbox is needed when running Chrome inside a container
15
+ process.env.CI ? "--no-sandbox" : null,
16
+ "--headless",
17
+ "--disable-dev-shm-usage",
18
+ "--disable-software-rasterizer",
19
+ "--mute-audio",
20
+ "--remote-debugging-port=0",
21
+ "--window-size=1440,900",
22
+ ].filter(Boolean),
23
+ },
24
+ },
25
+ };
26
+ }
@@ -1,14 +1,9 @@
1
1
  <!doctype html>
2
- <html lang="en">
2
+ <html>
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
- <title>__PROJECT_NAME__ Tests</title>
6
- <meta name="description" content="" />
5
+ <title>Tests</title>
7
6
  <meta name="viewport" content="width=device-width, initial-scale=1" />
8
-
9
- <link rel="stylesheet" href="https://unpkg.com/qunit@2/qunit/qunit.css" />
10
-
11
- <script type="module" src="/@embroider/virtual/test-support.js"></script>
12
7
  </head>
13
8
  <body>
14
9
  <div id="qunit"></div>
@@ -17,5 +12,19 @@
17
12
  <div id="ember-testing"></div>
18
13
  </div>
19
14
  </div>
15
+
16
+ <script src="/testem.js" integrity="" data-embroider-ignore></script>
17
+ <script type="module">
18
+ import "ember-testing";
19
+ </script>
20
+
21
+ <script type="module">
22
+ import { start } from "./test-helper";
23
+ // traditional test locations
24
+ import.meta.glob("./**/*.{js,ts,gjs,gts}", { eager: true });
25
+ // co-located test locations
26
+ import.meta.glob("./**/*-test.{js,ts,gjs,gts}", { eager: true, base: "../app" });
27
+ start();
28
+ </script>
20
29
  </body>
21
30
  </html>
@@ -1,12 +1,70 @@
1
- import Application from "#app/app.ts";
2
- import config from "#app/config.ts";
1
+ import { getOwner } from "@ember/owner";
2
+ import {
3
+ currentRouteName,
4
+ currentURL,
5
+ getSettledState,
6
+ resetOnerror,
7
+ setApplication,
8
+ visit,
9
+ } from "@ember/test-helpers";
10
+ import { getPendingWaiterState } from "@ember/test-waiters";
3
11
  import * as QUnit from "qunit";
4
- import { setApplication } from "@ember/test-helpers";
5
12
  import { setup } from "qunit-dom";
6
- import { start } from "ember-qunit";
13
+ import { setupEmberOnerrorValidation, start as qunitStart } from "ember-qunit";
14
+ import { setTesting } from "@embroider/macros";
15
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
16
+ // @ts-expect-error
17
+ import { getGlobalConfig } from "@embroider/macros/src/addon/runtime";
7
18
 
8
- setApplication(Application.create(config));
19
+ import Application from "#app/app";
20
+ import config from "#config";
9
21
 
10
- setup(QUnit.assert);
22
+ Object.assign(window, {
23
+ visit,
24
+ getSettledState,
25
+ getPendingWaiterState,
26
+ currentURL,
27
+ currentRouteName,
28
+ getOwner,
29
+ snapshotTimers: (label?: string) => {
30
+ console.debug(
31
+ label ?? "snapshotTimers",
32
+ JSON.parse(
33
+ JSON.stringify({
34
+ settled: getSettledState(),
35
+ waiters: getPendingWaiterState(),
36
+ }),
37
+ ),
38
+ );
39
+ },
40
+ });
11
41
 
12
- start();
42
+ export function start() {
43
+ config.locationType = "none";
44
+ config.APP.rootElement = "#ember-testing";
45
+ config.APP.autoboot = false;
46
+ setTesting(true);
47
+
48
+ const theMacrosGlobal = getGlobalConfig();
49
+
50
+ /**
51
+ * Caveats:
52
+ * - https://github.com/embroider-build/embroider/issues/2660
53
+ * - https://github.com/embroider-build/embroider/issues/1998
54
+ *
55
+ */
56
+ theMacrosGlobal["@embroider/macros"] ||= {};
57
+ theMacrosGlobal["@embroider/macros"].isTesting ||= true;
58
+
59
+ setApplication(Application.create(config.APP));
60
+ setup(QUnit.assert);
61
+ setupEmberOnerrorValidation();
62
+
63
+ QUnit.moduleStart(({ name }) => console.group(name));
64
+ QUnit.testStart(({ name }) => console.group(name));
65
+ QUnit.testDone(() => console.groupEnd());
66
+ QUnit.moduleDone(() => console.groupEnd());
67
+ QUnit.testDone(resetOnerror);
68
+
69
+ qunitStart();
70
+ }
@@ -0,0 +1,26 @@
1
+ import { module, test } from "qunit";
2
+ import { isTesting } from "@embroider/macros";
3
+ import { assert as emberAssert } from "@ember/debug";
4
+
5
+ module("@embroider/macros | runtime mode in tests", function () {
6
+ /**
7
+ * In compile-time mode `isTesting()` is inlined as `false` at build time,
8
+ * regardless of what the runtime global config says. Only when the test
9
+ * build runs the macros plugin in runtime mode (see babel.config.js +
10
+ * EMBER_ENV=test) does `isTesting()` reflect the value set by
11
+ * `setTesting(true)` in tests/test-helper.ts.
12
+ */
13
+ test("isTesting() is true", function (assert) {
14
+ assert.strictEqual(isTesting(), true);
15
+ });
16
+
17
+ /**
18
+ * `@ember/debug`'s `assert(description, condition)` is only active when
19
+ * DEBUG is true (i.e. non-production builds). Assertions are stripped
20
+ * from production bundles, so this also doubles as a sanity check that
21
+ * the test build keeps DEBUG enabled.
22
+ */
23
+ test("@ember/debug assert throws when the condition is false", function (assert) {
24
+ assert.throws(() => emberAssert("string", false));
25
+ });
26
+ });
@@ -1,53 +1,102 @@
1
+ import { applyFolderTo } from "#utils/fs.js";
2
+ import { getLatest } from "#utils/npm.js";
1
3
  import { packageJson, files } from "ember-apply";
2
- import { dirname, join } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { readFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
5
 
6
- const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const deps = {
7
+ "@ember/test-helpers": "^5.4.1",
8
+ "@ember/test-waiters": "^4.1.0",
9
+ "ember-qunit": "^9.0.4",
10
+ "qunit-theme-ember": "^1.0.0",
11
+ qunit: "^2.25.0",
12
+ "qunit-dom": "3.5.0",
13
+ testem: "3.17.0",
14
+ };
15
+ const tsDeps = {
16
+ "@types/qunit": "^2.19.12",
17
+ };
7
18
 
8
19
  /**
9
- * QUnit Layer
10
- *
11
- * Adds QUnit testing support (Ember's traditional testing framework)
20
+ * @type {import('#types').Layer}
12
21
  */
13
22
  export default {
14
23
  label: "QUnit",
15
- description: "Testing with QUnit",
16
-
17
- async run({ targetDir, projectName }) {
18
- // Apply test files
19
- await files.applyFolder(join(__dirname, "files"), targetDir);
20
24
 
21
- // Replace __PROJECT_NAME__ in tests/index.html
22
- const testHtmlPath = join(targetDir, "tests", "index.html");
23
- const content = await readFile(testHtmlPath, "utf-8");
24
- const updated = content.replaceAll("__PROJECT_NAME__", projectName);
25
- await files.copyFileTo(testHtmlPath, { content: updated });
26
-
27
- // Add dependencies
28
- await packageJson.addDependencies(
29
- {
30
- "@ember/test-helpers": "^4.0.4",
31
- },
32
- targetDir,
33
- );
25
+ async run(project) {
26
+ let filePath = join(import.meta.dirname, "files");
27
+ await applyFolderTo(filePath, project);
28
+ let ts = await project.hasOrWantsLayer("typescript");
34
29
 
35
- // Add devDependencies
36
30
  await packageJson.addDevDependencies(
37
- {
38
- qunit: "^2.22.0",
39
- "@ember/test-waiters": "^3.1.0",
40
- playwright: "^1.49.1",
41
- },
42
- targetDir,
31
+ await getLatest({
32
+ ...deps,
33
+ ...(ts ? tsDeps : {}),
34
+ }),
35
+ project.directory,
43
36
  );
44
37
 
45
38
  // Add scripts
46
39
  await packageJson.addScripts(
47
40
  {
48
- test: "vite build && node ./run-tests.mjs",
41
+ "build:test": "EMBER_ENV=test NODE_ENV=development vite build --mode development",
42
+ "test:ci": "testem ci --port 0",
43
+ test: "pnpm build:test && pnpm test:ci",
49
44
  },
50
- targetDir,
45
+ project.directory,
51
46
  );
52
47
  },
48
+ /**
49
+ * @overload
50
+ * @param {import('#utils/project.js').Project} project
51
+ * @param {true} explain
52
+ * @returns {Promise<{ isSetup: boolean; reasons: string[] }>}
53
+ */
54
+ /**
55
+ * @overload
56
+ * @param {import('#utils/project.js').Project} project
57
+ * @param {boolean | undefined} [explain]
58
+ * @returns {Promise<boolean>}
59
+ */
60
+ async isSetup(project, explain) {
61
+ const reasons = [];
62
+ let depsToCheck = [Object.keys(deps), project.wantsTypeScript ? Object.keys(tsDeps) : ""]
63
+ .flat()
64
+ .filter(Boolean);
65
+
66
+ let manifest = await packageJson.read(project.directory);
67
+
68
+ if (!manifest.scripts?.["build:test"]) {
69
+ if (!explain) return false;
70
+
71
+ reasons.push(`package.json is missing the "build:test" script`);
72
+ }
73
+
74
+ if (!manifest.scripts?.["test:ci"]) {
75
+ if (!explain) return false;
76
+
77
+ reasons.push(`package.json is missing the "test:ci" script`);
78
+ }
79
+ if (!manifest.scripts?.["test"]) {
80
+ if (!explain) return false;
81
+
82
+ reasons.push(`package.json is missing the "test" script`);
83
+ }
84
+
85
+ for (let dep of depsToCheck) {
86
+ if (!manifest.devDependencies?.[dep]) {
87
+ if (!explain) return false;
88
+
89
+ reasons.push(`package.json is missing ${dep} in devDependencies`);
90
+ }
91
+ }
92
+
93
+ if (explain) {
94
+ return {
95
+ isSetup: reasons.length === 0,
96
+ reasons,
97
+ };
98
+ }
99
+
100
+ return reasons.length === 0;
101
+ },
53
102
  };
@@ -0,0 +1,5 @@
1
+ export default {
2
+ label: "release-plan",
3
+ description: "release automation so that you can release anywhere",
4
+ run() {},
5
+ };
@@ -0,0 +1,14 @@
1
+ // Docs:
2
+ // https://docs.renovatebot.com/configuration-options/
3
+ //
4
+ // This config: https://github.com/nullVoxPopuli/renovate
5
+ // - weekly automated updates
6
+ // - requires that packages be released for at least 5 days before opening a PR with that dependency
7
+ // - doesn't constantly rebase open PRs
8
+ // - provides opinionated package rules for reduced PR spam
9
+ // - opens a separate PR to re-roll your lockfile occasionally
10
+ // - appropriate for application development
11
+ {
12
+ extends: ["github>NullVoxPopuli/renovate:js.json5"],
13
+ packageRules: [],
14
+ }
@@ -0,0 +1,16 @@
1
+ // Docs:
2
+ // https://docs.renovatebot.com/configuration-options/
3
+ //
4
+ // This config: https://github.com/nullVoxPopuli/renovate
5
+ // - weekly automated updates
6
+ // - requires that packages be released for at least 5 days before opening a PR with that dependency
7
+ // - doesn't constantly rebase open PRs
8
+ // - provides opinionated package rules for reduced PR spam
9
+ // - opens a separate PR to re-roll your lockfile occasionally
10
+ // - appropriate for npm library development
11
+ // (auto-updating of engines, peerDependencies, and dependencies is disabled
12
+ // only devDependencies get automatically upgraded in order ot keep your repo stable)
13
+ {
14
+ extends: ["github>NullVoxPopuli/renovate:npm.json5"],
15
+ packageRules: [],
16
+ }
@@ -0,0 +1,68 @@
1
+ import { files } from "ember-apply";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+
6
+ /**
7
+ * @type {import('#types').Layer}
8
+ */
9
+ export default {
10
+ label: "renovate [bot]",
11
+ hint: "Automatically have dependencies upgrade for you on a schedule",
12
+ async run(project) {
13
+ if (hasRenovateConfig(project)) {
14
+ return;
15
+ }
16
+
17
+ let source =
18
+ project.type === "app"
19
+ ? join(import.meta.dirname, "files/app.json5")
20
+ : join(import.meta.dirname, "files/library.json5");
21
+
22
+ let destination = join(project.directory, ".github/renovate.json5");
23
+
24
+ await mkdir(join(project.directory, ".github"), { recursive: true });
25
+ await files.copyFileTo(destination, { source });
26
+ },
27
+
28
+ /**
29
+ * @overload
30
+ * @param {import('#utils/project.js').Project} project
31
+ * @param {true} explain
32
+ * @returns {Promise<{ isSetup: boolean; reasons: string[] }>}
33
+ */
34
+ /**
35
+ * @overload
36
+ * @param {import('#utils/project.js').Project} project
37
+ * @param {boolean | undefined} [explain]
38
+ * @returns {Promise<boolean>}
39
+ */
40
+ async isSetup(project, explain) {
41
+ const reasons = [];
42
+
43
+ if (!hasRenovateConfig(project)) {
44
+ reasons.push("renovate.json5 is missing");
45
+
46
+ if (!explain) return false;
47
+ }
48
+
49
+ if (explain) {
50
+ return {
51
+ isSetup: reasons.length === 0,
52
+ reasons,
53
+ };
54
+ }
55
+
56
+ return reasons.length === 0;
57
+ },
58
+ };
59
+
60
+ /**
61
+ *
62
+ * @param {import('#utils/project.js').Project} project
63
+ * @returns
64
+ */
65
+ function hasRenovateConfig(project) {
66
+ let destination = join(project.directory, ".github/renovate.json5");
67
+ return existsSync(destination);
68
+ }
@@ -0,0 +1,93 @@
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 } from "node:fs/promises";
6
+
7
+ const expect = hardExpect.soft;
8
+
9
+ let githubActionsLayer = layers.find((layer) => layer.name === "github-actions")!;
10
+
11
+ describe("starting with javascript", () => {
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 typescript causes no changes", async () => {
27
+ await reapply(project, ["typescript"]);
28
+
29
+ let result = await githubActionsLayer.isSetup(project);
30
+
31
+ expect(result).toBe(true);
32
+ expect(await project.gitHasDiff()).toBe(false);
33
+ expect(project.hasFile("tsconfig.json")).toBe(true);
34
+ });
35
+ });
36
+
37
+ describe("starting with typescript", () => {
38
+ let project: Project;
39
+
40
+ beforeAll(async () => {
41
+ project = await generate({
42
+ type: "app",
43
+ packageManager: "pnpm",
44
+ layers: ["typescript", "git"],
45
+ });
46
+ });
47
+
48
+ afterAll(async () => {
49
+ await rm(project.directory, { recursive: true, force: true });
50
+ });
51
+
52
+ it("reapplying typescript causes no changes", async () => {
53
+ await reapply(project, ["typescript"]);
54
+
55
+ let result = await githubActionsLayer.isSetup(project);
56
+
57
+ expect(result).toBe(true);
58
+ expect(await project.gitHasDiff()).toBe(false);
59
+ expect(project.hasFile("tsconfig.json")).toBe(true);
60
+ });
61
+ });
62
+
63
+ describe("package.json scripts", () => {
64
+ let project: Project;
65
+
66
+ beforeAll(async () => {
67
+ project = await generate({
68
+ type: "app",
69
+ packageManager: "pnpm",
70
+ layers: ["typescript", "git"],
71
+ });
72
+
73
+ let { exitCode } = await project.run("pnpm install");
74
+
75
+ hardExpect(exitCode).toBe(0);
76
+ });
77
+
78
+ afterAll(async () => {
79
+ await rm(project.directory, { recursive: true, force: true });
80
+ });
81
+
82
+ it("type checking works", async () => {
83
+ let { exitCode } = await project.run("pnpm lint:types");
84
+
85
+ expect(exitCode).toBe(0);
86
+ });
87
+
88
+ it("build works", async () => {
89
+ let { exitCode } = await project.run("pnpm build");
90
+
91
+ expect(exitCode).toBe(0);
92
+ });
93
+ });