ember.nvp 1.6.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/.github/workflows/ci.yml +5 -1
  2. package/.release-plan.json +51 -9
  3. package/CHANGELOG.md +30 -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 +1 -1
  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 +138 -98
  12. package/packages/rolldown/index.ts +66 -28
  13. package/packages/rolldown/package.json +1 -1
  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 +117 -72
  18. package/packages/rolldown/src/bundle.test.ts +149 -0
  19. package/packages/rolldown/src/bundle.ts +71 -0
  20. package/packages/rolldown/src/config.ts +42 -18
  21. package/packages/rolldown/src/dts.test.ts +42 -26
  22. package/packages/rolldown/src/externals.test.ts +16 -11
  23. package/packages/rolldown/src/externals.ts +45 -28
  24. package/packages/rolldown/src/isolated-declarations.ts +56 -43
  25. package/packages/rolldown/src/plugin-like.ts +18 -15
  26. package/packages/rolldown/src/published-types.test.ts +18 -13
  27. package/packages/rolldown/src/transform.test.ts +26 -17
  28. package/packages/rolldown/src/transform.ts +68 -48
  29. package/packages/vite/README.md +10 -11
  30. package/packages/vite/package.json +1 -1
  31. package/packages/vite/src/ember.ts +44 -36
  32. package/packages/vite/src/test-html.ts +13 -11
  33. package/src/bases/README.md +6 -3
  34. package/src/bases/minimal-app/app.test.ts +7 -4
  35. package/src/bases/minimal-app/files/app/app.ts +2 -3
  36. package/src/bases/minimal-app/files/app/router.ts +3 -3
  37. package/src/bases/minimal-app/index.js +10 -6
  38. package/src/bases/minimal-custom-element/files/README.md +95 -0
  39. package/src/bases/minimal-custom-element/files/package.json +47 -0
  40. package/src/bases/minimal-custom-element/files/src/components/counter.gts +27 -0
  41. package/src/bases/minimal-custom-element/files/src/element.ts +45 -0
  42. package/src/bases/minimal-custom-element/files/src/index.ts +2 -0
  43. package/src/bases/minimal-custom-element/files/src/register.ts +5 -0
  44. package/src/bases/minimal-custom-element/files/tsconfig.json +10 -0
  45. package/src/bases/minimal-custom-element/files/tsdown.config.js +7 -0
  46. package/src/bases/minimal-custom-element/index.js +212 -0
  47. package/src/bases/minimal-extension/files/README.md +13 -5
  48. package/src/bases/minimal-extension/files/app/app.ts +2 -3
  49. package/src/bases/minimal-extension/files/app/config.ts +5 -3
  50. package/src/bases/minimal-extension/index.js +16 -11
  51. package/src/bases/minimal-library/files/README.md +8 -7
  52. package/src/bases/minimal-library/index.js +21 -15
  53. package/src/cli/args.js +1 -1
  54. package/src/cli/generator.js +4 -0
  55. package/src/cli/index.js +23 -18
  56. package/src/cli/questions/project-type.js +17 -2
  57. package/src/cli/questions/write.js +12 -10
  58. package/src/layers/are-the-types-wrong/index.js +6 -6
  59. package/src/layers/github-actions/index.js +7 -4
  60. package/src/layers/inspector-support/index.js +2 -2
  61. package/src/layers/publint/index.js +2 -2
  62. package/src/layers/qunit/index.js +21 -17
  63. package/src/layers/readme/index.js +55 -4
  64. package/src/layers/renovate/index.js +5 -5
  65. package/src/layers/typescript/index.js +20 -13
  66. package/src/layers/vitest/index.js +8 -4
  67. package/src/utils/babel.js +3 -2
  68. package/src/utils/buffer.js +2 -2
  69. package/src/utils/cli.js +4 -4
  70. package/src/utils/fs.js +23 -14
  71. package/src/utils/inspector-support.js +22 -19
  72. package/src/utils/npm.js +15 -12
  73. package/src/utils/project-type.js +19 -0
  74. package/src/utils/project.js +14 -2
  75. package/src/utils/remove-types.js +2 -4
  76. package/src/utils/rewrite-imports.js +43 -31
  77. package/src/utils/stage.js +44 -33
  78. package/src/utils/tsdown-config.js +6 -5
  79. package/src/utils/types.ts +10 -9
  80. package/test/ember.nvp/base-custom-element.test.ts +440 -0
  81. package/test/ember.nvp/base-library.test.ts +6 -4
  82. package/test/ember.nvp/generator-replace-or-update.test.ts +6 -4
  83. package/test/ember.nvp/inspector-support.test.ts +4 -3
  84. package/test/ember.nvp/layer-inspector-support.test.ts +2 -3
  85. package/test/ember.nvp/layer-publish-checks.test.ts +2 -3
  86. package/test/ember.nvp/layer-qunit.test.ts +2 -2
  87. package/test/ember.nvp/layer-readme.test.ts +63 -3
  88. package/test/ember.nvp/layer-vitest.test.ts +16 -9
  89. package/test/ember.nvp/layers-load.test.ts +26 -0
  90. package/test/ember.nvp/library-src-fixtures.ts +7 -3
  91. package/test/ember.nvp/permutations/custom-element-1.test.ts +3 -0
  92. package/test/ember.nvp/permutations/custom-element-2.test.ts +3 -0
  93. package/test/ember.nvp/permutations/custom-element-checks.test.ts +3 -0
  94. package/test/ember.nvp/permutations/shared.ts +22 -12
  95. package/test/ember.nvp/publish-configs.test.ts +44 -31
  96. package/test/ember.nvp/rewrite-imports.test.ts +4 -3
  97. package/test/ember.nvp/scoped-css-library.test.ts +6 -4
  98. package/test/test-helpers/permutate.test.ts +3 -3
  99. package/test/test-helpers.ts +12 -7
@@ -15,8 +15,9 @@ afterEach(() => {
15
15
  });
16
16
 
17
17
  /**
18
- * A babel config whose only plugin rewrites the string `"MARKER"` to `name`, so
19
- * the emitted code says which config file babel actually loaded.
18
+ * A babel config whose only plugin rewrites the string `"MARKER"` to `name`.
19
+ *
20
+ * So the emitted code says which config file babel loaded.
20
21
  */
21
22
  function markerConfig(name: string): string {
22
23
  return [
@@ -37,9 +38,13 @@ function markerConfig(name: string): string {
37
38
  }
38
39
 
39
40
  /**
40
- * Builds a `.gts` entry (so babel actually runs -- `maybeBabel` skips files that
41
- * need no transform) in a temp dir that becomes the cwd, the way config
42
- * detection sees it under the tsdown CLI. Returns the emitted code.
41
+ * Builds a `.gts` entry in a temp dir that becomes the cwd,
42
+ * the way config detection sees it under the tsdown CLI.
43
+ *
44
+ * A `.gts` entry so babel runs at all:
45
+ * `maybeBabel` skips files that need no transform.
46
+ *
47
+ * Returns the emitted code.
43
48
  */
44
49
  async function bundleWithBabel(
45
50
  files: Record<string, string>,
@@ -10,18 +10,33 @@ import type { Transform } from "babel-plugin-ember-template-compilation";
10
10
  import type { Plugin } from "rolldown";
11
11
 
12
12
  export interface BabelOptions {
13
+ /**
14
+ * Compile templates to the wire format instead of `precompileTemplate`.
15
+ *
16
+ * For a build that bundles its own ember-source.
17
+ * The wire format is private between one template compiler and one
18
+ * glimmer runtime of the same version, so this is only safe when both
19
+ * ship together.
20
+ *
21
+ * The compiler is ember-source's own, resolved from the package.
22
+ *
23
+ * Set by `ember({ bundle: true })`.
24
+ * Only applies without a babel config file.
25
+ */
26
+ bundle?: boolean;
13
27
  babelHelpers?: "bundled" | "runtime" | "inline" | "external";
14
28
  /**
15
29
  * The babel config file to use.
16
30
  *
17
- * - `undefined`: auto-detect. A `babel.publish.config.*` (in the package root
18
- * or `config/`) wins, because this is a publish build; otherwise babel's own
19
- * config resolution applies (see `detectConfigFile`).
31
+ * - `undefined`: auto-detect.
32
+ * A `babel.publish.config.*` (in the package root or `config/`) wins,
33
+ * because this is a publish build.
34
+ * Otherwise babel's own config resolution applies (see `detectConfigFile`).
20
35
  * - a string: use that config file.
21
36
  * - `false`: ignore config files entirely.
22
37
  *
23
- * Without a config file, templates, decorators, and TypeScript are still
24
- * handled (see `defaultPlugins`).
38
+ * Without a config file, templates, decorators, and TypeScript are still handled
39
+ * (see `defaultPlugins`).
25
40
  */
26
41
  configFile?: string | false;
27
42
  /**
@@ -29,17 +44,20 @@ export interface BabelOptions {
29
44
  */
30
45
  plugins?: PluginItem[];
31
46
  /**
32
- * Template AST transforms (e.g. ember-scoped-css's `scopedCSS.template()`)
33
- * passed to the default `babel-plugin-ember-template-compilation` entry.
47
+ * Template AST transforms passed to the default
48
+ * `babel-plugin-ember-template-compilation` entry.
49
+ * e.g. ember-scoped-css's `scopedCSS.template()`
34
50
  *
35
- * Only valid without a babel config file: with your own config, you list
36
- * `babel-plugin-ember-template-compilation` yourself, so its `transforms`
37
- * belong there.
51
+ * Only valid without a babel config file.
52
+ * With your own config, you list `babel-plugin-ember-template-compilation` yourself,
53
+ * so its `transforms` belong there.
38
54
  */
39
55
  templateTransforms?: Transform[];
40
56
  /**
41
- * Opt additional files into babel (see maybeBabel's `filter`). Use this to
42
- * cover addons that ship code needing babel (e.g. `ember-concurrency`).
57
+ * Opt additional files into babel (see maybeBabel's `filter`).
58
+ *
59
+ * Use this to cover addons that ship code needing babel
60
+ * (e.g. `ember-concurrency`).
43
61
  */
44
62
  filter?: {
45
63
  include: {
@@ -49,27 +67,34 @@ export interface BabelOptions {
49
67
  };
50
68
  }
51
69
 
52
- /** Tried in order, so an ESM config wins over a JSON one of the same name. */
70
+ /**
71
+ * Tried in order, so an ESM config wins over a JSON one of the same name.
72
+ */
53
73
  const PUBLISH_CONFIG_EXTENSIONS = ["mjs", "cjs", "js", "mts", "cts", "ts", "json"];
54
74
 
55
75
  /**
56
- * Tried in order: next to package.json first, then `config/` for libraries that
57
- * keep their build configuration out of the package root.
76
+ * Tried in order:
77
+ * - next to package.json first
78
+ * - then `config/`, for libraries that keep their build configuration
79
+ * out of the package root
58
80
  */
59
81
  const PUBLISH_CONFIG_DIRECTORIES = [".", "config"];
60
82
 
61
83
  /**
62
- * A `babel.publish.config.*` (in the package root or `config/`), if there is
63
- * one.
64
- *
65
- * A library's plain `babel.config.*` is its *development* config: it typically
66
- * compiles `@embroider/macros` away, targets the wire format, and wires up
67
- * whatever the in-package demo app or test suite needs. None of that belongs in
68
- * a published artifact -- macros must stay for the consuming app to evaluate,
69
- * and the wire format is private between one template compiler and one glimmer
70
- * runtime. Babel's own resolution can't tell the two apart (it finds whatever
71
- * `babel.config.*` exists), so a publish build has to prefer the config named
72
- * for publishing.
84
+ * A `babel.publish.config.*` (in the package root or `config/`), if there is one.
85
+ *
86
+ * A library's plain `babel.config.*` is its *development* config.
87
+ * It typically:
88
+ * - compiles `@embroider/macros` away
89
+ * - targets the wire format
90
+ * - wires up whatever the in-package demo app or test suite needs
91
+ *
92
+ * None of that belongs in a published artifact:
93
+ * - macros must stay, for the consuming app to evaluate
94
+ * - the wire format is private between one template compiler and one glimmer runtime
95
+ *
96
+ * Babel's own resolution can't tell the two apart (it finds whatever `babel.config.*` exists),
97
+ * so a publish build has to prefer the config named for publishing.
73
98
  */
74
99
  function detectPublishConfigFile(): string | undefined {
75
100
  for (const directory of PUBLISH_CONFIG_DIRECTORIES) {
@@ -84,14 +109,16 @@ function detectPublishConfigFile(): string | undefined {
84
109
  }
85
110
 
86
111
  /**
87
- * The config this publish build should use: a `babel.publish.config.*` when the
88
- * library has one, else the project's root babel config resolved the way babel
89
- * itself does (`babel.config.{js,cjs,mjs,cts,json}`, honoring `rootMode`
90
- * semantics) -- which for a library whose only config *is* its publish config
91
- * is the same file either way.
92
- *
93
- * The sync API evaluates the config file; on node 24+ that works for native
94
- * ESM configs too (require(esm)).
112
+ * The config this publish build must use:
113
+ * - a `babel.publish.config.*`, when the library has one
114
+ * - else the project's root babel config, resolved the way babel itself does
115
+ * (`babel.config.{js,cjs,mjs,cts,json}`, honoring `rootMode` semantics)
116
+ *
117
+ * For a library whose only config *is* its publish config,
118
+ * that is the same file either way.
119
+ *
120
+ * The sync API evaluates the config file.
121
+ * On node 24+ that works for native ESM configs too (require(esm)).
95
122
  */
96
123
  function detectConfigFile(): string | undefined {
97
124
  const publishConfig = detectPublishConfigFile();
@@ -107,17 +134,25 @@ function detectConfigFile(): string | undefined {
107
134
  * Everything a library without its own babel config needs:
108
135
  *
109
136
  * - TypeScript stripping
137
+ *
110
138
  * - template compilation to `precompileTemplate` (`targetFormat: "hbs"`).
111
- * Published libraries must NOT ship wire format: the wire format is
112
- * private between the template compiler and the glimmer runtime of the
113
- * same version, so baking it in ties the published artifact to the
114
- * consuming app's exact ember-source. The consuming app performs final
115
- * compilation.
116
- * - decorator-transforms, with its runtime left as a bare specifier so the
117
- * consuming app resolves it (the library keeps `decorator-transforms` as
118
- * a real dependency).
139
+ * Published libraries must NOT ship wire format.
140
+ * The wire format is private between the template compiler and the glimmer runtime
141
+ * of the same version, so baking it in ties the published artifact
142
+ * to the consuming app's exact ember-source.
143
+ * The consuming app performs final compilation.
144
+ *
145
+ * A bundle-mode build ships that exact ember-source,
146
+ * so it compiles to the wire format with ember-source's own compiler,
147
+ * and needs no compiler at runtime.
148
+ *
149
+ * - decorator-transforms, with its runtime left as a bare specifier
150
+ * so the consuming app resolves it.
151
+ * (the library keeps `decorator-transforms` as a real dependency)
152
+ *
153
+ * A bundle-mode build resolves it itself.
119
154
  */
120
- function defaultPlugins(templateTransforms?: Transform[]): PluginItem[] {
155
+ function defaultPlugins(templateTransforms?: Transform[], bundle = false): PluginItem[] {
121
156
  return [
122
157
  [
123
158
  transformTypeScript,
@@ -127,12 +162,15 @@ function defaultPlugins(templateTransforms?: Transform[]): PluginItem[] {
127
162
  allowDeclareFields: true,
128
163
  },
129
164
  ],
130
- [templateCompilation, { targetFormat: "hbs", transforms: templateTransforms ?? [] }],
165
+ [
166
+ templateCompilation,
167
+ { targetFormat: bundle ? "wire" : "hbs", transforms: templateTransforms ?? [] },
168
+ ],
131
169
  [
132
170
  decoratorTransforms,
133
171
  {
134
- // Emit `import ... from "decorator-transforms/runtime-esm"` as a bare
135
- // specifier so it stays external and the consuming app resolves it.
172
+ // Emit `import ... from "decorator-transforms/runtime-esm"` as a bare specifier,
173
+ // so it stays external and the consuming app resolves it.
136
174
  runtime: { import: "decorator-transforms/runtime-esm" },
137
175
  },
138
176
  ],
@@ -140,29 +178,34 @@ function defaultPlugins(templateTransforms?: Transform[]): PluginItem[] {
140
178
  }
141
179
 
142
180
  /**
143
- * We still need babel (not just oxc/rolldown's native transforms) for two
144
- * reasons:
145
- *
146
- * - template compilation: `babel-plugin-ember-template-compilation` turns the
147
- * content-tag output into `precompileTemplate` calls; oxc has no equivalent.
148
- * - decorators: Ember uses the legacy/stage-1 decorator signature together
149
- * with `decorator-transforms`, which rewrites decorated class fields
150
- * (`@tracked x`) into getter/setters backed by a runtime. oxc's native
151
- * "legacy decorators" emit tsc-style `__decorate` output, which does not
152
- * match those semantics.
153
- *
154
- * But we don't want to send *every* file through babel and give back the speed
155
- * rolldown/oxc buys us -- so we run babel via `maybeBabel`, which filters down
156
- * to only the files that actually need it (template-tag, decorators, template
157
- * imports). Everything else stays on the native transform.
158
- *
159
- * The library's own babel config is used when it exists -- its
160
- * `babel.publish.config.*` (root or `config/`) in preference to its
161
- * `babel.config.*`, since this is a publish build. Without one, `defaultPlugins` covers templates, decorators,
162
- * and TypeScript, so no config file is required.
163
- *
164
- * Libraries default to `babelHelpers: "bundled"` so the emitted output is
165
- * self-contained.
181
+ * We still need babel (not just oxc / rolldown's native transforms) for two reasons:
182
+ *
183
+ * - template compilation.
184
+ * `babel-plugin-ember-template-compilation` turns the content-tag output
185
+ * into `precompileTemplate` calls.
186
+ * oxc has no equivalent.
187
+ *
188
+ * - decorators.
189
+ * Ember uses the legacy / stage-1 decorator signature together with `decorator-transforms`,
190
+ * which rewrites decorated class fields (`@tracked x`) into getter / setters backed by a runtime.
191
+ * oxc's native "legacy decorators" emit tsc-style `__decorate` output,
192
+ * which does not match those semantics.
193
+ *
194
+ * But we don't want to send *every* file through babel
195
+ * and give back the speed rolldown / oxc buys us.
196
+ *
197
+ * So we run babel via `maybeBabel`, which filters down to only the files that need it
198
+ * (template-tag, decorators, template imports).
199
+ * Everything else stays on the native transform.
200
+ *
201
+ * The library's own babel config is used when it exists.
202
+ * Its `babel.publish.config.*` (root or `config/`) wins over its `babel.config.*`,
203
+ * since this is a publish build.
204
+ *
205
+ * Without one, `defaultPlugins` covers templates, decorators, and TypeScript,
206
+ * so no config file is required.
207
+ *
208
+ * Libraries default to `babelHelpers: "bundled"`, so the emitted output is self-contained.
166
209
  */
167
210
  export function emberBabel(options: BabelOptions = {}): Plugin {
168
211
  const configFile = options.configFile === undefined ? detectConfigFile() : options.configFile;
@@ -190,10 +233,12 @@ export function emberBabel(options: BabelOptions = {}): Plugin {
190
233
 
191
234
  return maybeBabel({
192
235
  ...shared,
193
- // No config file (or the caller opted out): don't let babel go looking
194
- // for one -- we provide the whole plugin list inline.
236
+ // No config file (or the caller opted out).
237
+ // Don't let babel go looking for one: we provide the whole plugin list inline.
195
238
  configFile: false,
196
239
  babelrc: false,
197
- plugins: defaultPlugins(options.templateTransforms).concat(options.plugins ?? []),
240
+ plugins: defaultPlugins(options.templateTransforms, options.bundle).concat(
241
+ options.plugins ?? [],
242
+ ),
198
243
  }) as unknown as Plugin;
199
244
  }
@@ -0,0 +1,149 @@
1
+ import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import path from "node:path";
4
+ import { rolldown } from "rolldown";
5
+ import type { InputOptions } from "rolldown";
6
+ import { afterEach, describe, expect, it } from "vitest";
7
+
8
+ import { emberBundle, emberSourceAliases } from "./bundle.ts";
9
+
10
+ let restoreCwd: (() => void) | undefined;
11
+
12
+ afterEach(() => {
13
+ restoreCwd?.();
14
+ restoreCwd = undefined;
15
+ });
16
+
17
+ /**
18
+ * An ember-source stub shaped like the real package:
19
+ * - a `./*` export whose conditions pick one of two builds
20
+ * - a `renamed-modules` map keyed by module file path
21
+ */
22
+ const EMBER_SOURCE_MANIFEST = {
23
+ name: "ember-source",
24
+ version: "0.0.0",
25
+ type: "module",
26
+ exports: {
27
+ "./*": {
28
+ development: "./dist/dev/packages/*",
29
+ production: "./dist/prod/packages/*",
30
+ default: "./dist/prod/packages/*",
31
+ },
32
+ "./package.json": "./package.json",
33
+ },
34
+ "ember-addon": {
35
+ "renamed-modules": {
36
+ "@ember/modifier/index.js": "ember-source/@ember/modifier/index.js",
37
+ "@ember/modifier/on.js": "ember-source/@ember/modifier/on.js",
38
+ "@ember/-internals/glimmer/index.js": "ember-source/@ember/-internals/glimmer/index.js",
39
+ "@glimmer/runtime/index.js": "ember-source/@glimmer/runtime/index.js",
40
+ "rsvp/index.js": "ember-source/rsvp/index.js",
41
+ "ember/index.js": "ember-source/ember/index.js",
42
+ },
43
+ },
44
+ };
45
+
46
+ /**
47
+ * Each provided module exists in both builds, and says which build it is.
48
+ */
49
+ const EMBER_SOURCE_MODULES = {
50
+ "@ember/modifier/index.js": `export const on = "on from BUILD";`,
51
+ "@ember/modifier/on.js": `export const on = "on from BUILD";`,
52
+ };
53
+
54
+ async function fixture({ emberSource = true } = {}): Promise<string> {
55
+ const dir = await mkdtemp(path.join(tmpdir(), "ember-rolldown-bundle-"));
56
+ await writeFile(
57
+ path.join(dir, "package.json"),
58
+ JSON.stringify({ name: "fixture", version: "0.0.0", type: "module" }),
59
+ );
60
+
61
+ if (emberSource) {
62
+ const stubDir = path.join(dir, "node_modules/ember-source");
63
+ await mkdir(stubDir, { recursive: true });
64
+ await writeFile(path.join(stubDir, "package.json"), JSON.stringify(EMBER_SOURCE_MANIFEST));
65
+
66
+ for (const build of ["dev", "prod"]) {
67
+ for (const [modulePath, source] of Object.entries(EMBER_SOURCE_MODULES)) {
68
+ const file = path.join(stubDir, "dist", build, "packages", modulePath);
69
+ await mkdir(path.dirname(file), { recursive: true });
70
+ await writeFile(file, source.replace("BUILD", build));
71
+ }
72
+ }
73
+ }
74
+
75
+ const previousCwd = process.cwd();
76
+ process.chdir(dir);
77
+ restoreCwd = () => process.chdir(previousCwd);
78
+
79
+ return dir;
80
+ }
81
+
82
+ async function bundle(dir: string, entry: string, resolve: InputOptions["resolve"] = {}) {
83
+ await writeFile(path.join(dir, "index.js"), entry);
84
+
85
+ const build = await rolldown({
86
+ input: path.join(dir, "index.js"),
87
+ plugins: [emberBundle()],
88
+ resolve,
89
+ onwarn() {},
90
+ });
91
+ const { output } = await build.generate({ format: "es" });
92
+
93
+ return output.filter((chunk) => "code" in chunk).map((chunk) => chunk.code)[0] ?? "";
94
+ }
95
+
96
+ describe("emberSourceAliases", () => {
97
+ it("aliases each provided package into ember-source", async () => {
98
+ await fixture();
99
+
100
+ expect(emberSourceAliases()).toEqual({
101
+ "@ember/modifier": "ember-source/@ember/modifier",
102
+ "@ember/-internals": "ember-source/@ember/-internals",
103
+ "@glimmer/runtime": "ember-source/@glimmer/runtime",
104
+ rsvp: "ember-source/rsvp",
105
+ ember: "ember-source/ember",
106
+ });
107
+ });
108
+
109
+ it("has nothing to alias without ember-source in the graph", async () => {
110
+ await fixture({ emberSource: false });
111
+
112
+ expect(emberSourceAliases()).toEqual({});
113
+ });
114
+ });
115
+
116
+ describe("emberBundle", () => {
117
+ it("bundles a provided package's index and module files from the production build", async () => {
118
+ const dir = await fixture();
119
+
120
+ const code = await bundle(
121
+ dir,
122
+ `import { on } from "@ember/modifier";\nimport { on as onModule } from "@ember/modifier/on";\nexport { on, onModule };`,
123
+ );
124
+
125
+ expect(code).toContain(`"on from prod"`);
126
+ expect(code).not.toContain(`from "@ember/modifier`);
127
+ });
128
+
129
+ it("follows the development condition", async () => {
130
+ const dir = await fixture();
131
+
132
+ const code = await bundle(dir, `export { on } from "@ember/modifier";`, {
133
+ conditionNames: ["development"],
134
+ });
135
+
136
+ expect(code).toContain(`"on from dev"`);
137
+ });
138
+
139
+ it("keeps explicit aliases", async () => {
140
+ const dir = await fixture();
141
+ await writeFile(path.join(dir, "mine.js"), `export const on = "mine";`);
142
+
143
+ const code = await bundle(dir, `export { on } from "@ember/modifier";`, {
144
+ alias: { "@ember/modifier": path.join(dir, "mine.js") },
145
+ });
146
+
147
+ expect(code).toContain(`"mine"`);
148
+ });
149
+ });
@@ -0,0 +1,71 @@
1
+ import type { Plugin } from "rolldown";
2
+
3
+ import { emberSourceRenamedModules } from "./externals.ts";
4
+
5
+ /**
6
+ * The package name a renamed-modules key belongs to
7
+ * `@ember/modifier/on.js` → `@ember/modifier`
8
+ * `rsvp/index.js` → `rsvp`
9
+ */
10
+ function packageOf(modulePath: string): string {
11
+ const segments = modulePath.split("/");
12
+ const length = modulePath.startsWith("@") ? 2 : 1;
13
+
14
+ return segments.slice(0, length).join("/");
15
+ }
16
+
17
+ /**
18
+ * One alias per package ember-source provides, pointing into ember-source
19
+ * `@ember/modifier` → `ember-source/@ember/modifier`
20
+ *
21
+ * The provided packages are the ones named in `ember-addon.renamed-modules`
22
+ * (`@ember/*`, `@glimmer/tracking`, `rsvp`, `backburner.js`, …).
23
+ *
24
+ * Real packages such as `@glimmer/component` are not in it,
25
+ * so they keep resolving from node_modules.
26
+ */
27
+ export function emberSourceAliases(): Record<string, string> {
28
+ const aliases: Record<string, string> = {};
29
+
30
+ for (const modulePath of Object.keys(emberSourceRenamedModules())) {
31
+ const name = packageOf(modulePath);
32
+
33
+ aliases[name] = `ember-source/${name}`;
34
+ }
35
+
36
+ return aliases;
37
+ }
38
+
39
+ /**
40
+ * Makes the modules ember-source provides resolvable,
41
+ * so a bundle-mode build can include them.
42
+ *
43
+ * ember-source exports every module under `./*`,
44
+ * with `development` and `production` conditions selecting one of its two builds.
45
+ *
46
+ * A bare `@ember/modifier` is not that path, though.
47
+ * It only exists as `ember-source/@ember/modifier`.
48
+ *
49
+ * So this plugin adds the alias for each provided package,
50
+ * and leaves the rest to the resolver:
51
+ * - the `default` condition selects the production build
52
+ * - `resolve.conditionNames: ["development"]` selects the development build
53
+ * (tsdown: `inputOptions.resolve.conditionNames`)
54
+ *
55
+ * Aliases set explicitly in the build config win over these.
56
+ */
57
+ export function emberBundle(): Plugin {
58
+ return {
59
+ name: "ember:bundle",
60
+
61
+ options(inputOptions) {
62
+ inputOptions.resolve ??= {};
63
+ inputOptions.resolve.alias = {
64
+ ...emberSourceAliases(),
65
+ ...inputOptions.resolve.alias,
66
+ };
67
+
68
+ return inputOptions;
69
+ },
70
+ };
71
+ }
@@ -1,33 +1,48 @@
1
1
  import type { TsdownPlugin, UserConfig } from "tsdown";
2
2
 
3
3
  /**
4
- * Sensible defaults for an Ember v2 library, applied through plugin hooks so
5
- * they travel with `ember()` — you get them whether you use this package's
6
- * `defineConfig`, tsdown's, or a plain `tsdown.config.js` / `rolldown.config.js`.
4
+ * Sensible defaults for an Ember v2 library.
5
+ *
6
+ * Applied through plugin hooks, so they travel with `ember()`.
7
+ * You get them whether you use this package's `defineConfig`, tsdown's,
8
+ * or a plain `tsdown.config.js` / `rolldown.config.js`.
7
9
  *
8
10
  * Every value is applied with `??=`, so anything you set explicitly wins.
9
11
  *
10
- * Under tsdown, the full set applies via the `tsdownConfig` hook (analogous to
11
- * Vite's `config` hook):
12
+ * Under tsdown, the full set applies via the `tsdownConfig` hook
13
+ * (analogous to Vite's `config` hook):
12
14
  *
13
- * - `sourcemap` — emit sourcemaps alongside the output.
14
- * - `clean` — wipe `dist/` between builds.
15
- * - `dts` — emit `.d.ts` declarations (via isolated declarations).
16
- * - `outExtensions` — `.js`/`.d.ts` rather than tsdown's default `.mjs`/`.d.mts`,
17
- * since exports maps conventionally point at `.js`/`.d.ts`.
18
- * - `report` — off; the size report is noise for a library build.
19
- * - `deps.neverBundle` — leave node builtins and the ember virtual packages to
20
- * the consuming app (`emberExternals()` handles the rest).
15
+ * - `sourcemap` — emit sourcemaps alongside the output
16
+ * - `clean` — wipe `dist/` between builds
17
+ * - `dts` — emit `.d.ts` declarations (via isolated declarations)
18
+ * - `outExtensions` — `.js` / `.d.ts` rather than tsdown's default `.mjs` / `.d.mts`
19
+ * (exports maps conventionally point at `.js` / `.d.ts`)
20
+ * - `report` — off.
21
+ * The size report is noise for a library build.
22
+ * - `deps.neverBundle` — leave node builtins and the ember virtual packages
23
+ * to the consuming app (`emberExternals()` handles the rest)
21
24
  *
22
- * A plain rolldown build has no notion of `clean`/`dts`/`outExtensions`/`report`
23
- * (those are tsdown-level concepts), so only the options with rolldown
24
- * equivalents are applied there, via rolldown's own `outputOptions` hook:
25
+ * A plain rolldown build has no notion of `clean` / `dts` / `outExtensions` / `report`.
26
+ * Those are tsdown-level concepts.
27
+ * So only the options with rolldown equivalents are applied there,
28
+ * via rolldown's own `outputOptions` hook:
25
29
  *
26
- * - `output.sourcemap` — on (output option).
30
+ * - `output.sourcemap` — on
27
31
  *
28
32
  * Externals are handled by `emberExternals()` (a `resolveId` hook) in both cases.
33
+ *
34
+ * Bundle mode flips the dependency defaults.
35
+ * The output is a self-contained browser artifact, so:
36
+ *
37
+ * - `platform` — `browser`
38
+ * - `deps.alwaysBundle` — everything but node builtins
39
+ * (including the package's own `dependencies` and `peerDependencies`,
40
+ * and the declarations' type imports)
41
+ * - `deps.neverBundle` — node builtins only
42
+ * - `deps.onlyBundle` — `false`
43
+ * (bundling node_modules is the point, so tsdown's hint about it is noise)
29
44
  */
30
- export function emberConfig(): TsdownPlugin {
45
+ export function emberConfig({ bundle = false }: { bundle?: boolean } = {}): TsdownPlugin {
31
46
  return {
32
47
  name: "ember:config",
33
48
 
@@ -39,6 +54,15 @@ export function emberConfig(): TsdownPlugin {
39
54
  config.report ??= false;
40
55
 
41
56
  config.deps ??= {};
57
+
58
+ if (bundle) {
59
+ config.platform ??= "browser";
60
+ config.deps.alwaysBundle ??= (id) => !id.startsWith("node:");
61
+ config.deps.neverBundle ??= ["node:*"];
62
+ config.deps.onlyBundle ??= false;
63
+ return;
64
+ }
65
+
42
66
  config.deps.neverBundle ??= ["node:*", "@ember/*", "@glimmer/*"];
43
67
  },
44
68