ember.nvp 1.7.0 → 1.7.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 (80) hide show
  1. package/.release-plan.json +45 -13
  2. package/CHANGELOG.md +18 -0
  3. package/README.md +41 -28
  4. package/RELEASE.md +16 -14
  5. package/package.json +1 -1
  6. package/packages/build-tooling-utils/README.md +7 -12
  7. package/packages/build-tooling-utils/package.json +1 -1
  8. package/packages/build-tooling-utils/src/extensions.ts +4 -3
  9. package/packages/build-tooling-utils/src/maybe-babel.ts +18 -15
  10. package/packages/rolldown/README.md +105 -100
  11. package/packages/rolldown/index.ts +37 -24
  12. package/packages/rolldown/package.json +1 -1
  13. package/packages/rolldown/src/app-reexports.test.ts +4 -3
  14. package/packages/rolldown/src/app-reexports.ts +37 -27
  15. package/packages/rolldown/src/babel.test.ts +10 -5
  16. package/packages/rolldown/src/babel.ts +88 -69
  17. package/packages/rolldown/src/config.ts +21 -17
  18. package/packages/rolldown/src/dts.test.ts +42 -26
  19. package/packages/rolldown/src/externals.test.ts +16 -11
  20. package/packages/rolldown/src/externals.ts +18 -12
  21. package/packages/rolldown/src/isolated-declarations.ts +56 -43
  22. package/packages/rolldown/src/plugin-like.ts +18 -15
  23. package/packages/rolldown/src/published-types.test.ts +18 -13
  24. package/packages/rolldown/src/transform.test.ts +26 -17
  25. package/packages/rolldown/src/transform.ts +68 -48
  26. package/packages/vite/README.md +10 -11
  27. package/packages/vite/package.json +1 -1
  28. package/packages/vite/src/ember.ts +44 -36
  29. package/packages/vite/src/test-html.ts +13 -11
  30. package/src/bases/README.md +6 -3
  31. package/src/bases/minimal-app/app.test.ts +7 -4
  32. package/src/bases/minimal-app/files/app/app.ts +2 -3
  33. package/src/bases/minimal-app/files/app/router.ts +3 -3
  34. package/src/bases/minimal-app/index.js +10 -6
  35. package/src/bases/minimal-custom-element/files/README.md +16 -12
  36. package/src/bases/minimal-extension/files/README.md +13 -5
  37. package/src/bases/minimal-extension/files/app/app.ts +2 -3
  38. package/src/bases/minimal-extension/files/app/config.ts +5 -3
  39. package/src/bases/minimal-extension/index.js +16 -11
  40. package/src/bases/minimal-library/files/README.md +8 -7
  41. package/src/bases/minimal-library/index.js +21 -15
  42. package/src/cli/index.js +23 -18
  43. package/src/cli/questions/write.js +12 -10
  44. package/src/layers/are-the-types-wrong/index.js +4 -4
  45. package/src/layers/github-actions/index.js +7 -4
  46. package/src/layers/inspector-support/index.js +2 -2
  47. package/src/layers/qunit/index.js +18 -14
  48. package/src/layers/readme/index.js +2 -4
  49. package/src/layers/renovate/index.js +2 -1
  50. package/src/layers/typescript/index.js +12 -12
  51. package/src/layers/vitest/index.js +7 -3
  52. package/src/utils/babel.js +3 -2
  53. package/src/utils/buffer.js +2 -2
  54. package/src/utils/cli.js +4 -4
  55. package/src/utils/fs.js +23 -14
  56. package/src/utils/inspector-support.js +22 -19
  57. package/src/utils/npm.js +15 -12
  58. package/src/utils/project-type.js +19 -0
  59. package/src/utils/project.js +4 -13
  60. package/src/utils/remove-types.js +2 -4
  61. package/src/utils/rewrite-imports.js +43 -31
  62. package/src/utils/stage.js +44 -33
  63. package/src/utils/tsdown-config.js +6 -5
  64. package/src/utils/types.ts +9 -8
  65. package/test/ember.nvp/base-library.test.ts +6 -4
  66. package/test/ember.nvp/generator-replace-or-update.test.ts +6 -4
  67. package/test/ember.nvp/inspector-support.test.ts +4 -3
  68. package/test/ember.nvp/layer-inspector-support.test.ts +2 -3
  69. package/test/ember.nvp/layer-publish-checks.test.ts +2 -3
  70. package/test/ember.nvp/layer-qunit.test.ts +2 -2
  71. package/test/ember.nvp/layer-readme.test.ts +2 -2
  72. package/test/ember.nvp/layer-vitest.test.ts +16 -9
  73. package/test/ember.nvp/layers-load.test.ts +26 -0
  74. package/test/ember.nvp/library-src-fixtures.ts +7 -3
  75. package/test/ember.nvp/permutations/shared.ts +19 -12
  76. package/test/ember.nvp/publish-configs.test.ts +44 -31
  77. package/test/ember.nvp/rewrite-imports.test.ts +4 -3
  78. package/test/ember.nvp/scoped-css-library.test.ts +6 -4
  79. package/test/test-helpers/permutate.test.ts +3 -3
  80. package/test/test-helpers.ts +10 -6
@@ -26,9 +26,7 @@ export async function removeTypes(extension, code) {
26
26
  }
27
27
 
28
28
  /**
29
- *
30
29
  * Support for removeTypes
31
- *
32
30
  */
33
31
 
34
32
  const preprocessor = new Preprocessor();
@@ -78,8 +76,8 @@ async function wrappedRemoveTypes(code = "", callback = babelRemoveTypes) {
78
76
  templateTag,
79
77
  );
80
78
  } else {
81
- // babel-remove-types uses prettier under the hood, and adds trailing `;` where allowed,
82
- // so we need to take that into account when restoring the template tags:
79
+ // babel-remove-types uses prettier under the hood, and adds trailing `;` where allowed.
80
+ // Restoring the template tags has to account for that:
83
81
  transformedWithTemplateTag = transformedWithTemplateTag.replace(
84
82
  `${replacementExpression(i)};`,
85
83
  templateTag,
@@ -6,10 +6,12 @@ import enhancedResolve from "enhanced-resolve";
6
6
  const { ResolverFactory, CachedInputFileSystem } = enhancedResolve;
7
7
 
8
8
  /**
9
- * The module files this util operates on. Doubles as the candidate
10
- * extensions the resolver may append when a request has none (or when we
11
- * retry a request without its original extension) -- the single source
12
- * of truth for both.
9
+ * The module files this util operates on.
10
+ *
11
+ * Doubles as the candidate extensions the resolver may append
12
+ * when a request has none, or when we retry a request without its original extension.
13
+ *
14
+ * The single source of truth for both.
13
15
  */
14
16
  const MODULE_EXTENSIONS = [".js", ".gjs", ".ts", ".gts"];
15
17
 
@@ -19,38 +21,48 @@ function createResolver() {
19
21
  fileSystem: new CachedInputFileSystem(fs, 100),
20
22
  useSyncFileSystemCalls: true,
21
23
  extensions: MODULE_EXTENSIONS,
22
- // no conditionNames: we only resolve local paths and sub-path
23
- // self-imports (whose targets in the emitted `imports` field are
24
- // plain strings, which match regardless of conditions)
24
+ // no conditionNames: we only resolve local paths and sub-path self-imports.
25
+ // Their targets in the emitted `imports` field are plain strings,
26
+ // which match regardless of conditions.
25
27
  });
26
28
  }
27
29
 
28
30
  /**
29
- * Imports must match files. Generation renames files (e.g. type removal
30
- * turns .ts into .js), so for every local specifier (./, ../, and #
31
- * subpath-imports -- expanded against the project's package.json
32
- * `imports` field) we check what actually exists in the emitted project
33
- * tree:
31
+ * Imports must match files.
32
+ *
33
+ * Generation renames files (e.g. type removal turns .ts into .js).
34
+ * So for every local specifier, we check what actually exists in the emitted project tree.
34
35
  *
35
- * - the specifier resolves to a file and carries an extension? leave it
36
- * alone.
37
- * - it resolves but has no extension? append the resolved file's
38
- * extension (fully-specified local imports resolve faster), unless the
39
- * fully-specified form no longer maps to the same file (e.g. `#config`,
40
- * whose `imports` target carries the extension itself).
41
- * - it doesn't resolve, but the same specifier with whatever extension
42
- * IS on disk does? rewrite it to that.
43
- * - nothing matches? leave it alone -- we can't know better, and the
44
- * project's own build will report it loudly.
36
+ * Local specifiers are:
37
+ * - ./ and ../
38
+ * - # subpath-imports, expanded against the project's package.json `imports` field
39
+ *
40
+ * For each one:
41
+ * - it resolves to a file and carries an extension?
42
+ * leave it alone.
43
+ * - it resolves but has no extension?
44
+ * append the resolved file's extension (fully-specified local imports resolve faster).
45
+ * Unless the fully-specified form no longer maps to the same file
46
+ * (e.g. `#config`, whose `imports` target carries the extension itself).
47
+ * - it doesn't resolve, but the same specifier with whatever extension IS on disk does?
48
+ * rewrite it to that.
49
+ * - nothing matches?
50
+ * leave it alone. We can't know better, and the project's own build will report it.
45
51
  *
46
52
  * Non-module files are returned untouched.
47
53
  *
48
- * ember-estree parses gjs natively and its visitors fire on actual module
49
- * specifiers only (static imports, re-exports, and dynamic import()) --
50
- * matched specifiers are rewritten on the AST and the File is printed
51
- * back out (comments included). Formatting is the project's own concern:
52
- * new projects run their configured lint:fix / format right away (it's
53
- * in the CLI's "Next steps").
54
+ * ember-estree parses gjs natively.
55
+ * Its visitors fire on actual module specifiers only:
56
+ * - static imports
57
+ * - re-exports
58
+ * - dynamic import()
59
+ *
60
+ * Matched specifiers are rewritten on the AST,
61
+ * and the File is printed back out (comments included).
62
+ *
63
+ * Formatting is the project's own concern.
64
+ * New projects run their configured lint:fix / format right away
65
+ * (it's in the CLI's "Next steps").
54
66
  *
55
67
  * Files with no specifiers to rewrite are returned untouched.
56
68
  *
@@ -101,9 +113,9 @@ export function rewriteImportsToMatchFiles(code, emittedPath) {
101
113
  // fully specified and correct
102
114
  if (currentExtension) return;
103
115
 
104
- // extensionless: append the resolved file's extension, as long as
105
- // the fully-specified form still maps to the same file (`#config`
106
- // maps with the extension in the target, so `#config.js` wouldn't)
116
+ // extensionless: append the resolved file's extension,
117
+ // as long as the fully-specified form still maps to the same file.
118
+ // (`#config` maps with the extension in the target, so `#config.js` wouldn't)
107
119
  let candidate = specifier + extname(resolved);
108
120
 
109
121
  if (resolveFrom(candidate) !== resolved) return;
@@ -6,9 +6,13 @@ import { styleText } from "node:util";
6
6
  import { structuredPatch } from "diff";
7
7
 
8
8
  /**
9
- * Directories that belong to the target project's environment rather than
10
- * its generated contents. They are never seeded into the stage, never
11
- * diffed, and never deleted on commit.
9
+ * Directories that belong to the target project's environment,
10
+ * not to its generated contents.
11
+ *
12
+ * They are:
13
+ * - never seeded into the stage
14
+ * - never diffed
15
+ * - never deleted on commit
12
16
  *
13
17
  * (`.git` has one exception: see {@link Stage#commit})
14
18
  */
@@ -20,30 +24,31 @@ const ENVIRONMENT_DIRECTORIES = new Set(["node_modules", ".git"]);
20
24
  */
21
25
 
22
26
  /**
23
- * A copy-on-write overlay for a project directory, in the spirit of
24
- * Docker's layered file systems:
27
+ * A copy-on-write overlay for a project directory,
28
+ * in the spirit of Docker's layered file systems:
25
29
  *
26
30
  * - the target directory is the read-only lower layer
27
31
  * - a real directory in the OS temp dir is the writable upper layer,
28
32
  * seeded ("copied up") from the target
29
33
  *
30
- * All generation (bases, layers, consolidators) runs against the upper
31
- * layer via `stage.directory`. Because the upper layer is a real
32
- * directory, layers keep using `node:fs`, `ember-apply`, and even
33
- * subprocesses (`git`, package managers) exactly as they would against
34
- * the real project -- an in-process virtual fs can't offer that, since
35
- * child processes can't see it.
34
+ * All generation (bases, layers, consolidators) runs against the upper layer,
35
+ * via `stage.directory`.
36
+ *
37
+ * The upper layer is a real directory on purpose.
38
+ * Layers keep using `node:fs`, `ember-apply`, and subprocesses
39
+ * (`git`, package managers) exactly as they would against the real project.
40
+ * An in-process virtual fs can't offer that: child processes can't see it.
36
41
  *
37
- * Nothing touches the target until {@link Stage#commit}; {@link Stage#discard}
38
- * throws the upper layer away.
42
+ * Nothing touches the target until {@link Stage#commit}.
43
+ * {@link Stage#discard} throws the upper layer away.
39
44
  */
40
45
  export class Stage {
41
46
  /**
42
- * @param {string} targetDirectory the directory the project should end up in
47
+ * @param {string} targetDirectory the directory the project must end up in
43
48
  * @param {{ seed?: boolean }} [options]
44
- * seed: copy the target's current contents into the stage first
45
- * (a "copy up"). Defaults to true. Pass false to stage a from-scratch
46
- * generation (e.g. the "replace" flow).
49
+ * seed: copy the target's current contents into the stage first (a "copy up").
50
+ * Defaults to true.
51
+ * Pass false to stage a from-scratch generation (e.g. the "replace" flow).
47
52
  * @returns {Promise<Stage>}
48
53
  */
49
54
  static async create(targetDirectory, options = {}) {
@@ -80,8 +85,10 @@ export class Stage {
80
85
  }
81
86
 
82
87
  /**
83
- * The writable upper layer. Generate in here,
84
- * e.g. `new Project(stage.directory, desires)`.
88
+ * The writable upper layer.
89
+ *
90
+ * Generate in here:
91
+ * `new Project(stage.directory, desires)`
85
92
  *
86
93
  * @type {string}
87
94
  */
@@ -169,19 +176,23 @@ export class Stage {
169
176
  }
170
177
 
171
178
  /**
172
- * Applies the staged changes to the target directory, then removes the
173
- * stage. Only paths reported by {@link Stage#changes} are touched --
174
- * everything else in the target (untracked files, node_modules, .git)
175
- * is left alone.
179
+ * Applies the staged changes to the target directory, then removes the stage.
176
180
  *
177
- * One exception: when the stage has a `.git` directory and the target
178
- * does not (the git layer ran `git init` inside the stage), the `.git`
179
- * directory is carried over so a freshly generated project keeps its
180
- * history.
181
+ * Only paths reported by {@link Stage#changes} are touched.
182
+ * Everything else in the target is left alone:
183
+ * - untracked files
184
+ * - node_modules
185
+ * - .git
181
186
  *
182
- * @param {Change[]} [changes] the changes to apply -- pass a subset of
183
- * {@link Stage#changes} to apply only some of them (e.g. the ones a
184
- * user accepted during review). Defaults to all staged changes.
187
+ * One exception: when the stage has a `.git` directory and the target does not
188
+ * (the git layer ran `git init` inside the stage),
189
+ * the `.git` directory is carried over.
190
+ * A freshly generated project keeps its history that way.
191
+ *
192
+ * @param {Change[]} [changes] the changes to apply.
193
+ * Pass a subset of {@link Stage#changes} to apply only some of them
194
+ * (e.g. the ones a user accepted during review).
195
+ * Defaults to all staged changes.
185
196
  * @returns {Promise<Change[]>} the changes that were written
186
197
  */
187
198
  async commit(changes) {
@@ -222,9 +233,9 @@ export class Stage {
222
233
  }
223
234
 
224
235
  /**
225
- * All files (as paths relative to `directory`), excluding environment
226
- * directories. Includes dotfiles (`.gitignore`, `.github/`, ...), which
227
- * `fs.glob` would skip.
236
+ * All files, as paths relative to `directory`, excluding environment directories.
237
+ *
238
+ * Includes dotfiles (`.gitignore`, `.github/`, ...), which `fs.glob` skips.
228
239
  *
229
240
  * @param {string} directory
230
241
  * @returns {Promise<Set<string>>}
@@ -2,12 +2,13 @@ import { readFile, writeFile } from "node:fs/promises";
2
2
  import { toTree, print, statements } from "ember-estree";
3
3
 
4
4
  /**
5
- * Adds a property to the object passed to `defineConfig(...)` in the
6
- * project's tsdown.config.js.
5
+ * Adds a property to the object passed to `defineConfig(...)`
6
+ * in the project's tsdown.config.js.
7
7
  *
8
- * No-ops when the property is already present (layers re-run), and when
9
- * the config doesn't have the `defineConfig({ ... })` shape -- an existing
10
- * project's hand-rolled config is the user's to manage.
8
+ * No-ops when:
9
+ * - the property is already present (layers re-run)
10
+ * - the config doesn't have the `defineConfig({ ... })` shape
11
+ * (an existing project's hand-rolled config is the user's to manage)
11
12
  *
12
13
  * @param {import('#utils/project.js').Project} project
13
14
  * @param {string} propertyName
@@ -9,13 +9,14 @@ export interface Layer {
9
9
  label: string;
10
10
  hint?: string;
11
11
  /**
12
- * Whether the layer is pre-selected in the CLI (the user can still
13
- * deselect it).
12
+ * Whether the layer is pre-selected in the CLI.
13
+ * The user can still deselect it.
14
14
  */
15
15
  defaultValue?: (projectType: ProjectType) => unknown;
16
16
  /**
17
- * Optional README documentation snippet or function. Whatever is returned
18
- * will show up in the generated project's README file.
17
+ * Optional README documentation snippet or function.
18
+ *
19
+ * Whatever is returned shows up in the generated project's README file.
19
20
  */
20
21
  readme?: string | ((project: Project) => string | undefined | Promise<string | undefined>);
21
22
  /**
@@ -40,10 +41,10 @@ export interface Layer {
40
41
 
41
42
  export interface DiscoveredLayer extends Layer {
42
43
  /**
43
- * The unique name of the layer
44
- * (exact match of the folder name
45
- * not provided by the layer
46
- * )
44
+ * The unique name of the layer.
45
+ *
46
+ * An exact match of the folder name.
47
+ * Not provided by the layer itself.
47
48
  */
48
49
  name: string;
49
50
  }
@@ -8,10 +8,12 @@ import { join } from "node:path";
8
8
  import type { Project } from "ember.nvp";
9
9
 
10
10
  /**
11
- * The minimal-library base has to produce a *buildable* library: these tests
12
- * generate both flavors (JavaScript and TypeScript), snapshot the interesting
13
- * generated files, and run the real `pnpm build` (tsdown + rolldown) against
14
- * them.
11
+ * The minimal-library base has to produce a *buildable* library.
12
+ *
13
+ * These tests:
14
+ * - generate both flavors (JavaScript and TypeScript)
15
+ * - snapshot the interesting generated files
16
+ * - run the real `pnpm build` (tsdown + rolldown) against them
15
17
  */
16
18
 
17
19
  async function installAndBuild(project: Project) {
@@ -7,10 +7,12 @@ import { join } from "node:path";
7
7
 
8
8
  /**
9
9
  * The replace-or-update question only matters when a project already exists
10
- * at the target path. These tests generate a project, assert the baseline
11
- * state, then regenerate over it the way the CLI does once the user has
12
- * answered the question, and assert the resulting project output is correct
13
- * for each answer.
10
+ * at the target path.
11
+ *
12
+ * These tests:
13
+ * - generate a project and assert the baseline state
14
+ * - regenerate over it, the way the CLI does once the user has answered
15
+ * - assert the resulting project is correct for each answer
14
16
  */
15
17
  describe("generating over an existing project", () => {
16
18
  const prettier = layers.find((layer) => layer.name === "prettier")!;
@@ -3,9 +3,10 @@ import { describe, it, expect } from "vitest";
3
3
  import { hasInspectorSupport, wireInspectorSupport } from "#utils/inspector-support.js";
4
4
 
5
5
  /**
6
- * The Application definition is found structurally, so every shape an app
7
- * author may have refactored their app file into gets wired the same way
8
- * the freshly generated shape does.
6
+ * The Application definition is found structurally.
7
+ *
8
+ * So every shape an app author has refactored their app file into
9
+ * gets wired the same way the freshly generated shape does.
9
10
  */
10
11
  describe("wireInspectorSupport", () => {
11
12
  it("wires the generated shape (export default class extends Application)", () => {
@@ -43,11 +43,10 @@ describe("layer: inspector-support", () => {
43
43
  "/**
44
44
  * Looking for services that come from addons?
45
45
  *
46
+ * App-tree merging from libraries is not supported.
46
47
  * See: https://github.com/embroider-build/embroider/issues/2659
47
48
  *
48
- * We currently don't support app-tree merging from libraries.
49
- *
50
- * For services, I highly recommend looking in to either of
49
+ * For services, look in to either of:
51
50
  * - https://github.com/chancancode/ember-polaris-service-
52
51
  * - https://ember-primitives.pages.dev/6-utils/createService.md
53
52
  * - https://ember-primitives.pages.dev/6-utils/createAsyncService.md
@@ -6,9 +6,8 @@ import { rm } from "node:fs/promises";
6
6
  import type { Project } from "ember.nvp";
7
7
 
8
8
  /**
9
- * publint and are-the-types-wrong run inside the library's own build
10
- * (tsdown), so the real assertion is their report lines in `pnpm build`
11
- * output.
9
+ * publint and are-the-types-wrong run inside the library's own build (tsdown),
10
+ * so the real assertion is their report lines in `pnpm build` output.
12
11
  */
13
12
 
14
13
  describe("layers: publint + are-the-types-wrong", () => {
@@ -16,8 +16,8 @@ async function installAndTest(project: Project) {
16
16
  }
17
17
 
18
18
  /**
19
- * The layer ships testing infrastructure only; the tests exercising the
20
- * base's example exports are generated here, in both flavors.
19
+ * The layer ships testing infrastructure only.
20
+ * The tests exercising the base's example exports are generated here, in both flavors.
21
21
  */
22
22
  const exampleTests = {
23
23
  typescript: {
@@ -168,13 +168,13 @@ describe("layer: readme", () => {
168
168
 
169
169
  ### Building
170
170
 
171
- To build the library:
171
+ To build the extension:
172
172
 
173
173
  \`\`\`sh
174
174
  pnpm build
175
175
  \`\`\`
176
176
 
177
- or
177
+ or, to rebuild on change:
178
178
 
179
179
  \`\`\`sh
180
180
  pnpm build:watch
@@ -9,9 +9,12 @@ import type { Project } from "ember.nvp";
9
9
 
10
10
  /**
11
11
  * The vitest layer ships infrastructure only (config, deps, scripts).
12
- * These tests generate projects with the layer, emit specs that exercise
13
- * each project's real exports, and run the real `pnpm test` (vitest in
14
- * headless-browser mode), asserting the specs genuinely run and pass.
12
+ *
13
+ * These tests:
14
+ * - generate projects with the layer
15
+ * - emit specs that exercise each project's real exports
16
+ * - run the real `pnpm test` (vitest in headless-browser mode),
17
+ * and assert the specs run and pass
15
18
  */
16
19
 
17
20
  async function emit(project: Project, files: Record<string, string>) {
@@ -32,9 +35,11 @@ async function installAndTest(project: Project) {
32
35
  }
33
36
 
34
37
  /**
35
- * Specs against the library base's real exports, in the flavor matching
36
- * the generated project (these files are emitted by this test, not
37
- * shipped by the layer, so no automatic conversion applies).
38
+ * Specs against the library base's real exports,
39
+ * in the flavor matching the generated project.
40
+ *
41
+ * These files are emitted by this test, not shipped by the layer,
42
+ * so no automatic conversion applies.
38
43
  */
39
44
  function libraryTests(ext: "ts" | "js") {
40
45
  return {
@@ -82,9 +87,11 @@ describe("add", () => {
82
87
  }
83
88
 
84
89
  /**
85
- * Specs against the app base's real modules: boots an app made from the
86
- * app's own router class (pointed at a test-friendly location) and
87
- * application template, and visits its application route.
90
+ * Specs against the app base's real modules.
91
+ *
92
+ * Boots an app made from the app's own router class
93
+ * (pointed at a test-friendly location) and application template,
94
+ * then visits its application route.
88
95
  */
89
96
  function appTests(ext: "ts" | "js") {
90
97
  return {
@@ -0,0 +1,26 @@
1
+ import { test, expect } from "vitest";
2
+ import { execa } from "execa";
3
+ import { join } from "node:path";
4
+
5
+ const root = join(import.meta.dirname, "../..");
6
+
7
+ /**
8
+ * vitest runs modules through its own loader,
9
+ * so it does not see import cycles the way Node does.
10
+ *
11
+ * `#layers` awaits a dynamic import of each layer at the top level.
12
+ * If a layer imports a module that imports `#layers` back,
13
+ * the top-level await never settles and Node exits with code 13.
14
+ *
15
+ * Node's loader is the only thing that catches that,
16
+ * so load the module in a child process.
17
+ */
18
+ test("the layers module loads in a plain node process", async () => {
19
+ const result = await execa(process.execPath, ["src/layers/index.js"], {
20
+ cwd: root,
21
+ reject: false,
22
+ });
23
+
24
+ expect(result.stderr).not.toContain("unsettled top-level await");
25
+ expect(result.exitCode).toBe(0);
26
+ });
@@ -4,9 +4,13 @@ import { dirname, join } from "node:path";
4
4
  import type { Project } from "ember.nvp";
5
5
 
6
6
  /**
7
- * The library base generates an empty `src/`; tests that build or test a
8
- * library write this example source (a class component, a template-only
9
- * component, and a plain module) into the generated project first.
7
+ * The library base generates an empty `src/`.
8
+ *
9
+ * Tests that build or test a library write this example source
10
+ * into the generated project first:
11
+ * - a class component
12
+ * - a template-only component
13
+ * - a plain module
10
14
  */
11
15
  export const librarySource = {
12
16
  typescript: {
@@ -22,8 +22,9 @@ const INITIAL_ONLY = new Set<string>([
22
22
 
23
23
  /**
24
24
  * Layers that don't yet generate a working setup for a project type.
25
- * Every layer is *supposed* to support every base eventually -- shrink
26
- * these as layers learn.
25
+ *
26
+ * Every layer is *supposed* to support every base eventually.
27
+ * Shrink these as layers learn.
27
28
  */
28
29
  const NOT_YET_SUPPORTED: Record<ProjectType, Set<string>> = {
29
30
  // publish checks: apps aren't published
@@ -44,11 +45,14 @@ const TYPE_FOR_BASE: Record<(typeof bases)[number], ProjectType> = {
44
45
  };
45
46
 
46
47
  /**
47
- * Layers that only add lint / format / publish checks. They don't affect
48
- * the runtime shape of a project, so they permutate among themselves in a
49
- * small "checks" matrix instead of multiplying the main one. ("apply anew"
50
- * still applies each of them on top of every main permutation, so linear
51
- * cross-group coverage remains.)
48
+ * Layers that only add lint / format / publish checks.
49
+ *
50
+ * They don't affect the runtime shape of a project,
51
+ * so they permutate among themselves in a small "checks" matrix
52
+ * instead of multiplying the main one.
53
+ *
54
+ * "apply anew" still applies each of them on top of every main permutation,
55
+ * so linear cross-group coverage remains.
52
56
  */
53
57
  const CHECK_LAYERS = new Set<string>([
54
58
  "eslint-bundled-ember",
@@ -105,11 +109,14 @@ const eachBase = bases.map((base) => {
105
109
  });
106
110
 
107
111
  /**
108
- * The permutation matrix grows exponentially with the layer count, so
109
- * each base runs from its own test file(s) -- CI runs them as separate,
110
- * parallel jobs. A base whose matrix outgrows one job's budget runs from
111
- * several files, each taking a deterministic 1-of-N slice. The "checks"
112
- * group (CHECK_LAYERS) runs from its own file(s) the same way.
112
+ * The permutation matrix grows exponentially with the layer count,
113
+ * so each base runs from its own test file(s).
114
+ * CI runs them as separate, parallel jobs.
115
+ *
116
+ * A base whose matrix outgrows one job's budget runs from several files,
117
+ * each taking a deterministic 1-of-N slice.
118
+ *
119
+ * The "checks" group (CHECK_LAYERS) runs from its own file(s) the same way.
113
120
  *
114
121
  * @param base which base's permutations this file runs
115
122
  * @param options.index / options.total which 1-of-N slice of the permutations this file runs