ember.nvp 1.4.0 → 1.6.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 (31) hide show
  1. package/.release-plan.json +4 -22
  2. package/CHANGELOG.md +52 -0
  3. package/package.json +1 -1
  4. package/packages/rolldown/index.ts +2 -3
  5. package/packages/rolldown/package.json +12 -5
  6. package/packages/rolldown/src/app-reexports.ts +2 -2
  7. package/packages/rolldown/src/plugin-like.ts +23 -0
  8. package/packages/rolldown/src/published-types.test.ts +102 -0
  9. package/packages/rolldown/src/transform.test.ts +50 -2
  10. package/packages/rolldown/src/transform.ts +8 -1
  11. package/packages/rolldown/tsdown.config.ts +11 -0
  12. package/src/bases/minimal-app/app.test.ts +102 -1
  13. package/src/bases/minimal-app/files/app/app.ts +2 -1
  14. package/src/bases/minimal-app/files/babel.config.js +17 -1
  15. package/src/bases/minimal-app/files/package.json +2 -2
  16. package/src/bases/minimal-extension/files/app/app.ts +1 -1
  17. package/src/bases/minimal-extension/files/babel.config.js +17 -1
  18. package/src/bases/minimal-extension/files/package.json +2 -2
  19. package/src/bases/minimal-library/files/package.json +1 -1
  20. package/src/layers/eslint-bundled-ember/index.js +2 -2
  21. package/src/layers/eslint-bundled-nvp/index.js +2 -2
  22. package/src/layers/eslint-ejected/index.js +3 -3
  23. package/src/layers/qunit/index.js +0 -1
  24. package/src/layers/qunit/library-files/tests/test-helper.ts +1 -1
  25. package/src/layers/readme/index.js +16 -16
  26. package/src/utils/babel.js +20 -2
  27. package/test/ember.nvp/inspector-support.test.ts +13 -13
  28. package/test/ember.nvp/layer-inspector-support.test.ts +2 -2
  29. package/test/ember.nvp/layer-readme.test.ts +16 -16
  30. package/test/ember.nvp/layer-vitest.test.ts +1 -1
  31. package/packages/rolldown/rolldown.config.js +0 -23
@@ -2,17 +2,13 @@
2
2
  "solution": {
3
3
  "ember.nvp": {
4
4
  "impact": "minor",
5
- "oldVersion": "1.3.0",
6
- "newVersion": "1.4.0",
5
+ "oldVersion": "1.5.0",
6
+ "newVersion": "1.6.0",
7
7
  "tagName": "latest",
8
8
  "constraints": [
9
9
  {
10
10
  "impact": "minor",
11
11
  "reason": "Appears in changelog section :rocket: Enhancement"
12
- },
13
- {
14
- "impact": "patch",
15
- "reason": "Appears in changelog section :house: Internal"
16
12
  }
17
13
  ],
18
14
  "pkgJSONPath": "./package.json"
@@ -21,25 +17,11 @@
21
17
  "oldVersion": "1.1.0"
22
18
  },
23
19
  "@nullvoxpopuli/ember-rolldown": {
24
- "impact": "minor",
25
- "oldVersion": "2.5.0",
26
- "newVersion": "2.6.0",
27
- "tagName": "latest",
28
- "constraints": [
29
- {
30
- "impact": "minor",
31
- "reason": "Appears in changelog section :rocket: Enhancement"
32
- },
33
- {
34
- "impact": "patch",
35
- "reason": "Appears in changelog section :house: Internal"
36
- }
37
- ],
38
- "pkgJSONPath": "./packages/rolldown/package.json"
20
+ "oldVersion": "2.7.0"
39
21
  },
40
22
  "@nullvoxpopuli/ember-vite": {
41
23
  "oldVersion": "1.1.0"
42
24
  }
43
25
  },
44
- "description": "## Release (2026-08-03)\n\n* ember.nvp 1.4.0 (minor)\n* @nullvoxpopuli/ember-rolldown 2.6.0 (minor)\n\n#### :rocket: Enhancement\n* `@nullvoxpopuli/ember-rolldown`, `ember.nvp`\n * [#111](https://github.com/NullVoxPopuli/ember.nvp/pull/111) rolldown: find publish configs in config/ too ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n* `@nullvoxpopuli/ember-rolldown`\n * [#109](https://github.com/NullVoxPopuli/ember.nvp/pull/109) rolldown: prefer babel.publish.config over babel.config ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n\n#### :house: Internal\n* `@nullvoxpopuli/ember-rolldown`, `ember.nvp`\n * [#112](https://github.com/NullVoxPopuli/ember.nvp/pull/112) rolldown: test the isolated-declarations guard through generated libraries ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n\n#### Committers: 1\n- [@NullVoxPopuli](https://github.com/NullVoxPopuli)\n"
26
+ "description": "## Release (2026-09-10)\n\n* ember.nvp 1.6.0 (minor)\n\n#### :rocket: Enhancement\n* `ember.nvp`\n * [#129](https://github.com/NullVoxPopuli/ember.nvp/pull/129) Add routes import ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n * [#127](https://github.com/NullVoxPopuli/ember.nvp/pull/127) Update eslint (+ deps) in layers ([@tcjr](https://github.com/tcjr))\n\n#### Committers: 2\n- Tom Carter ([@tcjr](https://github.com/tcjr))\n- [@NullVoxPopuli](https://github.com/NullVoxPopuli)\n"
45
27
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,57 @@
1
1
  # Changelog
2
2
 
3
+ ## Release (2026-09-10)
4
+
5
+ * ember.nvp 1.6.0 (minor)
6
+
7
+ #### :rocket: Enhancement
8
+ * `ember.nvp`
9
+ * [#129](https://github.com/NullVoxPopuli/ember.nvp/pull/129) Add routes import ([@NullVoxPopuli](https://github.com/NullVoxPopuli))
10
+ * [#127](https://github.com/NullVoxPopuli/ember.nvp/pull/127) Update eslint (+ deps) in layers ([@tcjr](https://github.com/tcjr))
11
+
12
+ #### Committers: 2
13
+ - Tom Carter ([@tcjr](https://github.com/tcjr))
14
+ - [@NullVoxPopuli](https://github.com/NullVoxPopuli)
15
+
16
+ ## Release (2026-08-18)
17
+
18
+ * ember.nvp 1.5.0 (minor)
19
+
20
+ #### :rocket: Enhancement
21
+ * `ember.nvp`
22
+ * [#125](https://github.com/NullVoxPopuli/ember.nvp/pull/125) Update to 7.2, drop ember-strict-application-resolver polyfill ([@NullVoxPopuli](https://github.com/NullVoxPopuli))
23
+
24
+ #### :bug: Bug Fix
25
+ * `ember.nvp`
26
+ * [#121](https://github.com/NullVoxPopuli/ember.nvp/pull/121) Fix initial README formatting ([@tcjr](https://github.com/tcjr))
27
+ * [#122](https://github.com/NullVoxPopuli/ember.nvp/pull/122) Strip debug code from production app builds ([@NullVoxPopuli](https://github.com/NullVoxPopuli))
28
+
29
+ #### Committers: 2
30
+ - Tom Carter ([@tcjr](https://github.com/tcjr))
31
+ - [@NullVoxPopuli](https://github.com/NullVoxPopuli)
32
+
33
+ ## Release (2026-08-07)
34
+
35
+ * @nullvoxpopuli/ember-rolldown 2.7.0 (minor)
36
+
37
+ #### :rocket: Enhancement
38
+ * `@nullvoxpopuli/ember-rolldown`
39
+ * [#118](https://github.com/NullVoxPopuli/ember.nvp/pull/118) rolldown: ship type declarations ([@NullVoxPopuli](https://github.com/NullVoxPopuli))
40
+
41
+ #### Committers: 1
42
+ - [@NullVoxPopuli](https://github.com/NullVoxPopuli)
43
+
44
+ ## Release (2026-08-05)
45
+
46
+ * @nullvoxpopuli/ember-rolldown 2.6.1 (patch)
47
+
48
+ #### :bug: Bug Fix
49
+ * `@nullvoxpopuli/ember-rolldown`
50
+ * [#115](https://github.com/NullVoxPopuli/ember.nvp/pull/115) rolldown: don't resolve absolute specifiers against the importer's directory ([@NullVoxPopuli](https://github.com/NullVoxPopuli))
51
+
52
+ #### Committers: 1
53
+ - [@NullVoxPopuli](https://github.com/NullVoxPopuli)
54
+
3
55
  ## Release (2026-08-03)
4
56
 
5
57
  * ember.nvp 1.4.0 (minor)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ember.nvp",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "ember project generator, alternative to the official blueprints, but more modern -- hopefully one day to upstream back in to ember-cli / official blueprints",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,10 +1,9 @@
1
- import type { Plugin } from "rolldown";
2
-
3
1
  import { emberBabel, type BabelOptions } from "./src/babel.ts";
4
2
  import { emberConfig } from "./src/config.ts";
5
3
  import { emberExternals } from "./src/externals.ts";
6
4
  import { emberIsolatedDeclarations } from "./src/isolated-declarations.ts";
7
5
  import { emberTransform } from "./src/transform.ts";
6
+ import type { RolldownPluginLike } from "./src/plugin-like.ts";
8
7
 
9
8
  interface Config {
10
9
  /**
@@ -49,7 +48,7 @@ interface Config {
49
48
  * });
50
49
  * ```
51
50
  */
52
- export function ember(config: Config = {}): Plugin[] {
51
+ export function ember(config: Config = {}): RolldownPluginLike[] {
53
52
  return [
54
53
  emberConfig(),
55
54
  emberIsolatedDeclarations(),
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nullvoxpopuli/ember-rolldown",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "Meta plugin for building ember v2 libraries/addons with rolldown & tsdown.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -11,9 +11,16 @@
11
11
  "publishConfig": {
12
12
  "main": "dist/index.js",
13
13
  "exports": {
14
- ".": "./dist/index.js",
15
- "./app-reexports": "./dist/app-reexports.js"
16
- }
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "./app-reexports": {
19
+ "types": "./dist/app-reexports.d.ts",
20
+ "default": "./dist/app-reexports.js"
21
+ }
22
+ },
23
+ "types": "dist/index.d.ts"
17
24
  },
18
25
  "keywords": [
19
26
  "rolldown",
@@ -31,7 +38,7 @@
31
38
  "directory": "packages/rolldown"
32
39
  },
33
40
  "scripts": {
34
- "build": "rolldown --config",
41
+ "build": "tsdown",
35
42
  "prepack": "pnpm build"
36
43
  },
37
44
  "packageManager": "pnpm@10.28.0",
@@ -1,7 +1,7 @@
1
1
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, extname, join, matchesGlob, relative, resolve, sep } from "node:path";
3
3
 
4
- import type { Plugin } from "rolldown";
4
+ import type { RolldownPluginLike } from "./plugin-like.ts";
5
5
 
6
6
  export interface AppReexportsOptions {
7
7
  /**
@@ -79,7 +79,7 @@ export interface AppReexportsOptions {
79
79
  export function appReexports(
80
80
  includeOrOptions: string | string[] | AppReexportsOptions = {},
81
81
  options: Omit<AppReexportsOptions, "include"> = {},
82
- ): Plugin {
82
+ ): RolldownPluginLike {
83
83
  const resolved =
84
84
  typeof includeOrOptions === "string"
85
85
  ? { ...options, include: [includeOrOptions] }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * What this package hands back: an object that rolldown and tsdown accept in
3
+ * `plugins`.
4
+ *
5
+ * Structurally this *is* rolldown's `Plugin` -- `name` is its only required
6
+ * member, every hook is optional -- so the two are assignable in both
7
+ * directions: a consumer who imports rolldown's own type can still write
8
+ * `const plugins: Plugin[] = ember()`, and tsdown's `plugins` option lists
9
+ * `{ name: string }` among the shapes it takes.
10
+ *
11
+ * Declaring it here rather than re-exporting rolldown's type is what keeps
12
+ * `rolldown` out of the published declarations. Otherwise it has to become a
13
+ * runtime `dependency` purely so consumers can resolve a type: leave it a
14
+ * devDependency and the declaration bundler inlines a ~200kB private copy of
15
+ * rolldown's types instead, nominally distinct from the consumer's own.
16
+ *
17
+ * The precise hook signatures still apply inside this package -- each plugin
18
+ * function is typed with rolldown's real `Plugin` (a devDependency), and this
19
+ * type only widens what crosses the package boundary.
20
+ */
21
+ export interface RolldownPluginLike {
22
+ name: string;
23
+ }
@@ -0,0 +1,102 @@
1
+ import { execa } from "execa";
2
+ import { readdir, readFile, rm, stat } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { beforeAll, describe, expect, it } from "vitest";
6
+
7
+ /**
8
+ * This package's own published shape: consumers write their build config in
9
+ * TypeScript, so `import { ember } from "@nullvoxpopuli/ember-rolldown"` has to
10
+ * carry types. Without them it is an implicit `any` (TS7016), and a config file
11
+ * silently stops being type-checked.
12
+ */
13
+ const packageDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
14
+
15
+ interface Manifest {
16
+ dependencies: Record<string, string>;
17
+ peerDependencies: Record<string, string>;
18
+ publishConfig: { exports: Record<string, Record<string, string>> };
19
+ }
20
+
21
+ async function readManifest(): Promise<Manifest> {
22
+ return JSON.parse(await readFile(path.join(packageDir, "package.json"), "utf8")) as Manifest;
23
+ }
24
+
25
+ async function exists(relative: string) {
26
+ return Boolean(await stat(path.join(packageDir, relative)).catch(() => null));
27
+ }
28
+
29
+ async function declarations() {
30
+ const files = (await readdir(path.join(packageDir, "dist"))).filter((file) =>
31
+ file.endsWith(".d.ts"),
32
+ );
33
+
34
+ return Object.fromEntries(
35
+ await Promise.all(
36
+ files.map(
37
+ async (file) =>
38
+ [file, await readFile(path.join(packageDir, "dist", file), "utf8")] as const,
39
+ ),
40
+ ),
41
+ );
42
+ }
43
+
44
+ describe("published types", () => {
45
+ beforeAll(async () => {
46
+ // `clean` is on, but don't let these assertions depend on that: stale
47
+ // artifacts from an earlier build satisfy every one of them on their own
48
+ await rm(path.join(packageDir, "dist"), { recursive: true, force: true });
49
+ await execa("pnpm", ["build"], { cwd: packageDir });
50
+ }, 120_000);
51
+
52
+ it("every publishConfig export declares types, and they exist", async () => {
53
+ const manifest = await readManifest();
54
+ const entries = Object.entries(manifest.publishConfig.exports);
55
+
56
+ expect(entries.length).toBeGreaterThan(0);
57
+
58
+ for (const [subpath, conditions] of entries) {
59
+ expect(conditions, `${subpath} declares types`).toHaveProperty("types");
60
+ expect(await exists(conditions["types"]!), `${subpath} -> ${conditions["types"]}`).toBe(true);
61
+ expect(await exists(conditions["default"]!), `${subpath} -> ${conditions["default"]}`).toBe(
62
+ true,
63
+ );
64
+ }
65
+ });
66
+
67
+ /**
68
+ * A declaration may only reach for packages a consumer is guaranteed to have.
69
+ * Reference a devDependency and there are two ways to lose, both quiet:
70
+ * the specifier survives and consumers can't resolve it (`skipLibCheck` hides
71
+ * that, degrading the type to an error type), or the declaration bundler
72
+ * inlines the whole type surface — a ~200kB copy of rolldown's types, in the
73
+ * case that prompted this test, nominally distinct from the consumer's own.
74
+ *
75
+ * `ember()` returns plugins, so `RolldownPluginLike` exists to keep rolldown
76
+ * on the devDependency side of that line.
77
+ */
78
+ it("declarations only reference packages consumers have", async () => {
79
+ const manifest = await readManifest();
80
+ const available = new Set([
81
+ ...Object.keys(manifest.dependencies),
82
+ ...Object.keys(manifest.peerDependencies),
83
+ ]);
84
+
85
+ for (const [file, contents] of Object.entries(await declarations())) {
86
+ const specifiers = [...contents.matchAll(/^import .* from "([^"]+)";$/gm)]
87
+ .map(([, specifier]) => specifier!)
88
+ .filter((specifier) => !specifier.startsWith("."));
89
+
90
+ for (const specifier of specifiers) {
91
+ const owner = specifier.startsWith("@")
92
+ ? specifier.split("/").slice(0, 2).join("/")
93
+ : specifier.split("/")[0]!;
94
+
95
+ expect(available, `${file} imports ${specifier}`).toContain(owner);
96
+ }
97
+
98
+ // an inlined copy of a bundler's types, rather than an import of them
99
+ expect(contents, `${file} inlines rolldown's types`).not.toMatch(/interface PluginContext/);
100
+ }
101
+ });
102
+ });
@@ -2,6 +2,7 @@ import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import path from "node:path";
4
4
  import { rolldown } from "rolldown";
5
+ import type { Plugin } from "rolldown";
5
6
  import { describe, expect, it } from "vitest";
6
7
 
7
8
  import { emberTransform } from "./transform.ts";
@@ -15,7 +16,10 @@ import { emberTransform } from "./transform.ts";
15
16
  */
16
17
  async function bundle(
17
18
  files: Record<string, string>,
18
- { input = ["index.ts"] }: { input?: string[] } = {},
19
+ {
20
+ input = ["index.ts"],
21
+ plugins = () => [],
22
+ }: { input?: string[]; plugins?: (dir: string) => Plugin[] } = {},
19
23
  ): Promise<string> {
20
24
  const dir = await mkdtemp(path.join(tmpdir(), "ember-rolldown-build-"));
21
25
 
@@ -29,7 +33,7 @@ async function bundle(
29
33
 
30
34
  const build = await rolldown({
31
35
  input: input.map((relative) => path.join(dir, relative)),
32
- plugins: [emberTransform()],
36
+ plugins: [emberTransform(), ...plugins(dir)],
33
37
  external: (id) => !isLocal(id),
34
38
  onwarn() {},
35
39
  });
@@ -174,6 +178,50 @@ describe("emberTransform (full plugin via rolldown)", () => {
174
178
  `);
175
179
  });
176
180
 
181
+ it("resolves an absolute .gts specifier imported from a plugin's virtual module", async () => {
182
+ /**
183
+ * The shape a code-generating plugin emits: a virtual module (id prefixed
184
+ * with `\0`, so it exists nowhere on disk) whose generated source imports
185
+ * real files by absolute path, because it has no directory to be relative
186
+ * to. `path.dirname` of such an id is `"\0."`, so the specifier must not be
187
+ * resolved against the importer's directory.
188
+ */
189
+ const registry = (dir: string): Plugin[] => {
190
+ const source = "./registry?virtual=views";
191
+ const virtualId = `\0${source}`;
192
+
193
+ return [
194
+ {
195
+ name: "test:virtual-registry",
196
+ resolveId(id) {
197
+ return id === source ? virtualId : null;
198
+ },
199
+ load(id) {
200
+ if (id !== virtualId) return null;
201
+
202
+ return [
203
+ `import example from '${path.join(dir, "components/view/example.gts")}';`,
204
+ `export const views = { example };`,
205
+ ].join("\n");
206
+ },
207
+ },
208
+ ];
209
+ };
210
+
211
+ const code = await bundle(
212
+ {
213
+ "index.ts": `export { views } from './registry?virtual=views';`,
214
+ "components/view/example.gts": `<template>example</template>`,
215
+ },
216
+ { plugins: registry },
217
+ );
218
+
219
+ // The absolutely-specified .gts got compiled and bundled in.
220
+ expect(code).not.toContain("<template>");
221
+ expect(code).toContain("template(");
222
+ expect(code).toContain("example");
223
+ });
224
+
177
225
  it("compiles a .gts module used directly as an entry (no importer)", async () => {
178
226
  const code = await bundle(
179
227
  {
@@ -93,7 +93,14 @@ export function emberTransform(): Plugin {
93
93
  };
94
94
  }
95
95
 
96
- const fileName = path.join(path.dirname(importer), id);
96
+ // An absolute specifier already names the file, so it needs no
97
+ // importer-relative resolution — and joining it onto the importer's
98
+ // directory would corrupt it. That is not hypothetical: a plugin's
99
+ // virtual module (id prefixed with `\0`) can generate imports of
100
+ // on-disk files by absolute path, and `path.dirname("\0./registry")`
101
+ // is `"\0."`, so joining yields `"\0./Users/.../thing.gts"` — a path
102
+ // no `existsSync`/`readFile` can accept (it has a null byte in it).
103
+ const fileName = path.isAbsolute(id) ? id : path.join(path.dirname(importer), id);
97
104
 
98
105
  if (id.endsWith(".gts")) {
99
106
  return {
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from "tsdown";
2
+
3
+ import { emberConfig } from "./src/config.ts";
4
+
5
+ export default defineConfig({
6
+ entry: {
7
+ index: "index.ts",
8
+ "app-reexports": "src/app-reexports.ts",
9
+ },
10
+ plugins: [emberConfig()],
11
+ });
@@ -3,8 +3,9 @@ import { generate, build, reapply } from "#test-helpers";
3
3
  import { packageJson } from "ember-apply";
4
4
 
5
5
  import type { Project } from "ember.nvp";
6
- import { rm } from "node:fs/promises";
6
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
7
7
  import { globSync } from "node:fs";
8
+ import { join } from "node:path";
8
9
 
9
10
  const expect = hardExpect.soft;
10
11
 
@@ -100,6 +101,106 @@ describe("typescript", () => {
100
101
  });
101
102
  });
102
103
 
104
+ const DEBUG_MARKERS = {
105
+ assert: "__debug_fixture_assert__",
106
+ deprecate: "__debug_fixture_deprecate__",
107
+ warn: "__debug_fixture_warn__",
108
+ isDevelopingApp: "__debug_fixture_is_developing_app__",
109
+ };
110
+
111
+ /**
112
+ * Lives in app/services/ so that app.ts's eager
113
+ * `import.meta.glob("./services/**\/*")` pulls it into the build
114
+ * without needing to modify any generated files.
115
+ */
116
+ const debugFixture = `
117
+ import { assert, deprecate, warn } from "@ember/debug";
118
+ import { isDevelopingApp, macroCondition } from "@embroider/macros";
119
+
120
+ assert("${DEBUG_MARKERS.assert}", true);
121
+
122
+ deprecate("${DEBUG_MARKERS.deprecate}", true, {
123
+ id: "debug-fixture",
124
+ until: "999.0.0",
125
+ for: "debug-fixture",
126
+ since: { available: "0.0.0", enabled: "0.0.0" },
127
+ });
128
+
129
+ warn("${DEBUG_MARKERS.warn}", true, { id: "debug-fixture" });
130
+
131
+ if (macroCondition(isDevelopingApp())) {
132
+ console.log("${DEBUG_MARKERS.isDevelopingApp}");
133
+ }
134
+ `;
135
+
136
+ /**
137
+ * Only .js files -- the sourcemaps (.map) contain the original
138
+ * source (with markers) even in production builds.
139
+ */
140
+ async function builtJavaScript(project: Project): Promise<string> {
141
+ let files = globSync("dist/**/*.js", { cwd: project.directory });
142
+
143
+ hardExpect(files.length, "build produced JS files").toBeGreaterThan(0);
144
+
145
+ let contents = await Promise.all(
146
+ files.map((file) => readFile(join(project.directory, file), "utf-8")),
147
+ );
148
+
149
+ return contents.join("\n");
150
+ }
151
+
152
+ describe("debug macros", () => {
153
+ let project: Project;
154
+
155
+ beforeAll(async () => {
156
+ project = await generate({
157
+ type: "app",
158
+ layers: ["typescript"],
159
+ });
160
+
161
+ await mkdir(join(project.directory, "app/services"), { recursive: true });
162
+ await writeFile(join(project.directory, "app/services/debug-fixture.ts"), debugFixture);
163
+
164
+ let { exitCode } = await project.install();
165
+
166
+ hardExpect(exitCode, "Install succeeds").toBe(0);
167
+ });
168
+
169
+ afterAll(async () => {
170
+ await rm(project.directory, { recursive: true, force: true });
171
+ });
172
+
173
+ it("has the debug-macros babel plugin", async () => {
174
+ let content = await project.read("babel.config.js");
175
+
176
+ expect(content).toContain("babel-plugin-debug-macros");
177
+ });
178
+
179
+ it("development build keeps debug code", async () => {
180
+ let { exitCode } = await build(project);
181
+
182
+ hardExpect(exitCode).toBe(0);
183
+
184
+ let output = await builtJavaScript(project);
185
+
186
+ for (let [name, marker] of Object.entries(DEBUG_MARKERS)) {
187
+ expect(output, `development build contains ${name}`).toContain(marker);
188
+ }
189
+ });
190
+
191
+ it("production build strips assert(), deprecate(), warn(), and if (macroCondition(isDevelopingApp()))", async () => {
192
+ let { exitCode } = await build(project, "production");
193
+
194
+ hardExpect(exitCode).toBe(0);
195
+
196
+ let output = await builtJavaScript(project);
197
+
198
+ for (let [name, marker] of Object.entries(DEBUG_MARKERS)) {
199
+ expect(output, `production build strips ${name}`).not.toContain(marker);
200
+ }
201
+ });
202
+ });
203
+
103
204
  describe("javascript", () => {
104
205
  let project: Project;
105
206
  beforeAll(async () => {
@@ -10,11 +10,12 @@
10
10
  * - https://ember-primitives.pages.dev/6-utils/createService.md
11
11
  * - https://ember-primitives.pages.dev/6-utils/createAsyncService.md
12
12
  */
13
- import Application from "ember-strict-application-resolver";
13
+ import Application from "@ember/application";
14
14
 
15
15
  export default class App extends Application {
16
16
  modules = {
17
17
  ...import.meta.glob("./router.*", { eager: true }),
18
+ ...import.meta.glob("./routes/**/*", { eager: true }),
18
19
  ...import.meta.glob("./templates/**/*", { eager: true }),
19
20
  ...import.meta.glob("./services/**/*", { eager: true }),
20
21
  };
@@ -1,5 +1,7 @@
1
1
  import { buildMacros } from "@embroider/macros/babel";
2
2
 
3
+ const isProduction = process.env.NODE_ENV === "production";
4
+
3
5
  const macros = buildMacros({
4
6
  configure(config) {
5
7
  if (process.env.EMBER_ENV === "test") {
@@ -40,8 +42,22 @@ export default {
40
42
  regenerator: false,
41
43
  },
42
44
  ],
45
+ isProduction && [
46
+ "babel-plugin-debug-macros",
47
+ {
48
+ debugTools: {
49
+ isDebug: false,
50
+ source: "@ember/debug",
51
+ assertPredicateIndex: 1,
52
+ },
53
+ externalizeHelpers: {
54
+ module: "@ember/debug",
55
+ },
56
+ },
57
+ "@ember/debug stripping",
58
+ ],
43
59
  ...macros.babelMacros,
44
- ],
60
+ ].filter(Boolean),
45
61
 
46
62
  generatorOpts: {
47
63
  compact: false,
@@ -29,8 +29,7 @@
29
29
  "@embroider/router": "^3.0.6",
30
30
  "@glimmer/component": "^2.1.1",
31
31
  "decorator-transforms": "^2.3.2",
32
- "ember-source": "~7.0.0",
33
- "ember-strict-application-resolver": "^0.1.0"
32
+ "ember-source": "~7.2.0"
34
33
  },
35
34
  "devDependencies": {
36
35
  "@babel/core": "^7.29.7",
@@ -44,6 +43,7 @@
44
43
  "@glint/tsserver-plugin": "^2.5.5",
45
44
  "@nullvoxpopuli/ember-vite": "workspace:*",
46
45
  "@rollup/plugin-babel": "^7.1.0",
46
+ "babel-plugin-debug-macros": "^2.0.0",
47
47
  "babel-plugin-ember-template-compilation": "^4.0.0",
48
48
  "typescript": "^6.0.3",
49
49
  "vite": "^8.0.14"
@@ -10,7 +10,7 @@
10
10
  * - https://ember-primitives.pages.dev/6-utils/createService.md
11
11
  * - https://ember-primitives.pages.dev/6-utils/createAsyncService.md
12
12
  */
13
- import Application from "ember-strict-application-resolver";
13
+ import Application from "@ember/application";
14
14
 
15
15
  export default class App extends Application {
16
16
  modules = {
@@ -1,5 +1,7 @@
1
1
  import { buildMacros } from "@embroider/macros/babel";
2
2
 
3
+ const isProduction = process.env.NODE_ENV === "production";
4
+
3
5
  const macros = buildMacros({
4
6
  configure(config) {
5
7
  if (process.env.EMBER_ENV === "test") {
@@ -40,8 +42,22 @@ export default {
40
42
  regenerator: false,
41
43
  },
42
44
  ],
45
+ isProduction && [
46
+ "babel-plugin-debug-macros",
47
+ {
48
+ debugTools: {
49
+ isDebug: false,
50
+ source: "@ember/debug",
51
+ assertPredicateIndex: 1,
52
+ },
53
+ externalizeHelpers: {
54
+ module: "@ember/debug",
55
+ },
56
+ },
57
+ "@ember/debug stripping",
58
+ ],
43
59
  ...macros.babelMacros,
44
- ],
60
+ ].filter(Boolean),
45
61
 
46
62
  generatorOpts: {
47
63
  compact: false,
@@ -30,8 +30,7 @@
30
30
  "@embroider/router": "^3.0.6",
31
31
  "@glimmer/component": "^2.1.1",
32
32
  "decorator-transforms": "^2.3.2",
33
- "ember-source": "~7.0.0",
34
- "ember-strict-application-resolver": "^0.1.0"
33
+ "ember-source": "~7.2.0"
35
34
  },
36
35
  "devDependencies": {
37
36
  "@babel/core": "^7.29.7",
@@ -45,6 +44,7 @@
45
44
  "@glint/tsserver-plugin": "^2.5.5",
46
45
  "@nullvoxpopuli/ember-vite": "workspace:*",
47
46
  "@rollup/plugin-babel": "^7.1.0",
47
+ "babel-plugin-debug-macros": "^2.0.0",
48
48
  "babel-plugin-ember-template-compilation": "^4.0.0",
49
49
  "typescript": "^6.0.3",
50
50
  "vite": "^8.0.14"
@@ -36,7 +36,7 @@
36
36
  "@ember/library-tsconfig": "^2.0.0",
37
37
  "@glimmer/component": "^2.1.1",
38
38
  "@nullvoxpopuli/ember-rolldown": "workspace:*",
39
- "ember-source": "~7.0.0",
39
+ "ember-source": "~7.2.0",
40
40
  "tsdown": "^0.22.7",
41
41
  "typescript": "^6.0.3"
42
42
  },
@@ -18,8 +18,8 @@ export default {
18
18
 
19
19
  await packageJson.addDevDependencies(
20
20
  await getLatest({
21
- "ember-eslint": "^0.6.1",
22
- eslint: "^9.39.2",
21
+ "ember-eslint": "^1.0.0",
22
+ eslint: "^10.9.1",
23
23
  }),
24
24
  project.directory,
25
25
  );
@@ -18,8 +18,8 @@ export default {
18
18
 
19
19
  await packageJson.addDevDependencies(
20
20
  await getLatest({
21
- "@nullvoxpopuli/eslint-configs": "^5.5.0",
22
- eslint: "^9.39.2",
21
+ "@nullvoxpopuli/eslint-configs": "^7.0.0",
22
+ eslint: "^10.9.1",
23
23
  }),
24
24
  project.directory,
25
25
  );
@@ -7,14 +7,14 @@ import { existsSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
 
9
9
  const toInstall = {
10
- "@babel/eslint-parser": "^7.28.6",
11
- "@eslint/js": "^9.39.2",
10
+ "@babel/eslint-parser": "^8.0.1",
11
+ "@eslint/js": "^10.0.1",
12
12
  "eslint-config-prettier": "^10.1.8",
13
13
  "eslint-plugin-ember": "^12.7.5",
14
14
  "eslint-plugin-n": "^17.23.2",
15
15
  "eslint-plugin-qunit": "^8.2.5",
16
16
  "typescript-eslint": "^8.53.0",
17
- eslint: "^9.39.2",
17
+ eslint: "^10.9.1",
18
18
  };
19
19
 
20
20
  /**
@@ -28,7 +28,6 @@ const libraryDeps = {
28
28
  "@embroider/macros": "^1.20.3",
29
29
  "@nullvoxpopuli/ember-vite": "workspace:*",
30
30
  "babel-plugin-ember-template-compilation": "^4.0.0",
31
- "ember-strict-application-resolver": "^0.1.0",
32
31
  vite: "^8.0.14",
33
32
  };
34
33
  const libraryTsDeps = {
@@ -4,7 +4,7 @@ import * as QUnit from "qunit";
4
4
  import { setup } from "qunit-dom";
5
5
  import { setupEmberOnerrorValidation, start as qunitStart } from "ember-qunit";
6
6
  import { setTesting } from "@embroider/macros";
7
- import EmberApp from "ember-strict-application-resolver";
7
+ import EmberApp from "@ember/application";
8
8
 
9
9
  class Router extends EmberRouter {
10
10
  location = "none";
@@ -21,23 +21,23 @@ An Ember application created with \`ember.nvp\`.
21
21
 
22
22
  ### Installation
23
23
 
24
- ~~~sh
24
+ \`\`\`sh
25
25
  ${project.packageManager} install
26
- ~~~
26
+ \`\`\`
27
27
 
28
28
  ### Development
29
29
 
30
30
  To start the local development server:
31
31
 
32
- ~~~sh
32
+ \`\`\`sh
33
33
  ${project.runPrefix} dev
34
- ~~~
34
+ \`\`\`
35
35
 
36
36
  or
37
37
 
38
- ~~~sh
38
+ \`\`\`sh
39
39
  ${project.runPrefix} start
40
- ~~~
40
+ \`\`\`
41
41
  ${layerDocsMarkdown}`;
42
42
  }
43
43
 
@@ -61,29 +61,29 @@ A browser extension using Ember created with \`ember.nvp\`.
61
61
 
62
62
  To start development:
63
63
 
64
- ~~~sh
64
+ \`\`\`sh
65
65
  ${project.runPrefix} dev
66
- ~~~
66
+ \`\`\`
67
67
 
68
68
  or
69
69
 
70
- ~~~sh
70
+ \`\`\`sh
71
71
  ${project.runPrefix} start
72
- ~~~
72
+ \`\`\`
73
73
 
74
74
  ### Building
75
75
 
76
76
  To build the library:
77
77
 
78
- ~~~sh
78
+ \`\`\`sh
79
79
  ${project.runPrefix} build
80
- ~~~
80
+ \`\`\`
81
81
 
82
82
  or
83
83
 
84
- ~~~sh
84
+ \`\`\`sh
85
85
  ${project.runPrefix} build:watch
86
- ~~~
86
+ \`\`\`
87
87
  ${layerDocsMarkdown}`;
88
88
  }
89
89
 
@@ -107,9 +107,9 @@ An Ember library/addon created with \`ember.nvp\`.
107
107
 
108
108
  To build the library:
109
109
 
110
- ~~~sh
110
+ \`\`\`sh
111
111
  ${project.runPrefix} build
112
- ~~~
112
+ \`\`\`
113
113
  ${layerDocsMarkdown}`;
114
114
  }
115
115
 
@@ -65,15 +65,33 @@ export async function prependPlugin(project, plugin, configPath = "babel.config.
65
65
  root
66
66
  .find(j.Property, {
67
67
  key: { name: "plugins" },
68
- value: { type: "ArrayExpression" },
69
68
  })
70
69
  .forEach(
71
70
  /**
72
71
  * @param {any} path
73
72
  */
74
73
  (path) => {
75
- path.node.value.elements.unshift(plugin);
74
+ let array = pluginsArrayOf(path.node.value);
75
+
76
+ array?.elements.unshift(plugin);
76
77
  },
77
78
  );
78
79
  });
79
80
  }
81
+
82
+ /**
83
+ * The plugins value may be the array itself, or the array with
84
+ * methods chained off of it: `[ ... ].filter(Boolean)`
85
+ *
86
+ * @param {any} node
87
+ * @returns {any | undefined} the underlying ArrayExpression
88
+ */
89
+ function pluginsArrayOf(node) {
90
+ if (node.type === "ArrayExpression") return node;
91
+
92
+ if (node.type === "CallExpression" && node.callee.type === "MemberExpression") {
93
+ return pluginsArrayOf(node.callee.object);
94
+ }
95
+
96
+ return undefined;
97
+ }
@@ -10,7 +10,7 @@ import { hasInspectorSupport, wireInspectorSupport } from "#utils/inspector-supp
10
10
  describe("wireInspectorSupport", () => {
11
11
  it("wires the generated shape (export default class extends Application)", () => {
12
12
  const code = [
13
- `import Application from "ember-strict-application-resolver";`,
13
+ `import Application from "@ember/application";`,
14
14
  ``,
15
15
  `export default class App extends Application {`,
16
16
  ` modules = {`,
@@ -20,7 +20,7 @@ describe("wireInspectorSupport", () => {
20
20
  ].join("\n");
21
21
 
22
22
  expect(wireInspectorSupport(code, "app/app.ts")).toMatchInlineSnapshot(`
23
- "import Application from "ember-strict-application-resolver";
23
+ "import Application from "@ember/application";
24
24
  import setupInspector from "@embroider/legacy-inspector-support/ember-source-4.12";
25
25
  export default class App extends Application {
26
26
  modules = { ...import.meta.glob("./router.*", { eager: true }) };
@@ -31,7 +31,7 @@ describe("wireInspectorSupport", () => {
31
31
 
32
32
  it("wires a named class exported in a separate statement", () => {
33
33
  const code = [
34
- `import Application from "ember-strict-application-resolver";`,
34
+ `import Application from "@ember/application";`,
35
35
  ``,
36
36
  `class App extends Application {`,
37
37
  ` modules = {};`,
@@ -41,7 +41,7 @@ describe("wireInspectorSupport", () => {
41
41
  ].join("\n");
42
42
 
43
43
  expect(wireInspectorSupport(code, "app/app.js")).toMatchInlineSnapshot(`
44
- "import Application from "ember-strict-application-resolver";
44
+ "import Application from "@ember/application";
45
45
  import setupInspector from "@embroider/legacy-inspector-support/ember-source-4.12";
46
46
  class App extends Application {
47
47
  modules = { };
@@ -53,13 +53,13 @@ describe("wireInspectorSupport", () => {
53
53
 
54
54
  it("wires an anonymous default-exported class", () => {
55
55
  const code = [
56
- `import Application from "ember-strict-application-resolver";`,
56
+ `import Application from "@ember/application";`,
57
57
  ``,
58
58
  `export default class extends Application {}`,
59
59
  ].join("\n");
60
60
 
61
61
  expect(wireInspectorSupport(code, "app/app.js")).toMatchInlineSnapshot(`
62
- "import Application from "ember-strict-application-resolver";
62
+ "import Application from "@ember/application";
63
63
  import setupInspector from "@embroider/legacy-inspector-support/ember-source-4.12";
64
64
  export default class extends Application {
65
65
  inspector = setupInspector(this);
@@ -69,7 +69,7 @@ describe("wireInspectorSupport", () => {
69
69
 
70
70
  it("wires a class expression assigned to a variable", () => {
71
71
  const code = [
72
- `import Application from "ember-strict-application-resolver";`,
72
+ `import Application from "@ember/application";`,
73
73
  ``,
74
74
  `const App = class extends Application {`,
75
75
  ` modules = {};`,
@@ -79,7 +79,7 @@ describe("wireInspectorSupport", () => {
79
79
  ].join("\n");
80
80
 
81
81
  expect(wireInspectorSupport(code, "app/app.js")).toMatchInlineSnapshot(`
82
- "import Application from "ember-strict-application-resolver";
82
+ "import Application from "@ember/application";
83
83
  import setupInspector from "@embroider/legacy-inspector-support/ember-source-4.12";
84
84
  const App = class extends Application {
85
85
  modules = { };
@@ -108,14 +108,14 @@ describe("wireInspectorSupport", () => {
108
108
  it("adds only the missing member when the import is already present", () => {
109
109
  const code = [
110
110
  `import wireUp from "@embroider/legacy-inspector-support/ember-source-4.12";`,
111
- `import Application from "ember-strict-application-resolver";`,
111
+ `import Application from "@ember/application";`,
112
112
  ``,
113
113
  `export default class App extends Application {}`,
114
114
  ].join("\n");
115
115
 
116
116
  expect(wireInspectorSupport(code, "app/app.ts")).toMatchInlineSnapshot(`
117
117
  "import wireUp from "@embroider/legacy-inspector-support/ember-source-4.12";
118
- import Application from "ember-strict-application-resolver";
118
+ import Application from "@ember/application";
119
119
  export default class App extends Application {
120
120
  inspector = wireUp(this);
121
121
  }"
@@ -124,7 +124,7 @@ describe("wireInspectorSupport", () => {
124
124
 
125
125
  it("is idempotent", () => {
126
126
  const code = [
127
- `import Application from "ember-strict-application-resolver";`,
127
+ `import Application from "@ember/application";`,
128
128
  ``,
129
129
  `export default class App extends Application {`,
130
130
  ` modules = {};`,
@@ -157,7 +157,7 @@ describe("wireInspectorSupport", () => {
157
157
  describe("hasInspectorSupport", () => {
158
158
  it("is false before wiring and true after", () => {
159
159
  const code = [
160
- `import Application from "ember-strict-application-resolver";`,
160
+ `import Application from "@ember/application";`,
161
161
  ``,
162
162
  `export default class App extends Application {}`,
163
163
  ].join("\n");
@@ -169,7 +169,7 @@ describe("hasInspectorSupport", () => {
169
169
  it("is false when only the import is present", () => {
170
170
  const code = [
171
171
  `import setupInspector from "@embroider/legacy-inspector-support/ember-source-4.12";`,
172
- `import Application from "ember-strict-application-resolver";`,
172
+ `import Application from "@ember/application";`,
173
173
  ``,
174
174
  `export default class App extends Application {}`,
175
175
  ].join("\n");
@@ -52,10 +52,10 @@ describe("layer: inspector-support", () => {
52
52
  * - https://ember-primitives.pages.dev/6-utils/createService.md
53
53
  * - https://ember-primitives.pages.dev/6-utils/createAsyncService.md
54
54
  */
55
- import Application from 'ember-strict-application-resolver';
55
+ import Application from '@ember/application';
56
56
  import setupInspector from "@embroider/legacy-inspector-support/ember-source-4.12";
57
57
  export default class App extends Application {
58
- modules = { ...import.meta.glob('./router.*', { eager: true }), ...import.meta.glob('./templates/**/*', { eager: true }), ...import.meta.glob('./services/**/*', { eager: true }) };
58
+ modules = { ...import.meta.glob('./router.*', { eager: true }), ...import.meta.glob('./routes/**/*', { eager: true }), ...import.meta.glob('./templates/**/*', { eager: true }), ...import.meta.glob('./services/**/*', { eager: true }) };
59
59
  inspector = setupInspector(this);
60
60
  }"
61
61
  `);
@@ -42,23 +42,23 @@ describe("layer: readme", () => {
42
42
 
43
43
  ### Installation
44
44
 
45
- ~~~sh
45
+ \`\`\`sh
46
46
  pnpm install
47
- ~~~
47
+ \`\`\`
48
48
 
49
49
  ### Development
50
50
 
51
51
  To start the local development server:
52
52
 
53
- ~~~sh
53
+ \`\`\`sh
54
54
  pnpm dev
55
- ~~~
55
+ \`\`\`
56
56
 
57
57
  or
58
58
 
59
- ~~~sh
59
+ \`\`\`sh
60
60
  pnpm start
61
- ~~~
61
+ \`\`\`
62
62
 
63
63
  ## Features & Tooling
64
64
 
@@ -109,9 +109,9 @@ describe("layer: readme", () => {
109
109
 
110
110
  To build the library:
111
111
 
112
- ~~~sh
112
+ \`\`\`sh
113
113
  npm run build
114
- ~~~
114
+ \`\`\`
115
115
 
116
116
  ## Features & Tooling
117
117
 
@@ -156,29 +156,29 @@ describe("layer: readme", () => {
156
156
 
157
157
  To start development:
158
158
 
159
- ~~~sh
159
+ \`\`\`sh
160
160
  pnpm dev
161
- ~~~
161
+ \`\`\`
162
162
 
163
163
  or
164
164
 
165
- ~~~sh
165
+ \`\`\`sh
166
166
  pnpm start
167
- ~~~
167
+ \`\`\`
168
168
 
169
169
  ### Building
170
170
 
171
171
  To build the library:
172
172
 
173
- ~~~sh
173
+ \`\`\`sh
174
174
  pnpm build
175
- ~~~
175
+ \`\`\`
176
176
 
177
177
  or
178
178
 
179
- ~~~sh
179
+ \`\`\`sh
180
180
  pnpm build:watch
181
- ~~~
181
+ \`\`\`
182
182
 
183
183
  ## Features & Tooling
184
184
 
@@ -91,7 +91,7 @@ function appTests(ext: "ts" | "js") {
91
91
  [`tests/application/welcome-test.g${ext}`]: `import { describe, expect } from "vitest";
92
92
  import { applicationTest } from "ember-vitest";
93
93
  import { visit } from "@ember/test-helpers";
94
- import Application from "ember-strict-application-resolver";
94
+ import Application from "@ember/application";
95
95
 
96
96
  import Router from "#app/router.${ext}";
97
97
  import ApplicationTemplate from "#app/templates/application.g${ext}";
@@ -1,23 +0,0 @@
1
- import { defineConfig } from "rolldown";
2
-
3
- import manifest from "./package.json" with { type: "json" };
4
-
5
- const deps = [
6
- ...Object.keys(manifest.dependencies || {}),
7
- ...Object.keys(manifest.peerDependencies || {}),
8
- ];
9
-
10
- export default defineConfig({
11
- input: {
12
- index: "index.ts",
13
- "app-reexports": "src/app-reexports.ts",
14
- },
15
- output: {
16
- format: "esm",
17
- dir: "dist",
18
- },
19
- external(id) {
20
- if (id.startsWith("node:")) return true;
21
- if (deps.some((dep) => id.startsWith(dep))) return true;
22
- },
23
- });