ember.nvp 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/.github/workflows/ci.yml +22 -0
  2. package/.release-plan.json +35 -7
  3. package/CHANGELOG.md +32 -0
  4. package/README.md +41 -28
  5. package/RELEASE.md +16 -14
  6. package/package.json +1 -1
  7. package/packages/build-tooling-utils/README.md +7 -12
  8. package/packages/build-tooling-utils/package.json +2 -2
  9. package/packages/build-tooling-utils/src/extensions.ts +4 -3
  10. package/packages/build-tooling-utils/src/maybe-babel.ts +18 -15
  11. package/packages/rolldown/README.md +105 -100
  12. package/packages/rolldown/index.ts +37 -24
  13. package/packages/rolldown/package.json +4 -4
  14. package/packages/rolldown/src/app-reexports.test.ts +4 -3
  15. package/packages/rolldown/src/app-reexports.ts +37 -27
  16. package/packages/rolldown/src/babel.test.ts +10 -5
  17. package/packages/rolldown/src/babel.ts +88 -69
  18. package/packages/rolldown/src/config.ts +21 -17
  19. package/packages/rolldown/src/dts.test.ts +42 -26
  20. package/packages/rolldown/src/externals.test.ts +16 -11
  21. package/packages/rolldown/src/externals.ts +18 -12
  22. package/packages/rolldown/src/isolated-declarations.ts +56 -43
  23. package/packages/rolldown/src/plugin-like.ts +18 -15
  24. package/packages/rolldown/src/published-types.test.ts +18 -13
  25. package/packages/rolldown/src/transform.test.ts +26 -17
  26. package/packages/rolldown/src/transform.ts +68 -48
  27. package/packages/vite/README.md +10 -11
  28. package/packages/vite/package.json +2 -2
  29. package/packages/vite/src/ember.ts +44 -36
  30. package/packages/vite/src/test-html.ts +13 -11
  31. package/src/bases/README.md +6 -3
  32. package/src/bases/minimal-app/app.test.ts +7 -4
  33. package/src/bases/minimal-app/files/app/app.ts +2 -3
  34. package/src/bases/minimal-app/files/app/router.ts +3 -3
  35. package/src/bases/minimal-app/index.js +10 -6
  36. package/src/bases/minimal-custom-element/files/README.md +16 -12
  37. package/src/bases/minimal-extension/files/README.md +13 -5
  38. package/src/bases/minimal-extension/files/app/app.ts +2 -3
  39. package/src/bases/minimal-extension/files/app/config.ts +5 -3
  40. package/src/bases/minimal-extension/index.js +16 -11
  41. package/src/bases/minimal-library/files/README.md +8 -7
  42. package/src/bases/minimal-library/index.js +21 -15
  43. package/src/cli/index.js +23 -18
  44. package/src/cli/questions/write.js +12 -10
  45. package/src/layers/are-the-types-wrong/index.js +4 -4
  46. package/src/layers/github-actions/index.js +7 -4
  47. package/src/layers/inspector-support/index.js +2 -2
  48. package/src/layers/qunit/index.js +18 -14
  49. package/src/layers/readme/index.js +2 -4
  50. package/src/layers/renovate/index.js +2 -1
  51. package/src/layers/typescript/index.js +12 -12
  52. package/src/layers/vitest/index.js +7 -3
  53. package/src/utils/babel.js +3 -2
  54. package/src/utils/buffer.js +2 -2
  55. package/src/utils/cli.js +4 -4
  56. package/src/utils/fs.js +23 -14
  57. package/src/utils/inspector-support.js +22 -19
  58. package/src/utils/npm.js +15 -12
  59. package/src/utils/project-type.js +19 -0
  60. package/src/utils/project.js +4 -13
  61. package/src/utils/remove-types.js +2 -4
  62. package/src/utils/rewrite-imports.js +43 -31
  63. package/src/utils/stage.js +44 -33
  64. package/src/utils/tsdown-config.js +6 -5
  65. package/src/utils/types.ts +9 -8
  66. package/test/ember.nvp/base-library.test.ts +6 -4
  67. package/test/ember.nvp/generator-replace-or-update.test.ts +6 -4
  68. package/test/ember.nvp/inspector-support.test.ts +4 -3
  69. package/test/ember.nvp/layer-inspector-support.test.ts +2 -3
  70. package/test/ember.nvp/layer-publish-checks.test.ts +2 -3
  71. package/test/ember.nvp/layer-qunit.test.ts +2 -2
  72. package/test/ember.nvp/layer-readme.test.ts +2 -2
  73. package/test/ember.nvp/layer-vitest.test.ts +16 -9
  74. package/test/ember.nvp/layers-load.test.ts +26 -0
  75. package/test/ember.nvp/library-src-fixtures.ts +7 -3
  76. package/test/ember.nvp/permutations/shared.ts +19 -12
  77. package/test/ember.nvp/publish-configs.test.ts +44 -31
  78. package/test/ember.nvp/rewrite-imports.test.ts +4 -3
  79. package/test/ember.nvp/scoped-css-library.test.ts +6 -4
  80. package/test/test-helpers/permutate.test.ts +3 -3
  81. package/test/test-helpers.ts +10 -6
package/src/cli/index.js CHANGED
@@ -20,7 +20,7 @@ import { askReplaceOrUpdate } from "./questions/replace-or-update.js";
20
20
  * This whole file's primary purpose is to be an interactive CLI
21
21
  * for `generateProject`.
22
22
  *
23
- * Other tools can call `generateProject` themselves if they wish
23
+ * Other tools can call `generateProject` themselves
24
24
  * if they want to provide different TUI/GUIs.
25
25
  */
26
26
  async function main() {
@@ -38,19 +38,19 @@ async function main() {
38
38
  await askIfOK();
39
39
 
40
40
  /**
41
- * Everything is generated in a stage: a real directory (in the OS temp
42
- * dir) acting as a writable overlay of the target directory.
41
+ * Everything is generated in a stage:
42
+ * a real directory (in the OS temp dir) acting as a writable overlay of the target directory.
43
43
  *
44
- * When updating an existing project, the stage is seeded with a full
45
- * copy of it (sans node_modules/.git), so the bases and layers run
46
- * against the project's current state -- isSetup checks, package.json
47
- * reads, file existence checks, etc. all see the existing files, and
48
- * the codemods modify them in the stage exactly as if they were
49
- * operating in place. "replace" starts from an empty stage instead.
44
+ * When updating an existing project, the stage is seeded with a full copy of it
45
+ * (sans node_modules/.git), so the bases and layers run against the project's current state:
46
+ * - isSetup checks, package.json reads, file existence checks, etc. all see the existing files
47
+ * - the codemods modify them in the stage exactly as if they were operating in place
50
48
  *
51
- * The target itself is not touched until stage.commit() below, which
52
- * applies the difference between the stage and the target (or the
53
- * subset of it the user accepts during review).
49
+ * "replace" starts from an empty stage instead.
50
+ *
51
+ * The target itself is not touched until stage.commit() below.
52
+ * That applies the difference between the stage and the target
53
+ * (or the subset of it the user accepts during review).
54
54
  */
55
55
  const stage = await Stage.create(projectPath, { seed: replaceOrUpdate !== "replace" });
56
56
 
@@ -98,9 +98,12 @@ async function main() {
98
98
  }
99
99
 
100
100
  /**
101
- * New projects (and "replace", which was already confirmed) are written
102
- * without asking. Updates to an existing project must be confirmed:
103
- * write all / review each change (accept/reject) / cancel.
101
+ * New projects (and "replace", which was already confirmed) are written without asking.
102
+ *
103
+ * Updates to an existing project must be confirmed:
104
+ * - write all
105
+ * - review each change (accept/reject)
106
+ * - cancel
104
107
  */
105
108
  const isUpdate = !stage.isNew && replaceOrUpdate !== "replace";
106
109
 
@@ -123,9 +126,11 @@ async function main() {
123
126
 
124
127
  /**
125
128
  * Converted / generated files are written expecting the project's own
126
- * formatting and lint pass to run right away (e.g. blank lines between
127
- * import groups are `eslint --fix`'s job) -- so when the project has a
128
- * lint:fix script, surface it as the step right after install.
129
+ * formatting and lint pass to run right away.
130
+ * (e.g. blank lines between import groups are `eslint --fix`'s job)
131
+ *
132
+ * So when the project has a lint:fix script,
133
+ * surface it as the step right after install.
129
134
  */
130
135
  let lintFixStep = "";
131
136
  try {
@@ -17,10 +17,11 @@ function isValid(selected) {
17
17
  }
18
18
 
19
19
  /**
20
- * Nothing has been written to the target directory yet -- everything so
21
- * far only exists in the stage. Ask whether to write the staged changes,
22
- * with the option to review the diff change-by-change, accepting or
23
- * rejecting each one.
20
+ * Nothing has been written to the target directory yet.
21
+ * Everything so far only exists in the stage.
22
+ *
23
+ * Ask whether to write the staged changes,
24
+ * with the option to review the diff change-by-change, accepting or rejecting each one.
24
25
  *
25
26
  * @param {import('#utils/stage.js').Stage} stage
26
27
  * @param {import('#utils/stage.js').Change[]} changes
@@ -61,10 +62,11 @@ export async function askToWrite(stage, changes) {
61
62
  }
62
63
 
63
64
  /**
64
- * Show each change's diff and ask what to do with it. "accept remaining"
65
- * and "reject remaining" finish the review early, so they require an
66
- * extra confirmation; declining the confirmation returns to the change
67
- * being reviewed.
65
+ * Show each change's diff and ask what to do with it.
66
+ *
67
+ * "accept remaining" and "reject remaining" finish the review early,
68
+ * so they require an extra confirmation.
69
+ * Declining the confirmation returns to the change being reviewed.
68
70
  *
69
71
  * @param {import('#utils/stage.js').Stage} stage
70
72
  * @param {import('#utils/stage.js').Change[]} changes
@@ -83,8 +85,8 @@ async function reviewEachChange(stage, changes) {
83
85
 
84
86
  const remaining = changes.length - index;
85
87
 
86
- // Bypass clack for the diff itself: diffs are long, and clack's
87
- // log decorations wrap every line.
88
+ // Bypass clack for the diff itself.
89
+ // Diffs are long, and clack's log decorations wrap every line.
88
90
  console.log("\n" + (await stage.diff([change])));
89
91
 
90
92
  const answer = await p.select({
@@ -20,8 +20,8 @@ export default {
20
20
  // Declarations are the thing being checked
21
21
  if (!(await project.hasOrWantsLayer("typescript"))) return;
22
22
 
23
- // ESM-only packages don't offer the CJS resolution modes the strict
24
- // profile requires
23
+ // ESM-only packages don't offer the CJS resolution modes
24
+ // that the strict profile requires
25
25
  await addTsdownConfigProperty(project, "attw", `attw: { profile: "esm-only" }`);
26
26
  await packageJson.addDevDependencies(await getLatest(deps), project.directory);
27
27
  },
@@ -42,8 +42,8 @@ export default {
42
42
  /** @type {string[]} */
43
43
  const reasons = [];
44
44
 
45
- // Nothing to set up outside TypeScript libraries, so nothing can be
46
- // missing
45
+ // Nothing to set up outside TypeScript libraries,
46
+ // so nothing can be missing
47
47
  if (!project.isLibrary || !(await project.hasOrWantsLayer("typescript"))) {
48
48
  return explain ? { isSetup: true, reasons } : true;
49
49
  }
@@ -55,8 +55,9 @@ export default {
55
55
  };
56
56
 
57
57
  /**
58
- * This is a very silly way to handle YAML, but 🤷
59
- * will update if this starts getting to be troublesome.
58
+ * String concatenation is a very silly way to handle YAML, but 🤷
59
+ *
60
+ * Will update if this starts getting to be troublesome.
60
61
  */
61
62
  const preamble = `name: CI
62
63
 
@@ -135,8 +136,10 @@ async function transform(project) {
135
136
  content = await addOrUpdateTests(project, content);
136
137
 
137
138
  /**
138
- * If we've still added nothing, we can't actually add the preamble
139
- * because there are no jobs to run (we need at least one job).
139
+ * A workflow needs at least one job.
140
+ *
141
+ * If nothing was added, there is no job to run,
142
+ * so the preamble is not added either.
140
143
  */
141
144
  if (content === "") {
142
145
  return { content, original, didChange: original !== content, outputPath };
@@ -42,8 +42,8 @@ export default {
42
42
  await writeFile(path, wired);
43
43
  }
44
44
 
45
- // the dependency is only useful once the Application actually got
46
- // wired (the codemod leaves unrecognized Application shapes alone)
45
+ // The codemod leaves unrecognized Application shapes alone.
46
+ // The dependency is only useful once the Application is wired.
47
47
  if (!hasInspectorSupport(wired, path)) return;
48
48
 
49
49
  await packageJson.addDevDependencies(await getLatest(deps), project.directory);
@@ -18,9 +18,12 @@ const tsDeps = {
18
18
  };
19
19
 
20
20
  /**
21
- * Libraries don't have the app base's vite stack, so the test build
22
- * brings the pieces the generated configs and test-helper import
23
- * directly. Everything else (babel itself, the embroider machinery)
21
+ * Libraries don't have the app base's vite stack.
22
+ *
23
+ * So the test build brings the pieces that the generated configs
24
+ * and test-helper import directly.
25
+ *
26
+ * Everything else (babel itself, the embroider machinery)
24
27
  * comes with @nullvoxpopuli/ember-vite.
25
28
  */
26
29
  const libraryDeps = {
@@ -79,7 +82,6 @@ export default {
79
82
  project.directory,
80
83
  );
81
84
 
82
- // Add scripts
83
85
  await packageJson.addScripts(
84
86
  {
85
87
  "build:test": "EMBER_ENV=test NODE_ENV=development vite build --mode development",
@@ -151,10 +153,11 @@ export default {
151
153
  };
152
154
 
153
155
  /**
154
- * Tests import the library's source (not its published dist), so the
155
- * `#src/*` subpath must exist before the test files are applied --
156
- * applying rewrites their import extensions by resolving each specifier
157
- * through this mapping.
156
+ * Tests import the library's source, not its published dist.
157
+ *
158
+ * The `#src/*` subpath must exist before the test files are applied:
159
+ * applying rewrites their import extensions
160
+ * by resolving each specifier through this mapping.
158
161
  *
159
162
  * @param {import('#utils/project.js').Project} project
160
163
  */
@@ -166,12 +169,13 @@ async function addSourceImports(project) {
166
169
  }
167
170
 
168
171
  /**
169
- * The test babel config is the qunit layer's own (a root babel config,
170
- * when a library has one, belongs to the publish build), so this layer
171
- * also keeps the config's TypeScript handling in step with the project:
172
- * the shipped config carries the TS plugin, which must not survive in a
173
- * JavaScript project, and must come back when a project adopts the
174
- * typescript layer later.
172
+ * The test babel config is the qunit layer's own.
173
+ * (a root babel config, when a library has one, belongs to the publish build)
174
+ *
175
+ * So this layer keeps the config's TypeScript handling in step with the project:
176
+ * - the shipped config carries the TS plugin
177
+ * - it must not survive in a JavaScript project
178
+ * - it must come back when a project adopts the typescript layer later
175
179
  *
176
180
  * @param {import('#utils/project.js').Project} project
177
181
  * @param {boolean} ts
@@ -74,13 +74,13 @@ ${project.runPrefix} start
74
74
 
75
75
  ### Building
76
76
 
77
- To build the library:
77
+ To build the extension:
78
78
 
79
79
  \`\`\`sh
80
80
  ${project.runPrefix} build
81
81
  \`\`\`
82
82
 
83
- or
83
+ or, to rebuild on change:
84
84
 
85
85
  \`\`\`sh
86
86
  ${project.runPrefix} build:watch
@@ -175,7 +175,6 @@ export default {
175
175
  * @param {import('#utils/project.js').Project} project
176
176
  */
177
177
  async run(project) {
178
- // Collect layer docs
179
178
  const layerDocs = [];
180
179
  for (const layer of project.desires.layers) {
181
180
  if (layer.name === "readme" || !layer.readme) {
@@ -199,7 +198,6 @@ export default {
199
198
  layerDocsMarkdown = `\n## Features & Tooling\n\n${layerDocs.join("\n\n")}\n`;
200
199
  }
201
200
 
202
- // Merge the layers' snippets into the main readme template
203
201
  let content;
204
202
  switch (project.type) {
205
203
  case "extension":
@@ -14,7 +14,8 @@ export default {
14
14
  return;
15
15
  }
16
16
 
17
- // extensions aren't published to npm either, so they follow the app config
17
+ // extensions aren't published to npm either,
18
+ // so they follow the app config
18
19
  let source = project.isLibrary
19
20
  ? join(import.meta.dirname, "files/library.json5")
20
21
  : join(import.meta.dirname, "files/app.json5");
@@ -4,7 +4,7 @@ import { cp } from "node:fs/promises";
4
4
  import { existsSync } from "node:fs";
5
5
  import { hasConfiguredTSBabel, prependPlugin } from "#utils/babel.js";
6
6
  import { getLatest } from "#utils/npm.js";
7
- import { isLibraryType } from "#utils/project.js";
7
+ import { isLibraryType } from "#utils/project-type.js";
8
8
 
9
9
  const bases = join(import.meta.dirname, "../../bases");
10
10
  const appBase = join(bases, "minimal-app/files");
@@ -20,8 +20,8 @@ const sharedDeps = {
20
20
  };
21
21
 
22
22
  const appDeps = {
23
- // Apps strip types via their own babel.config.js; libraries have no babel
24
- // config -- ember() handles type stripping.
23
+ // Apps strip types via their own babel.config.js.
24
+ // Libraries have no babel config: ember() handles type stripping.
25
25
  "@babel/plugin-transform-typescript": "^7.28.5",
26
26
  "@ember/app-tsconfig": "^2.0.0",
27
27
  };
@@ -47,8 +47,8 @@ export default {
47
47
  label: "TypeScript",
48
48
 
49
49
  /**
50
- * Libraries publish declarations, so they are TypeScript unless the
51
- * user opts out.
50
+ * Libraries publish declarations,
51
+ * so they are TypeScript unless the user opts out.
52
52
  *
53
53
  * @param {import('#types').ProjectType} projectType
54
54
  */
@@ -58,10 +58,8 @@ export default {
58
58
 
59
59
  async run(project) {
60
60
  /**
61
- * if jsconfig exists, switch to tsconfig
62
- */
63
- /**
64
- * if tsconfig exists,
61
+ * TODO:
62
+ * - if jsconfig exists, switch to tsconfig
65
63
  */
66
64
  await addTSConfig(project);
67
65
  await updatePackageJson(project);
@@ -89,8 +87,8 @@ export default {
89
87
  reasons.push("tsconfig.json is missing");
90
88
  }
91
89
 
92
- // Only projects with their own babel config need the TS plugin in it;
93
- // without one (libraries), ember() strips types.
90
+ // Only projects with their own babel config need the TS plugin in it.
91
+ // Without one (libraries), ember() strips types.
94
92
  if (project.hasFile("babel.config.js") && !(await hasConfiguredTSBabel(project))) {
95
93
  if (!explain) return false;
96
94
 
@@ -151,6 +149,8 @@ async function updatePackageJson(project) {
151
149
  }
152
150
 
153
151
  /**
152
+ * Copies the base's tsconfig, unless the project already has one.
153
+ *
154
154
  * @param {import('#utils/project.js').Project} project
155
155
  */
156
156
  async function addTSConfig(project) {
@@ -183,7 +183,7 @@ async function addTSConfig(project) {
183
183
  */
184
184
  async function updateBabelConfig(project) {
185
185
  if (!project.hasFile("babel.config.js")) {
186
- // No babel config to patch (libraries): ember() strips types.
186
+ // Nothing to patch (libraries): ember() strips types.
187
187
  return;
188
188
  }
189
189
 
@@ -13,9 +13,13 @@ const deps = {
13
13
  webdriverio: "^9.29.1",
14
14
  };
15
15
 
16
- // Apps already get these from their base. The babel plugins are resolved
17
- // by name from the project by @nullvoxpopuli/ember-vite's no-config babel
18
- // fallback (decorator-transforms is already a base dependency).
16
+ /**
17
+ * Apps already get these from their base.
18
+ *
19
+ * The babel plugins are resolved by name from the project,
20
+ * by @nullvoxpopuli/ember-vite's no-config babel fallback.
21
+ * (decorator-transforms is already a base dependency)
22
+ */
19
23
  const libraryDeps = {
20
24
  "@babel/plugin-transform-typescript": "^7.28.10",
21
25
  "@nullvoxpopuli/ember-vite": "workspace:*",
@@ -80,8 +80,9 @@ export async function prependPlugin(project, plugin, configPath = "babel.config.
80
80
  }
81
81
 
82
82
  /**
83
- * The plugins value may be the array itself, or the array with
84
- * methods chained off of it: `[ ... ].filter(Boolean)`
83
+ * The plugins value may be:
84
+ * - the array itself
85
+ * - the array with methods chained off of it: `[ ... ].filter(Boolean)`
85
86
  *
86
87
  * @param {any} node
87
88
  * @returns {any | undefined} the underlying ArrayExpression
@@ -1,8 +1,8 @@
1
1
  /**
2
+ * Takes a substring of a string based on byte offsets.
3
+ *
2
4
  * from https://github.com/ember-cli/ember-cli/blob/8ea5b2e22d37805f5749a4917e0ef25fac3c9cda/lib/models/blueprint.js#L1789
3
5
  *
4
- * Takes a substring of a string based on byte offsets.
5
-
6
6
  * @param {string} value : The input string.
7
7
  * @param {number} start : The byte index of the substring start.
8
8
  * @param {number} end : The byte index of the substring end.
package/src/utils/cli.js CHANGED
@@ -7,12 +7,12 @@ export function isInteractive() {
7
7
  }
8
8
 
9
9
  /**
10
- * I would like to have selecs look more like a table,
10
+ * I would like to have selects look more like a table,
11
11
  * but @clack/prompts makes this a bit hard.
12
12
  *
13
- * I tried using padding, but then when it prints your selection,
14
- * I don't think there is a way to format just that, so
15
- * there are a bunch of goofy spacings in the labels.
13
+ * I tried using padding.
14
+ * But when it prints your selection, I don't think there is a way to format just that.
15
+ * So there are a bunch of goofy spacings in the labels.
16
16
  */
17
17
  const PADDING = 10;
18
18
 
package/src/utils/fs.js CHANGED
@@ -7,17 +7,22 @@ import { rewriteImportsToMatchFiles } from "./rewrite-imports.js";
7
7
 
8
8
  /**
9
9
  * `**\/*` alone skips dotfiles, which bases legitimately provide
10
- * (`.gitignore`, `.env.development`, ...): the extra patterns match
11
- * dotfiles at any depth and files inside dot-directories (`.github/...`).
10
+ * (`.gitignore`, `.env.development`, ...).
11
+ *
12
+ * The extra patterns match:
13
+ * - dotfiles at any depth
14
+ * - files inside dot-directories (`.github/...`)
12
15
  */
13
16
  const EVERY_FILE = ["**/*", "**/.*", "**/.*/**/*"];
14
17
 
15
18
  /**
19
+ * Modified version of applyFolder from ember-apply.
16
20
  *
17
- * Modified version of applyFolder from ember-apply
18
- * where the caller gets to decide the final output filPath and contents - whereas the version from ember-apply only allows similar file paths in source and destination.
21
+ * Here, the caller decides the final output path and contents.
22
+ * The version from ember-apply only allows the same file paths
23
+ * in source and destination.
19
24
  *
20
- * We need customization because we will intake ts and output js
25
+ * We need that freedom because we intake ts and output js.
21
26
  *
22
27
  * @param {string} from
23
28
  * @param {{to: import('#utils/project.js').Project, process: (data: { entry: string, contents: string }) => string | Promise<void>}} options sub folder within the target project to copy the contents to
@@ -56,11 +61,12 @@ export async function applyFolderTo(from, project) {
56
61
  /**
57
62
  * Files we wrote, to be import-rewritten in a second pass.
58
63
  *
59
- * Rewriting has to wait until every file is on disk: it checks which
60
- * file a specifier actually resolves to, so rewriting while the folder
61
- * is still half-applied depends on iteration order (e.g. `index.js`
62
- * pointing at `./utils/math.ts` before `math.js` exists would be left
63
- * alone, emitting a broken import).
64
+ * Rewriting has to wait until every file is on disk.
65
+ * It checks which file a specifier actually resolves to,
66
+ * so rewriting a half-applied folder depends on iteration order.
67
+ *
68
+ * For example: `index.js` pointing at `./utils/math.ts` before `math.js` exists
69
+ * would be left alone, emitting a broken import.
64
70
  *
65
71
  * @type {string[]}
66
72
  */
@@ -110,10 +116,13 @@ export async function applyFolderTo(from, project) {
110
116
  });
111
117
 
112
118
  /**
113
- * Imports must always match the emitted files (and carry their
114
- * extension). The util decides which files it applies to and is a
115
- * no-op for everything else. Files that already existed are the
116
- * user's; we leave them alone.
119
+ * Imports must always match the emitted files, and carry their extension.
120
+ *
121
+ * The util decides which files it applies to.
122
+ * It is a no-op for everything else.
123
+ *
124
+ * Files that already existed are the user's.
125
+ * We leave them alone.
117
126
  */
118
127
  for (let filePath of written) {
119
128
  let contents = await readFile(filePath, "utf-8");
@@ -9,9 +9,9 @@ export const INSPECTOR_PACKAGE = "@embroider/legacy-inspector-support";
9
9
  export const INSPECTOR_ENTRY = `${INSPECTOR_PACKAGE}/ember-source-4.12`;
10
10
 
11
11
  /**
12
- * Modules whose default export is the Application (sub)class an app
13
- * extends. The class extending one of these is where inspector support
14
- * gets attached.
12
+ * Modules whose default export is the Application (sub)class an app extends.
13
+ *
14
+ * The class extending one of these is where inspector support gets attached.
15
15
  */
16
16
  const APPLICATION_MODULES = new Set(["ember-strict-application-resolver", "@ember/application"]);
17
17
 
@@ -56,10 +56,11 @@ function classMembers(classNode) {
56
56
  }
57
57
 
58
58
  /**
59
- * Everything wiring and detection need to know about the module: its
60
- * import declarations, the import from {@link INSPECTOR_PACKAGE} (any
61
- * subpath), the class extending an Application import, and that class's
62
- * `inspector` member.
59
+ * Everything wiring and detection need to know about the module:
60
+ * - its import declarations
61
+ * - the import from {@link INSPECTOR_PACKAGE} (any subpath)
62
+ * - the class extending an Application import
63
+ * - that class's `inspector` member
63
64
  *
64
65
  * @param {string} code
65
66
  * @param {string} filePath
@@ -117,9 +118,9 @@ function analyze(code, filePath) {
117
118
  }
118
119
 
119
120
  /**
120
- * Whether the module already wires inspector support: it imports
121
- * {@link INSPECTOR_PACKAGE} and its Application class carries the
122
- * `inspector` member.
121
+ * Whether the module already wires inspector support:
122
+ * - it imports {@link INSPECTOR_PACKAGE}
123
+ * - its Application class carries the `inspector` member
123
124
  *
124
125
  * @param {string} code
125
126
  * @param {string} filePath
@@ -143,16 +144,18 @@ export function hasInspectorSupport(code, filePath) {
143
144
  * }
144
145
  * ```
145
146
  *
146
- * The Application definition is found structurally -- any class
147
- * (declaration or expression, named, anonymous, or assigned to a
148
- * variable) extending a binding imported from an Application-providing
149
- * module -- so user-restructured app files are wired the same as
150
- * freshly generated ones.
147
+ * The Application definition is found structurally:
148
+ * any class extending a binding imported from an Application-providing module.
149
+ * - declaration or expression
150
+ * - named, anonymous, or assigned to a variable
151
+ *
152
+ * So user-restructured app files are wired the same as freshly generated ones.
153
+ *
154
+ * Idempotent.
155
+ * Already-wired (or partially wired) modules keep their existing import name and member.
151
156
  *
152
- * Idempotent: already-wired (or partially wired) modules keep their
153
- * existing import name and member. Returns the code unchanged when
154
- * there is nothing to do -- including when no Application definition
155
- * can be found.
157
+ * Returns the code unchanged when there is nothing to do,
158
+ * including when no Application definition can be found.
156
159
  *
157
160
  * @param {string} code
158
161
  * @param {string} filePath
package/src/utils/npm.js CHANGED
@@ -8,9 +8,10 @@ import { join, resolve } from "node:path";
8
8
  const CACHE = {};
9
9
 
10
10
  /**
11
- * Local, not-yet-published workspace packages that must be `link:`ed rather
12
- * than resolved from the registry. Maps the published package name to its
13
- * folder under `packages/`.
11
+ * Local, not-yet-published workspace packages.
12
+ * These must be `link:`ed rather than resolved from the registry.
13
+ *
14
+ * Maps the published package name to its folder under `packages/`.
14
15
  *
15
16
  * @type {{ [name: string]: string }}
16
17
  */
@@ -37,16 +38,18 @@ export async function getLatest(deps) {
37
38
 
38
39
  /**
39
40
  * HACK FOR CI.
40
- * In practice, these packages will be published separately, and those
41
- * versions will be used. Only the local, not-yet-published
42
- * @nullvoxpopuli/ember-* packages need the link; every other dependency
43
- * must still resolve its real version.
44
41
  *
45
- * We use `link:` (a symlink) rather than `file:` (a copy into the store)
46
- * on purpose: the packages ship TypeScript source, and Node 24 refuses to
47
- * strip types for files physically located under node_modules. A symlink
48
- * makes Node resolve the realpath to packages/* (outside node_modules),
49
- * so its `.ts` runs directly.
42
+ * In practice, these packages will be published separately,
43
+ * and those versions will be used.
44
+ *
45
+ * Only the local, not-yet-published @nullvoxpopuli/ember-* packages need the link.
46
+ * Every other dependency must still resolve its real version.
47
+ *
48
+ * We use `link:` (a symlink) rather than `file:` (a copy into the store) on purpose:
49
+ * - the packages ship TypeScript source
50
+ * - Node 24 refuses to strip types for files physically located under node_modules
51
+ * - a symlink makes Node resolve the realpath to packages/* (outside node_modules),
52
+ * so its `.ts` runs directly
50
53
  */
51
54
  if (needsLocalLink && LOCAL_PACKAGES[dep]) {
52
55
  version =
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Libraries and custom elements share one toolchain:
3
+ * - built with tsdown
4
+ * - no babel config
5
+ * - published to npm
6
+ *
7
+ * Layers that branch on "is this a library?" must treat both the same.
8
+ *
9
+ * This module has no imports on purpose.
10
+ * Layers are loaded by `#layers`, and `#utils/project.js` imports `#layers`.
11
+ * A layer that imports `#utils/project.js` at the top level
12
+ * creates a cycle that never settles, and Node exits with code 13.
13
+ *
14
+ * @param {import('#types').ProjectType} type
15
+ * @returns {boolean}
16
+ */
17
+ export function isLibraryType(type) {
18
+ return type === "library" || type === "custom-element";
19
+ }
@@ -5,24 +5,15 @@ import { join } from "node:path";
5
5
  import { hasGit } from "#utils/git.js";
6
6
  import { assert } from "node:console";
7
7
  import { layers, layerNames } from "#layers";
8
+ import { isLibraryType } from "#utils/project-type.js";
8
9
 
9
- /**
10
- * Libraries and custom elements share one toolchain: built with tsdown,
11
- * no babel config, published to npm. Layers that branch on "is this a
12
- * library?" must treat both the same.
13
- *
14
- * @param {import('#types').ProjectType} type
15
- * @returns {boolean}
16
- */
17
- export function isLibraryType(type) {
18
- return type === "library" || type === "custom-element";
19
- }
10
+ export { isLibraryType };
20
11
 
21
12
  /**
22
13
  * State container for the project.
23
14
  *
24
- * May eventually include information for discovering existing state
25
- * about a project.
15
+ * May eventually include information for discovering
16
+ * existing state about a project.
26
17
  */
27
18
  export class Project {
28
19
  #directory;