ember.nvp 0.6.0 → 1.0.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 (85) hide show
  1. package/.github/workflows/ci.yml +17 -2
  2. package/.release-plan.json +29 -5
  3. package/CHANGELOG.md +73 -0
  4. package/package.json +3 -2
  5. package/packages/build-tooling-utils/README.md +37 -0
  6. package/packages/build-tooling-utils/index.ts +2 -0
  7. package/packages/build-tooling-utils/package.json +42 -0
  8. package/packages/build-tooling-utils/rolldown.config.js +18 -0
  9. package/packages/build-tooling-utils/src/extensions.ts +18 -0
  10. package/packages/build-tooling-utils/src/maybe-babel.ts +146 -0
  11. package/packages/rolldown/README.md +120 -0
  12. package/packages/rolldown/index.ts +60 -0
  13. package/packages/rolldown/package.json +59 -0
  14. package/packages/rolldown/rolldown.config.js +21 -0
  15. package/packages/rolldown/src/babel.ts +136 -0
  16. package/packages/rolldown/src/config.ts +50 -0
  17. package/packages/rolldown/src/externals.ts +83 -0
  18. package/packages/rolldown/src/isolated-declarations.ts +65 -0
  19. package/packages/rolldown/src/transform.ts +162 -0
  20. package/packages/vite/README.md +7 -21
  21. package/packages/vite/index.ts +1 -1
  22. package/packages/vite/package.json +4 -6
  23. package/packages/vite/src/ember.ts +129 -15
  24. package/packages/vite/src/test-html.ts +86 -0
  25. package/src/bases/minimal-app/files/app/router.ts +1 -0
  26. package/src/bases/minimal-app/files/tsconfig.json +1 -0
  27. package/src/bases/minimal-app/index.js +2 -2
  28. package/src/bases/minimal-library/files/README.md +41 -0
  29. package/src/bases/minimal-library/files/package.json +49 -0
  30. package/src/bases/minimal-library/files/tsconfig.json +10 -0
  31. package/src/bases/minimal-library/files/tsdown.config.js +7 -0
  32. package/src/bases/minimal-library/index.js +158 -2
  33. package/src/cli/index.js +21 -2
  34. package/src/cli/questions/layers.js +5 -2
  35. package/src/cli/questions/project-type.js +0 -5
  36. package/src/layers/are-the-types-wrong/index.js +74 -0
  37. package/src/layers/git/files/gitignore +14 -1
  38. package/src/layers/git/index.js +6 -5
  39. package/src/layers/github-actions/index.js +12 -8
  40. package/src/layers/index.js +1 -1
  41. package/src/layers/inspector-support/index.js +101 -0
  42. package/src/layers/prettier/files/.prettierignore +4 -0
  43. package/src/layers/publint/index.js +68 -0
  44. package/src/layers/qunit/files/tests/unit/macros-test.ts +2 -2
  45. package/src/layers/qunit/index.js +107 -7
  46. package/src/layers/qunit/library-files/config/test/babel.config.js +41 -0
  47. package/src/layers/qunit/library-files/config/test/testem.cjs +26 -0
  48. package/src/layers/qunit/library-files/tests/test-helper.ts +36 -0
  49. package/src/layers/qunit/library-files/tests/unit/.gitkeep +0 -0
  50. package/src/layers/qunit/library-files/vite.config.mjs +6 -0
  51. package/src/layers/typescript/index.js +43 -7
  52. package/src/layers/vitest/files/vitest.config.mjs +11 -11
  53. package/src/layers/vitest/index.js +70 -32
  54. package/src/utils/babel.js +9 -6
  55. package/src/utils/fs.js +44 -3
  56. package/src/utils/inspector-support.js +196 -0
  57. package/src/utils/npm.js +21 -7
  58. package/src/utils/remove-types.js +5 -0
  59. package/src/utils/rewrite-imports.js +157 -0
  60. package/src/utils/tsdown-config.js +85 -0
  61. package/src/utils/types.ts +5 -1
  62. package/test/ember.nvp/base-library.test.ts +236 -0
  63. package/test/ember.nvp/generator-replace-or-update.test.ts +2 -3
  64. package/test/ember.nvp/inspector-support.test.ts +179 -0
  65. package/test/ember.nvp/layer-inspector-support.test.ts +69 -0
  66. package/test/ember.nvp/layer-publish-checks.test.ts +109 -0
  67. package/test/ember.nvp/layer-qunit.test.ts +381 -16
  68. package/test/ember.nvp/layer-vitest.test.ts +250 -0
  69. package/test/ember.nvp/layers.test.ts +16 -0
  70. package/test/ember.nvp/library-src-fixtures.ts +81 -0
  71. package/test/ember.nvp/permutations/app-1.test.ts +3 -0
  72. package/test/ember.nvp/permutations/app-2.test.ts +3 -0
  73. package/test/ember.nvp/permutations/library-1.test.ts +3 -0
  74. package/test/ember.nvp/permutations/library-2.test.ts +3 -0
  75. package/test/ember.nvp/permutations/shared.ts +239 -0
  76. package/test/ember.nvp/rewrite-imports.test.ts +235 -0
  77. package/test/test-helpers/permutate.test.ts +17 -0
  78. package/test/test-helpers.ts +46 -8
  79. package/tsconfig.json +1 -0
  80. package/packages/vite/src/maybe-babel.ts +0 -219
  81. package/src/layers/qunit/files/run-tests.mjs +0 -30
  82. package/src/layers/vitest/files/tests/test-helper.ts +0 -5
  83. package/test/ember.nvp/permutations.test.ts +0 -200
  84. /package/src/{layers/vitest/files/tests/integration/.gitkeep → bases/minimal-library/files/src/index.ts} +0 -0
  85. /package/src/layers/{vitest/files/tests/unit → qunit/library-files/tests/rendering}/.gitkeep +0 -0
@@ -9,21 +9,50 @@ const bases = join(import.meta.dirname, "../../bases");
9
9
  const appBase = join(bases, "minimal-app/files");
10
10
  const libraryBase = join(bases, "minimal-library/files");
11
11
 
12
- const deps = {
13
- "@babel/plugin-transform-typescript": "^7.28.5",
14
- "@ember/app-tsconfig": "^2.0.0",
12
+ const sharedDeps = {
15
13
  "@glint/ember-tsc": "^1.0.8",
16
14
  "@glint/template": "^1.7.3",
17
15
  "@glint/tsserver-plugin": "^2.0.8",
18
- typescript: "^5.9.3",
16
+ typescript: "^6.0.3",
17
+ };
18
+
19
+ const appDeps = {
20
+ // Apps strip types via their own babel.config.js; libraries have no babel
21
+ // config -- ember() handles type stripping.
22
+ "@babel/plugin-transform-typescript": "^7.28.5",
23
+ "@ember/app-tsconfig": "^2.0.0",
24
+ };
25
+
26
+ const libraryDeps = {
27
+ "@ember/library-tsconfig": "^2.0.0",
19
28
  };
20
29
 
30
+ /**
31
+ * @param {import('#utils/project.js').Project} project
32
+ */
33
+ function depsFor(project) {
34
+ return {
35
+ ...sharedDeps,
36
+ ...(project.type === "library" ? libraryDeps : appDeps),
37
+ };
38
+ }
39
+
21
40
  /**
22
41
  * @type {import('#types').Layer}
23
42
  */
24
43
  export default {
25
44
  label: "TypeScript",
26
45
 
46
+ /**
47
+ * Libraries publish declarations, so they are TypeScript unless the
48
+ * user opts out.
49
+ *
50
+ * @param {import('#types').ProjectType} projectType
51
+ */
52
+ defaultValue(projectType) {
53
+ return projectType === "library";
54
+ },
55
+
27
56
  async run(project) {
28
57
  /**
29
58
  * if jsconfig exists, switch to tsconfig
@@ -57,7 +86,9 @@ export default {
57
86
  reasons.push("tsconfig.json is missing");
58
87
  }
59
88
 
60
- if (!(await hasConfiguredTSBabel(project))) {
89
+ // Only projects with their own babel config need the TS plugin in it;
90
+ // without one (libraries), ember() strips types.
91
+ if (project.hasFile("babel.config.js") && !(await hasConfiguredTSBabel(project))) {
61
92
  if (!explain) return false;
62
93
 
63
94
  reasons.push(`babel.config is missing @babel/plugin-transform-typescript`);
@@ -71,7 +102,7 @@ export default {
71
102
  reasons.push(`package.json is missing the "lint:types" script`);
72
103
  }
73
104
 
74
- for (let dep of Object.keys(deps)) {
105
+ for (let dep of Object.keys(depsFor(project))) {
75
106
  if (!manifest.devDependencies?.[dep]) {
76
107
  if (!explain) return false;
77
108
 
@@ -101,7 +132,7 @@ async function updatePackageJson(project) {
101
132
  Object.assign(json.scripts, {
102
133
  "lint:types": "ember-tsc --noEmit",
103
134
  });
104
- Object.assign(json.devDependencies, await getLatest(deps));
135
+ Object.assign(json.devDependencies, await getLatest(depsFor(project)));
105
136
  }, project.directory);
106
137
  }
107
138
 
@@ -127,6 +158,11 @@ async function addTSConfig(project) {
127
158
  * @param {import('#utils/project.js').Project} project
128
159
  */
129
160
  async function updateBabelConfig(project) {
161
+ if (!project.hasFile("babel.config.js")) {
162
+ // No babel config to patch (libraries): ember() strips types.
163
+ return;
164
+ }
165
+
130
166
  if (await hasConfiguredTSBabel(project)) {
131
167
  return;
132
168
  }
@@ -1,18 +1,18 @@
1
+ import { ember } from "@nullvoxpopuli/ember-vite";
2
+ import { webdriverio } from "@vitest/browser-webdriverio";
1
3
  import { defineConfig } from "vitest/config";
2
- import { extensions, ember } from "@embroider/vite";
3
- import { babel } from "@rollup/plugin-babel";
4
4
 
5
5
  export default defineConfig({
6
- plugins: [
7
- ember(),
8
- babel({
9
- babelHelpers: "runtime",
10
- extensions: extensions.flatMap((extension) => [extension, ".gts", ".gjs"]),
11
- }),
12
- ],
6
+ plugins: [ember()],
13
7
  test: {
14
- environment: "happy-dom",
15
- setupFiles: ["./tests/test-helper.ts"],
16
8
  include: ["tests/**/*-test.{js,ts,gjs,gts}"],
9
+ maxConcurrency: 1,
10
+ passWithNoTests: true,
11
+ browser: {
12
+ provider: webdriverio(),
13
+ enabled: true,
14
+ headless: true,
15
+ instances: [{ browser: "chrome" }],
16
+ },
17
17
  },
18
18
  });
@@ -1,7 +1,44 @@
1
+ import { applyFolderTo } from "#utils/fs.js";
1
2
  import { getLatest } from "#utils/npm.js";
2
- import { packageJson, files } from "ember-apply";
3
+ import { packageJson } from "ember-apply";
3
4
  import { join } from "node:path";
4
5
 
6
+ const deps = {
7
+ "@ember/test-helpers": "^5.4.3",
8
+ "@vitest/browser": "^4.1.10",
9
+ "@vitest/browser-webdriverio": "^4.1.10",
10
+ "@vitest/ui": "^4.1.10",
11
+ "ember-vitest": "^0.4.0",
12
+ vitest: "^4.1.10",
13
+ webdriverio: "^9.29.1",
14
+ };
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).
19
+ const libraryDeps = {
20
+ "@babel/plugin-transform-typescript": "^7.28.10",
21
+ "@nullvoxpopuli/ember-vite": "workspace:*",
22
+ "babel-plugin-ember-template-compilation": "^4.0.0",
23
+ vite: "^8.0.14",
24
+ };
25
+
26
+ const scripts = {
27
+ test: "vitest run",
28
+ "test:watch": "vitest",
29
+ "test:ui": "vitest --ui",
30
+ };
31
+
32
+ /**
33
+ * @param {import('#utils/project.js').Project} project
34
+ */
35
+ function depsFor(project) {
36
+ return {
37
+ ...deps,
38
+ ...(project.type === "library" ? libraryDeps : {}),
39
+ };
40
+ }
41
+
5
42
  /**
6
43
  * @type {import('#types').Layer}
7
44
  */
@@ -9,34 +46,11 @@ export default {
9
46
  label: "Vitest",
10
47
 
11
48
  async run(project) {
12
- // Apply test files
13
- await files.applyFolder(join(import.meta.dirname, "files"), project.directory);
14
-
15
- // Add devDependencies
16
- await packageJson.addDevDependencies(
17
- await getLatest({
18
- "@ember/test-helpers": "^5.4.1",
19
- "@ember/test-waiters": "^4.1.1",
20
- "@testing-library/dom": "^10.4.1",
21
- "ember-vitest": "^0.3.3",
22
- vitest: "^4.0.0",
23
- "@vitest/ui": "^4.0.0",
24
- "@vitest/browser": "^4.0.0",
25
- "@vitest/browser-webdriverio": "^4.0.0",
26
- webdriverio: "^9.23.2",
27
- }),
28
- project.directory,
29
- );
30
-
31
- // Add scripts
32
- await packageJson.addScripts(
33
- {
34
- test: "vitest run",
35
- "test:watch": "vitest",
36
- "test:ui": "vitest --ui",
37
- },
38
- project.directory,
39
- );
49
+ await applyFolderTo(join(import.meta.dirname, "files"), project);
50
+
51
+ await packageJson.addDevDependencies(await getLatest(depsFor(project)), project.directory);
52
+
53
+ await packageJson.addScripts(scripts, project.directory);
40
54
  },
41
55
 
42
56
  /**
@@ -52,15 +66,39 @@ export default {
52
66
  * @returns {Promise<boolean>}
53
67
  */
54
68
  async isSetup(project, explain) {
55
- const reasons = ["Vitest setup detection not implemented"];
69
+ const reasons = [];
70
+
71
+ if (!project.hasFile("vitest.config.mjs")) {
72
+ if (!explain) return false;
73
+
74
+ reasons.push("vitest.config.mjs is missing");
75
+ }
76
+
77
+ let manifest = await packageJson.read(project.directory);
78
+
79
+ for (let script of Object.keys(scripts)) {
80
+ if (!manifest.scripts?.[script]) {
81
+ if (!explain) return false;
82
+
83
+ reasons.push(`package.json is missing the "${script}" script`);
84
+ }
85
+ }
86
+
87
+ for (let dep of Object.keys(depsFor(project))) {
88
+ if (!manifest.devDependencies?.[dep]) {
89
+ if (!explain) return false;
90
+
91
+ reasons.push(`package.json is missing ${dep} in devDependencies`);
92
+ }
93
+ }
56
94
 
57
95
  if (explain) {
58
96
  return {
59
- isSetup: false,
97
+ isSetup: reasons.length === 0,
60
98
  reasons,
61
99
  };
62
100
  }
63
101
 
64
- return false;
102
+ return reasons.length === 0;
65
103
  },
66
104
  };
@@ -11,11 +11,12 @@ export async function hasConfiguredTSBabel(project) {
11
11
  /**
12
12
  * @param {import('./project.js').Project} project
13
13
  * @param {string} pluginName
14
+ * @param {string} [configPath] which babel config to inspect
14
15
  */
15
- export async function hasConfiguredPlugin(project, pluginName) {
16
+ export async function hasConfiguredPlugin(project, pluginName, configPath = "babel.config.js") {
16
17
  let hasPlugin = false;
17
18
 
18
- await js.analyze(project.path("babel.config.js"), async ({ root, j }) => {
19
+ await js.analyze(project.path(configPath), async ({ root, j }) => {
19
20
  root
20
21
  .find(j.ArrayExpression, {
21
22
  elements: {
@@ -33,9 +34,10 @@ export async function hasConfiguredPlugin(project, pluginName) {
33
34
  /**
34
35
  * @param {import('./project.js').Project} project
35
36
  * @param {string} pluginName
37
+ * @param {string} [configPath] which babel config to modify
36
38
  */
37
- export async function removeConfiguredPlugin(project, pluginName) {
38
- await js.transform(project.path("babel.config.js"), async ({ root, j }) => {
39
+ export async function removeConfiguredPlugin(project, pluginName, configPath = "babel.config.js") {
40
+ await js.transform(project.path(configPath), async ({ root, j }) => {
39
41
  root
40
42
  .find(j.ArrayExpression, {
41
43
  elements: {
@@ -56,9 +58,10 @@ export async function removeConfiguredPlugin(project, pluginName) {
56
58
  /**
57
59
  * @param {import('./project.js').Project} project
58
60
  * @param {string} plugin
61
+ * @param {string} [configPath] which babel config to modify
59
62
  */
60
- export async function prependPlugin(project, plugin) {
61
- await js.transform(project.path("babel.config.js"), async ({ root, j }) => {
63
+ export async function prependPlugin(project, plugin, configPath = "babel.config.js") {
64
+ await js.transform(project.path(configPath), async ({ root, j }) => {
62
65
  root
63
66
  .find(j.Property, {
64
67
  key: { name: "plugins" },
package/src/utils/fs.js CHANGED
@@ -3,6 +3,14 @@ import { readFile, glob, mkdir, writeFile } from "node:fs/promises";
3
3
  import { join, dirname } from "node:path";
4
4
  import { parse as parsePath } from "node:path";
5
5
  import { removeTypes } from "./remove-types.js";
6
+ import { rewriteImportsToMatchFiles } from "./rewrite-imports.js";
7
+
8
+ /**
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/...`).
12
+ */
13
+ const EVERY_FILE = ["**/*", "**/.*", "**/.*/**/*"];
6
14
 
7
15
  /**
8
16
  *
@@ -15,7 +23,10 @@ import { removeTypes } from "./remove-types.js";
15
23
  * @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
16
24
  */
17
25
  export async function applyFolder(from, options) {
18
- for await (const entry of glob("**/*", { exclude: ["**/node_modules", "**/dist"], cwd: from })) {
26
+ for await (const entry of glob(EVERY_FILE, {
27
+ exclude: ["**/node_modules", "**/dist"],
28
+ cwd: from,
29
+ })) {
19
30
  let sourcePath = join(from, entry);
20
31
  let targetPath = join(options.to.directory, entry);
21
32
  let directory = dirname(targetPath);
@@ -42,6 +53,19 @@ export async function applyFolder(from, options) {
42
53
  * @param {import('#utils/project.js').Project} project
43
54
  */
44
55
  export async function applyFolderTo(from, project) {
56
+ /**
57
+ * Files we wrote, to be import-rewritten in a second pass.
58
+ *
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
+ *
65
+ * @type {string[]}
66
+ */
67
+ let written = [];
68
+
45
69
  await applyFolder(from, {
46
70
  to: project,
47
71
  async process({ entry, contents }) {
@@ -65,9 +89,9 @@ export async function applyFolderTo(from, project) {
65
89
  */
66
90
  return;
67
91
  }
68
- let newContents = await removeTypes(ext, contents);
69
92
 
70
- await writeFile(filePath, newContents);
93
+ await writeFile(filePath, await removeTypes(ext, contents));
94
+ written.push(filePath);
71
95
  return;
72
96
  }
73
97
  }
@@ -79,7 +103,24 @@ export async function applyFolderTo(from, project) {
79
103
  */
80
104
  return;
81
105
  }
106
+
82
107
  await writeFile(entry, contents);
108
+ written.push(entry);
83
109
  },
84
110
  });
111
+
112
+ /**
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.
117
+ */
118
+ for (let filePath of written) {
119
+ let contents = await readFile(filePath, "utf-8");
120
+ let rewritten = rewriteImportsToMatchFiles(contents, filePath);
121
+
122
+ if (rewritten !== contents) {
123
+ await writeFile(filePath, rewritten);
124
+ }
125
+ }
85
126
  }
@@ -0,0 +1,196 @@
1
+ import { toTree, print, statement } from "ember-estree";
2
+
3
+ export const INSPECTOR_PACKAGE = "@embroider/legacy-inspector-support";
4
+
5
+ /**
6
+ * The `ember-source-4.12` flavor covers ember-source 4.12 and newer,
7
+ * which includes the ember-source the app base generates with.
8
+ */
9
+ export const INSPECTOR_ENTRY = `${INSPECTOR_PACKAGE}/ember-source-4.12`;
10
+
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.
15
+ */
16
+ const APPLICATION_MODULES = new Set(["ember-strict-application-resolver", "@ember/application"]);
17
+
18
+ /** @typedef {import('ember-estree').ASTNode} ASTNode */
19
+
20
+ /**
21
+ * @param {unknown} value
22
+ * @returns {ASTNode | null}
23
+ */
24
+ function asNode(value) {
25
+ return value && typeof value === "object" ? /** @type {ASTNode} */ (value) : null;
26
+ }
27
+
28
+ /**
29
+ * @param {unknown} value
30
+ * @returns {string} an Identifier node's name ("" when it isn't one)
31
+ */
32
+ function nameOf(value) {
33
+ let node = asNode(value);
34
+
35
+ return node?.type === "Identifier" && typeof node.name === "string" ? node.name : "";
36
+ }
37
+
38
+ /**
39
+ * @param {ASTNode} importNode
40
+ * @returns {ASTNode[]}
41
+ */
42
+ function specifiersOf(importNode) {
43
+ return Array.isArray(importNode.specifiers)
44
+ ? /** @type {ASTNode[]} */ (importNode.specifiers)
45
+ : [];
46
+ }
47
+
48
+ /**
49
+ * @param {ASTNode} classNode
50
+ * @returns {ASTNode[]} the class's members
51
+ */
52
+ function classMembers(classNode) {
53
+ let members = asNode(classNode.body)?.body;
54
+
55
+ return Array.isArray(members) ? /** @type {ASTNode[]} */ (members) : [];
56
+ }
57
+
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.
63
+ *
64
+ * @param {string} code
65
+ * @param {string} filePath
66
+ */
67
+ function analyze(code, filePath) {
68
+ /** @type {ASTNode[]} */
69
+ let imports = [];
70
+ /** @type {ASTNode[]} */
71
+ let classes = [];
72
+
73
+ // (not templateOnly, so this is always a FileNode)
74
+ let tree = /** @type {import('ember-estree').FileNode} */ (
75
+ toTree(code, {
76
+ filePath,
77
+ visitors: {
78
+ ImportDeclaration: (node) => imports.push(node),
79
+ ClassDeclaration: (node) => classes.push(node),
80
+ ClassExpression: (node) => classes.push(node),
81
+ },
82
+ })
83
+ );
84
+
85
+ /** @type {Set<string>} */
86
+ let applicationLocals = new Set();
87
+ /** @type {ASTNode | null} */
88
+ let inspectorImport = null;
89
+
90
+ for (let node of imports) {
91
+ let source = asNode(node.source);
92
+ let from = source?.type === "Literal" && typeof source.value === "string" ? source.value : "";
93
+
94
+ if (APPLICATION_MODULES.has(from)) {
95
+ for (let specifier of specifiersOf(node)) {
96
+ let local = nameOf(specifier.local);
97
+
98
+ if (local) applicationLocals.add(local);
99
+ }
100
+ }
101
+
102
+ if (from === INSPECTOR_PACKAGE || from.startsWith(`${INSPECTOR_PACKAGE}/`)) {
103
+ inspectorImport ??= node;
104
+ }
105
+ }
106
+
107
+ let applicationClass =
108
+ classes.find((node) => applicationLocals.has(nameOf(node.superClass))) ?? null;
109
+
110
+ let inspectorMember = applicationClass
111
+ ? (classMembers(applicationClass).find(
112
+ (member) => !member.static && !member.computed && nameOf(member.key) === "inspector",
113
+ ) ?? null)
114
+ : null;
115
+
116
+ return { tree, imports, inspectorImport, applicationClass, inspectorMember };
117
+ }
118
+
119
+ /**
120
+ * Whether the module already wires inspector support: it imports
121
+ * {@link INSPECTOR_PACKAGE} and its Application class carries the
122
+ * `inspector` member.
123
+ *
124
+ * @param {string} code
125
+ * @param {string} filePath
126
+ * @returns {boolean}
127
+ */
128
+ export function hasInspectorSupport(code, filePath) {
129
+ let { inspectorImport, inspectorMember } = analyze(code, filePath);
130
+
131
+ return Boolean(inspectorImport && inspectorMember);
132
+ }
133
+
134
+ /**
135
+ * Wires ember-inspector support into the module containing the app's
136
+ * Application definition, the way the official app blueprint does:
137
+ *
138
+ * ```ts
139
+ * import setupInspector from "@embroider/legacy-inspector-support/ember-source-4.12";
140
+ *
141
+ * export default class App extends Application {
142
+ * inspector = setupInspector(this);
143
+ * }
144
+ * ```
145
+ *
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.
151
+ *
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.
156
+ *
157
+ * @param {string} code
158
+ * @param {string} filePath
159
+ * @returns {string}
160
+ */
161
+ export function wireInspectorSupport(code, filePath) {
162
+ let { tree, imports, inspectorImport, applicationClass, inspectorMember } = analyze(
163
+ code,
164
+ filePath,
165
+ );
166
+
167
+ if (!applicationClass) return code;
168
+ if (inspectorImport && inspectorMember) return code;
169
+
170
+ let localName = "setupInspector";
171
+
172
+ if (inspectorImport) {
173
+ let defaultSpecifier = specifiersOf(inspectorImport).find(
174
+ (specifier) => specifier.type === "ImportDefaultSpecifier",
175
+ );
176
+
177
+ localName = nameOf(defaultSpecifier?.local);
178
+
179
+ // an import that binds no default gives the member nothing to call
180
+ if (!localName) return code;
181
+ } else {
182
+ let body = /** @type {ASTNode[]} */ (tree.program.body);
183
+ let lastImport = imports.at(-1);
184
+ let insertAt = lastImport ? body.indexOf(lastImport) + 1 : 0;
185
+
186
+ body.splice(insertAt, 0, statement`import ${localName} from "${INSPECTOR_ENTRY}";`);
187
+ }
188
+
189
+ if (!inspectorMember) {
190
+ let [member] = classMembers(statement`class _ { inspector = ${localName}(this); }`);
191
+
192
+ if (member) classMembers(applicationClass).push(member);
193
+ }
194
+
195
+ return print(tree);
196
+ }
package/src/utils/npm.js CHANGED
@@ -7,6 +7,18 @@ import { join, resolve } from "node:path";
7
7
  */
8
8
  const CACHE = {};
9
9
 
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/`.
14
+ *
15
+ * @type {{ [name: string]: string }}
16
+ */
17
+ const LOCAL_PACKAGES = {
18
+ "@nullvoxpopuli/ember-vite": "vite",
19
+ "@nullvoxpopuli/ember-rolldown": "rolldown",
20
+ };
21
+
10
22
  /**
11
23
  * @param {{ [name: string]: string }} deps map of dep name to semver range
12
24
  */
@@ -25,18 +37,20 @@ export async function getLatest(deps) {
25
37
 
26
38
  /**
27
39
  * HACK FOR CI.
28
- * In practice, this package will be published separately, and that version will be used.
29
- * Only the local, not-yet-published @nullvoxpopuli/ember-vite package needs the link;
30
- * every other dependency must still resolve its real version.
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.
31
44
  *
32
45
  * We use `link:` (a symlink) rather than `file:` (a copy into the store)
33
- * on purpose: the package ships TypeScript source, and Node 24 refuses to
46
+ * on purpose: the packages ship TypeScript source, and Node 24 refuses to
34
47
  * strip types for files physically located under node_modules. A symlink
35
- * makes Node resolve the realpath to packages/vite (outside node_modules),
48
+ * makes Node resolve the realpath to packages/* (outside node_modules),
36
49
  * so its `.ts` runs directly.
37
50
  */
38
- if (needsLocalLink && dep === "@nullvoxpopuli/ember-vite") {
39
- version = "link:" + resolve(join(import.meta.dirname, "../../packages/vite"));
51
+ if (needsLocalLink && LOCAL_PACKAGES[dep]) {
52
+ version =
53
+ "link:" + resolve(join(import.meta.dirname, "../../packages", LOCAL_PACKAGES[dep]));
40
54
  } else {
41
55
  if (range == "workspace:*") {
42
56
  range = "latest";
@@ -9,6 +9,11 @@ import { substringBytes } from "./buffer.js";
9
9
  * @returns
10
10
  */
11
11
  export async function removeTypes(extension, code) {
12
+ // Nothing to transform (babel-remove-types throws on empty input)
13
+ if (code.trim() === "") {
14
+ return code;
15
+ }
16
+
12
17
  if (extension === ".gts") {
13
18
  return await wrappedRemoveTypes(code, babelRemoveTypes);
14
19
  }