ember.nvp 0.5.0 → 0.7.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 (91) hide show
  1. package/.github/workflows/ci.yml +17 -2
  2. package/.release-plan.json +84 -4
  3. package/CHANGELOG.md +68 -0
  4. package/README.md +29 -0
  5. package/package.json +4 -2
  6. package/packages/build-tooling-utils/README.md +37 -0
  7. package/packages/build-tooling-utils/index.ts +2 -0
  8. package/packages/build-tooling-utils/package.json +42 -0
  9. package/packages/build-tooling-utils/rolldown.config.js +18 -0
  10. package/packages/build-tooling-utils/src/extensions.ts +18 -0
  11. package/packages/build-tooling-utils/src/maybe-babel.ts +146 -0
  12. package/packages/rolldown/README.md +115 -0
  13. package/packages/rolldown/index.ts +56 -0
  14. package/packages/rolldown/package.json +59 -0
  15. package/packages/rolldown/rolldown.config.js +21 -0
  16. package/packages/rolldown/src/babel.ts +136 -0
  17. package/packages/rolldown/src/define-config.ts +32 -0
  18. package/packages/rolldown/src/externals.ts +83 -0
  19. package/packages/rolldown/src/isolated-declarations.ts +65 -0
  20. package/packages/rolldown/src/transform.ts +162 -0
  21. package/packages/vite/README.md +7 -21
  22. package/packages/vite/index.ts +1 -1
  23. package/packages/vite/package.json +4 -6
  24. package/packages/vite/src/ember.ts +129 -15
  25. package/packages/vite/src/test-html.ts +86 -0
  26. package/src/bases/minimal-app/files/app/router.ts +1 -0
  27. package/src/bases/minimal-app/files/tsconfig.json +1 -0
  28. package/src/bases/minimal-app/index.js +2 -2
  29. package/src/bases/minimal-library/files/README.md +41 -0
  30. package/src/bases/minimal-library/files/package.json +49 -0
  31. package/src/bases/minimal-library/files/tsconfig.json +10 -0
  32. package/src/bases/minimal-library/files/tsdown.config.js +6 -0
  33. package/src/bases/minimal-library/index.js +158 -2
  34. package/src/cli/args.js +7 -1
  35. package/src/cli/index.js +90 -16
  36. package/src/cli/questions/layers.js +5 -2
  37. package/src/cli/questions/project-type.js +0 -5
  38. package/src/cli/questions/write.js +169 -0
  39. package/src/index.js +1 -0
  40. package/src/layers/are-the-types-wrong/index.js +74 -0
  41. package/src/layers/git/files/gitignore +14 -1
  42. package/src/layers/git/index.js +6 -5
  43. package/src/layers/github-actions/index.js +12 -8
  44. package/src/layers/index.js +1 -1
  45. package/src/layers/inspector-support/index.js +101 -0
  46. package/src/layers/prettier/files/.prettierignore +4 -0
  47. package/src/layers/publint/index.js +68 -0
  48. package/src/layers/qunit/files/tests/unit/macros-test.ts +2 -2
  49. package/src/layers/qunit/index.js +107 -7
  50. package/src/layers/qunit/library-files/config/test/babel.config.js +41 -0
  51. package/src/layers/qunit/library-files/config/test/testem.cjs +26 -0
  52. package/src/layers/qunit/library-files/tests/test-helper.ts +36 -0
  53. package/src/layers/qunit/library-files/tests/unit/.gitkeep +0 -0
  54. package/src/layers/qunit/library-files/vite.config.mjs +6 -0
  55. package/src/layers/typescript/index.js +43 -7
  56. package/src/layers/vitest/files/vitest.config.mjs +11 -11
  57. package/src/layers/vitest/index.js +70 -32
  58. package/src/utils/babel.js +9 -6
  59. package/src/utils/fs.js +44 -3
  60. package/src/utils/inspector-support.js +196 -0
  61. package/src/utils/npm.js +21 -7
  62. package/src/utils/remove-types.js +5 -0
  63. package/src/utils/rewrite-imports.js +157 -0
  64. package/src/utils/stage.js +317 -0
  65. package/src/utils/tsdown-config.js +85 -0
  66. package/src/utils/types.ts +5 -1
  67. package/test/ember.nvp/base-library.test.ts +232 -0
  68. package/test/ember.nvp/generator-replace-or-update.test.ts +2 -3
  69. package/test/ember.nvp/inspector-support.test.ts +179 -0
  70. package/test/ember.nvp/layer-inspector-support.test.ts +69 -0
  71. package/test/ember.nvp/layer-publish-checks.test.ts +103 -0
  72. package/test/ember.nvp/layer-qunit.test.ts +376 -15
  73. package/test/ember.nvp/layer-vitest.test.ts +250 -0
  74. package/test/ember.nvp/layers.test.ts +16 -0
  75. package/test/ember.nvp/library-src-fixtures.ts +81 -0
  76. package/test/ember.nvp/permutations/app-1.test.ts +3 -0
  77. package/test/ember.nvp/permutations/app-2.test.ts +3 -0
  78. package/test/ember.nvp/permutations/library-1.test.ts +3 -0
  79. package/test/ember.nvp/permutations/library-2.test.ts +3 -0
  80. package/test/ember.nvp/permutations/shared.ts +239 -0
  81. package/test/ember.nvp/rewrite-imports.test.ts +235 -0
  82. package/test/ember.nvp/stage.test.ts +188 -0
  83. package/test/test-helpers/permutate.test.ts +17 -0
  84. package/test/test-helpers.ts +20 -7
  85. package/tsconfig.json +1 -0
  86. package/packages/vite/src/maybe-babel.ts +0 -219
  87. package/src/layers/qunit/files/run-tests.mjs +0 -30
  88. package/src/layers/vitest/files/tests/test-helper.ts +0 -5
  89. package/test/ember.nvp/permutations.test.ts +0 -200
  90. /package/src/{layers/vitest/files/tests/integration/.gitkeep → bases/minimal-library/files/src/index.ts} +0 -0
  91. /package/src/layers/{vitest/files/tests/unit → qunit/library-files/tests/rendering}/.gitkeep +0 -0
@@ -0,0 +1,162 @@
1
+ import { Preprocessor } from "content-tag";
2
+ import { existsSync } from "node:fs";
3
+ import { readFile, writeFile, readdir } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import type { Plugin } from "rolldown";
6
+
7
+ const processor = new Preprocessor();
8
+
9
+ /**
10
+ * `.d.ts` files emitted for `.gts` source reference the original `.gts`
11
+ * specifiers. Consumers can't resolve those, so we rewrite them back to
12
+ * extension-less specifiers.
13
+ */
14
+ function fixDeclarationImports(content: string): string {
15
+ return content
16
+ .replace(/from\s+'([^']+)\.gts'/g, "from '$1'")
17
+ .replace(/from\s+"([^"]+)\.gts"/g, 'from "$1"')
18
+ .replace(/import\("([^"]+)\.gts"\)/g, 'import("$1")')
19
+ .replace(/import\('([^']+)\.gts'\)/g, "import('$1')");
20
+ }
21
+
22
+ async function fixDtsExtensionsInDir(dir: string): Promise<void> {
23
+ const entries = await readdir(dir, { withFileTypes: true });
24
+
25
+ for (const entry of entries) {
26
+ const fullPath = path.join(dir, entry.name);
27
+
28
+ if (entry.isDirectory()) {
29
+ await fixDtsExtensionsInDir(fullPath);
30
+ } else if (entry.name.endsWith(".d.ts")) {
31
+ const content = await readFile(fullPath, { encoding: "utf8" });
32
+ const fixed = fixDeclarationImports(content);
33
+
34
+ if (fixed !== content) {
35
+ await writeFile(fullPath, fixed);
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Maps `.gts` -> `.ts` and `.gjs` -> `.js` (so rolldown can identify them as
43
+ * ts/js) and preprocesses `<template>` via content-tag.
44
+ */
45
+ export function emberTransform(): Plugin {
46
+ const virtualFiles = new Set<string>();
47
+
48
+ return {
49
+ name: "ember:transform",
50
+
51
+ resolveId: {
52
+ order: "pre",
53
+ handler(id, importer) {
54
+ if (!importer) return null;
55
+
56
+ const fileName = path.join(path.dirname(importer), id);
57
+
58
+ if (id.endsWith(".gts") && !importer.endsWith(".d.ts")) {
59
+ virtualFiles.add(fileName);
60
+ return {
61
+ id: fileName.replace(/\.gts$/, ".ts"),
62
+ meta: { fileName },
63
+ };
64
+ }
65
+
66
+ if (id.endsWith(".gjs") && !importer.endsWith(".d.ts")) {
67
+ virtualFiles.add(fileName);
68
+ return {
69
+ id: fileName.replace(/\.gjs$/, ".js"),
70
+ meta: { fileName },
71
+ };
72
+ }
73
+
74
+ if (id.endsWith(".d.ts") && importer.endsWith(".d.ts")) {
75
+ const gtsFile = fileName.replace(".d.ts", ".gts");
76
+ if (virtualFiles.has(gtsFile)) {
77
+ return fileName;
78
+ }
79
+ }
80
+
81
+ if (id.endsWith(".ts") && !importer.endsWith(".d.ts")) {
82
+ const gtsFileName = fileName.replace(/\.ts$/, ".gts");
83
+ if (existsSync(gtsFileName) && !existsSync(fileName)) {
84
+ virtualFiles.add(gtsFileName);
85
+ return { id: fileName, meta: { fileName: gtsFileName } };
86
+ }
87
+ }
88
+
89
+ if (id.endsWith(".js") && !importer.endsWith(".d.ts")) {
90
+ const gjsFileName = fileName.replace(/\.js$/, ".gjs");
91
+ if (existsSync(gjsFileName) && !existsSync(fileName)) {
92
+ virtualFiles.add(gjsFileName);
93
+ return { id: fileName, meta: { fileName: gjsFileName } };
94
+ }
95
+ }
96
+
97
+ return null;
98
+ },
99
+ },
100
+
101
+ load: {
102
+ order: "pre",
103
+ filter: {
104
+ id: /\.(ts|js)$/,
105
+ },
106
+ async handler(id) {
107
+ const meta = this.getModuleInfo(id)?.meta ?? {};
108
+ const fileName = meta?.fileName;
109
+
110
+ if (fileName) {
111
+ this.addWatchFile(fileName);
112
+ const source = await readFile(fileName, { encoding: "utf8" });
113
+
114
+ if (source.includes("<template>")) {
115
+ const { code, map } = processor.process(source, {
116
+ filename: fileName,
117
+ });
118
+ return { code, map };
119
+ }
120
+
121
+ return source;
122
+ }
123
+
124
+ return null;
125
+ },
126
+ },
127
+
128
+ transform: {
129
+ order: "pre",
130
+ filter: {
131
+ code: /\.gts/,
132
+ id: /\.(js|ts)$/,
133
+ },
134
+ handler(input, id) {
135
+ if (input.includes(".gts") && id.endsWith(".d.ts")) {
136
+ return input.replace(
137
+ /(['"`])((?:\.\.?\/|\/|@|[A-Za-z0-9_\-])[^'"]*?\.gts)\1/g,
138
+ (_m, q, p) => `${q}${p.replace(/\.gts$/, ".d.ts")}${q}`,
139
+ );
140
+ }
141
+
142
+ if (input.includes(".gts") && !id.endsWith(".d.ts")) {
143
+ return input.replace(
144
+ /(['"`])((?:\.\.?\/|\/|@|[A-Za-z0-9_\-])[^'"]*?\.gts)\1/g,
145
+ (_m, q, p) => `${q}${p.replace(/\.gts$/, ".ts")}${q}`,
146
+ );
147
+ }
148
+
149
+ return input;
150
+ },
151
+ },
152
+
153
+ writeBundle: {
154
+ async handler(options) {
155
+ const outDir = (options as { dir?: string }).dir;
156
+ if (outDir) {
157
+ await fixDtsExtensionsInDir(path.resolve(outDir));
158
+ }
159
+ },
160
+ },
161
+ };
162
+ }
@@ -8,7 +8,9 @@ A speed-optimized default meta-config for ember projects.
8
8
  npm add @nullvoxpopuli/ember-vite
9
9
  ```
10
10
 
11
- Requires node 24+
11
+ Requires node 24+, and — since these packages ship TypeScript source — a
12
+ modern TypeScript when type-checking: 6+ with `lib` covering `es2025` (e.g.
13
+ `esnext`).
12
14
 
13
15
  ## Usage
14
16
 
@@ -43,34 +45,18 @@ The `ember()` plugin takes the following options:
43
45
 
44
46
  ```ts
45
47
  ember({
46
- // optional production configs
47
48
  production: {
48
- // optional configuration of codeSplitting.groups
49
- // in the vite config.
50
- //
51
- // Docs here:
52
- // https://rolldown.rs/reference/OutputOptions.codeSplitting#groups
53
49
  codeSplittingGroups: [
54
- /* ... */
50
+ /* https://rolldown.rs/reference/OutputOptions.codeSplitting#groups */
55
51
  ],
56
52
  },
57
- // optional babel config
58
53
  babel: {
59
- // defaults to true.
60
- // when babel runs, do so in parallel across many cores.
61
54
  parallel: false,
62
- // optional way to configure when babel is activateed.
63
- // by default, all transforming is oxc, except when babel is needed
64
- // (for things not currently implemented in oxc)
65
55
  include: {
66
- // if a file imports from anything in this array, use babel instead of
67
- // oxc for that file.
68
- //
69
- // For example: ['ember-concurrency', 'ember-intl/format-message'],
70
- whenImporting: [
71
- /* ... */
72
- ],
56
+ whenImporting: ["ember-concurrency", "ember-intl/format-message"],
73
57
  },
74
58
  },
75
59
  });
76
60
  ```
61
+
62
+ Each option is documented on its type.
@@ -1,2 +1,2 @@
1
1
  export { ember } from "./src/ember.ts";
2
- export { maybeBabel } from "./src/maybe-babel.ts";
2
+ export { maybeBabel } from "@nullvoxpopuli/ember-build-tooling-utils";
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nullvoxpopuli/ember-vite",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "Meta plugin for ember projects focusing on speed.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -27,18 +27,16 @@
27
27
  "dependencies": {
28
28
  "@babel/core": "^7.28.10",
29
29
  "@embroider/core": "^4.4.2",
30
+ "@embroider/macros": "^1.20.3",
30
31
  "@embroider/vite": "^1.5.0",
31
- "@rollup/plugin-babel": "^7.1.0",
32
- "oxc-parser": "^0.132.0",
33
- "oxc-transform": "^0.132.0",
34
- "zimmerframe": "^1.1.4"
32
+ "@nullvoxpopuli/ember-build-tooling-utils": "workspace:*"
35
33
  },
36
34
  "engines": {
37
35
  "node": ">= 24"
38
36
  },
39
37
  "devDependencies": {
40
38
  "rolldown": "^1.1.2",
41
- "typescript": "^5.9.3",
39
+ "typescript": "^6.0.3",
42
40
  "vite": "^8.0.14"
43
41
  }
44
42
  }
@@ -1,9 +1,12 @@
1
+ import { buildMacros } from "@embroider/macros/babel";
1
2
  import { ember as embroiderEmber, extensions, resolver, templateTag } from "@embroider/vite";
2
3
  import { existsSync } from "node:fs";
3
4
  import { join, resolve } from "node:path";
4
5
  import type { ConfigEnv, ResolvedConfig } from "vite";
5
6
 
6
- import { maybeBabel } from "./maybe-babel.ts";
7
+ import { maybeBabel } from "@nullvoxpopuli/ember-build-tooling-utils";
8
+
9
+ import { testHtml } from "./test-html.ts";
7
10
 
8
11
  const emberConfig = embroiderEmber()[3]!;
9
12
 
@@ -12,6 +15,92 @@ function absolutePath(relativePath: string) {
12
15
  return join(cwd, relativePath);
13
16
  }
14
17
 
18
+ /**
19
+ * Everything a project without its own babel config needs:
20
+ *
21
+ * - TypeScript stripping
22
+ * - template compilation (wire format: vite serves/bundles for the
23
+ * project's own ember-source, so the final compile happens here)
24
+ * - build macros, evaluated at build time
25
+ * - decorator-transforms, with its runtime left as a bare specifier so it
26
+ * resolves from the project (which depends on `decorator-transforms`)
27
+ *
28
+ * The plugins are named, not imported: babel resolves them from the
29
+ * project, which carries them as its own dependencies.
30
+ */
31
+ function defaultBabelPlugins() {
32
+ const macros = buildMacros();
33
+
34
+ return [
35
+ [
36
+ "@babel/plugin-transform-typescript",
37
+ {
38
+ allExtensions: true,
39
+ onlyRemoveTypeImports: true,
40
+ allowDeclareFields: true,
41
+ },
42
+ ],
43
+ ["babel-plugin-ember-template-compilation", { transforms: [...macros.templateMacros] }],
44
+ ["module:decorator-transforms", { runtime: { import: "decorator-transforms/runtime-esm" } }],
45
+ ...macros.babelMacros,
46
+ ];
47
+ }
48
+
49
+ type BabelOptions =
50
+ | { configFile: string }
51
+ | {
52
+ configFile: false;
53
+ babelrc: false;
54
+ babelHelpers: "inline";
55
+ plugins: ReturnType<typeof defaultBabelPlugins>;
56
+ };
57
+
58
+ /**
59
+ * The project's babel config wins when it exists -- `babel.configFile`
60
+ * selects which one (e.g. a test-only `config/test/babel.config.js`).
61
+ * Without one (libraries), `defaultBabelPlugins` covers TypeScript,
62
+ * templates, macros, and decorators, so no config file is required --
63
+ * helpers are inlined because projects without a babel config don't
64
+ * carry `@babel/runtime`.
65
+ */
66
+ function babelOptions(nvpConfig: Config): BabelOptions {
67
+ const configFile = resolve(
68
+ join(process.cwd(), nvpConfig.babel?.configFile ?? "./babel.config.js"),
69
+ );
70
+
71
+ if (existsSync(configFile)) {
72
+ return { configFile };
73
+ }
74
+
75
+ return {
76
+ configFile: false,
77
+ babelrc: false,
78
+ babelHelpers: "inline",
79
+ plugins: defaultBabelPlugins(),
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Additional opt-ins beyond maybeBabel's built-in list.
85
+ *
86
+ * These libraries ship code that still needs babel (ember-concurrency's
87
+ * decorators, ember-scoped-css transforms, ember-intl's formatMessage macro),
88
+ * and the `initializeRuntimeMacrosConfig` runtime call must be compiled away.
89
+ */
90
+ function babelFilter(nvpConfig: Config) {
91
+ return {
92
+ include: {
93
+ imports: [
94
+ "ember-concurrency",
95
+ "ember-scoped-css",
96
+ "ember-intl/helpers/format-message",
97
+ ...(nvpConfig.babel?.include?.whenImporting ?? []),
98
+ ],
99
+ code: [/\bintl\.formatMessage\b/, "initializeRuntimeMacrosConfig"] as (string | RegExp)[],
100
+ },
101
+ };
102
+ }
103
+
15
104
  interface Config {
16
105
  babel?: {
17
106
  /**
@@ -20,6 +109,16 @@ interface Config {
20
109
  */
21
110
  parallel?: false;
22
111
 
112
+ /**
113
+ * which babel config file to use for this build, relative to the
114
+ * project root. defaults to "./babel.config.js".
115
+ *
116
+ * useful when the root babel config belongs to another pipeline
117
+ * (e.g. a library's publish build) and the vite build needs its own,
118
+ * such as "./config/test/babel.config.js".
119
+ */
120
+ configFile?: string;
121
+
23
122
  /**
24
123
  * optional way to configure when babel is activateed.
25
124
  * by default, all transforming is oxc, except when babel is needed
@@ -51,8 +150,8 @@ interface Config {
51
150
  }
52
151
 
53
152
  export function ember(nvpConfig: Config = {}) {
54
- const babelConfigFile = resolve(join(process.cwd(), "./babel.config.js"));
55
- const defaultEnv = { mode: process.env.NODE_ENV || "development" };
153
+ const babel = babelOptions(nvpConfig);
154
+ const filter = babelFilter(nvpConfig);
56
155
 
57
156
  /*
58
157
  * Plugins must be returned as top-level array entries — NOT pushed into
@@ -98,16 +197,21 @@ export function ember(nvpConfig: Config = {}) {
98
197
  // Libraries will have precompileTemplate and macros, etc,
99
198
  // and we need to compile that away using this app's
100
199
  // template compiler
101
- maybeBabel({ env, parallel: nvpConfig.babel?.parallel, configFile: babelConfigFile }),
200
+ maybeBabel({
201
+ ...babel,
202
+ parallel: nvpConfig.babel?.parallel,
203
+ filter,
204
+ }),
102
205
  ];
103
206
  },
104
207
  },
105
208
  resolver({ rolldown: true }),
106
209
  templateTag(),
210
+ testHtml(),
107
211
  maybeBabel({
108
- env: defaultEnv,
212
+ ...babel,
109
213
  parallel: nvpConfig.babel?.parallel,
110
- configFile: babelConfigFile,
214
+ filter,
111
215
  }),
112
216
  ];
113
217
  }
@@ -146,13 +250,10 @@ function dev(viteConfig: ResolvedConfig, nvpConfig: Config) {
146
250
  viteConfig.build.rolldownOptions.input ||= {};
147
251
  viteConfig.build.rolldownOptions.output ||= {};
148
252
 
149
- Object.assign(viteConfig.build.rolldownOptions.input, {
150
- main: absolutePath("./index.html"),
151
- });
152
-
153
- if (existsSync("./tests.html")) {
253
+ // libraries don't, by default, have a "demo app" entrypoint
254
+ if (existsSync("./index.html")) {
154
255
  Object.assign(viteConfig.build.rolldownOptions.input, {
155
- tests: absolutePath("./tests.html"),
256
+ main: absolutePath("./index.html"),
156
257
  });
157
258
  }
158
259
 
@@ -160,6 +261,16 @@ function dev(viteConfig: ResolvedConfig, nvpConfig: Config) {
160
261
  Object.assign(viteConfig.build.rolldownOptions.input, {
161
262
  tests: absolutePath("./tests/index.html"),
162
263
  });
264
+ } else if (existsSync("./tests.html")) {
265
+ Object.assign(viteConfig.build.rolldownOptions.input, {
266
+ tests: absolutePath("./tests.html"),
267
+ });
268
+ } else if (existsSync("./tests")) {
269
+ // no html of its own: the default-test-html plugin answers for
270
+ // tests/index.html
271
+ Object.assign(viteConfig.build.rolldownOptions.input, {
272
+ tests: absolutePath("./tests/index.html"),
273
+ });
163
274
  }
164
275
 
165
276
  Object.assign(viteConfig.build.rolldownOptions.output, {
@@ -203,9 +314,12 @@ function prod(viteConfig: ResolvedConfig, nvpConfig: Config) {
203
314
  viteConfig.build.rolldownOptions.optimization ||= {};
204
315
  viteConfig.build.rolldownOptions.output ||= {};
205
316
 
206
- Object.assign(viteConfig.build.rolldownOptions.input, {
207
- main: absolutePath("./index.html"),
208
- });
317
+ // libraries don't, by default, have a "demo app" entrypoint
318
+ if (existsSync("./index.html")) {
319
+ Object.assign(viteConfig.build.rolldownOptions.input, {
320
+ main: absolutePath("./index.html"),
321
+ });
322
+ }
209
323
 
210
324
  Object.assign(viteConfig.build.rolldownOptions.optimization, {
211
325
  inlineConst: {
@@ -0,0 +1,86 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { ViteDevServer } from "vite";
4
+
5
+ /**
6
+ * Everything a QUnit + testem run needs, with no project-specific
7
+ * references: root-relative specifiers keep it independent of where the
8
+ * project keeps its files, and the globs pick up tests/** as well as
9
+ * co-located `*-test` modules under src/**.
10
+ */
11
+ const DEFAULT_TEST_HTML = `<!doctype html>
12
+ <html>
13
+ <head>
14
+ <meta charset="utf-8" />
15
+ <title>Tests</title>
16
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
17
+ </head>
18
+ <body>
19
+ <div id="qunit"></div>
20
+ <div id="qunit-fixture">
21
+ <div id="ember-testing-container">
22
+ <div id="ember-testing"></div>
23
+ </div>
24
+ </div>
25
+
26
+ <script src="/testem.js" integrity="" data-embroider-ignore></script>
27
+ <script type="module">
28
+ import "ember-testing";
29
+ </script>
30
+
31
+ <script type="module">
32
+ import { start } from "/tests/test-helper";
33
+ import.meta.glob("/tests/**/*.{js,ts,gjs,gts}", { eager: true });
34
+ import.meta.glob("/src/**/*-test.{js,ts,gjs,gts}", { eager: true });
35
+ start();
36
+ </script>
37
+ </body>
38
+ </html>
39
+ `;
40
+
41
+ /**
42
+ * Lets a project run its tests without maintaining a tests/index.html:
43
+ * whenever that file is absent, this plugin answers for it -- `load`
44
+ * supplies the html to the build (so `tests/index.html` still lands in
45
+ * dist for testem), and the dev-server middleware serves it (through
46
+ * `transformIndexHtml`, so the html goes through the same pipeline as an
47
+ * on-disk file).
48
+ *
49
+ * A tests/index.html on disk always wins; every hook here defers to it.
50
+ */
51
+ export function testHtml() {
52
+ const htmlPath = join(process.cwd(), "tests", "index.html");
53
+
54
+ return {
55
+ name: "ember:default-test-html",
56
+
57
+ resolveId(id: string) {
58
+ if (id === htmlPath && !existsSync(htmlPath)) {
59
+ return id;
60
+ }
61
+ },
62
+
63
+ load(id: string) {
64
+ if (id === htmlPath && !existsSync(htmlPath)) {
65
+ return DEFAULT_TEST_HTML;
66
+ }
67
+ },
68
+
69
+ configureServer(server: ViteDevServer) {
70
+ server.middlewares.use(async (req, res, next) => {
71
+ if (existsSync(htmlPath)) return next();
72
+
73
+ const url = (req.url ?? "").split("?")[0];
74
+
75
+ if (url !== "/tests" && url !== "/tests/" && url !== "/tests/index.html") {
76
+ return next();
77
+ }
78
+
79
+ const html = await server.transformIndexHtml("/tests/index.html", DEFAULT_TEST_HTML);
80
+
81
+ res.setHeader("Content-Type", "text/html");
82
+ res.end(html);
83
+ });
84
+ },
85
+ };
86
+ }
@@ -1,4 +1,5 @@
1
1
  import EmbroiderRouter from "@embroider/router";
2
+
2
3
  import config from "#config";
3
4
 
4
5
  export default class Router extends EmbroiderRouter {
@@ -2,6 +2,7 @@
2
2
  "extends": "@ember/app-tsconfig",
3
3
  "compilerOptions": {
4
4
  "allowJs": true,
5
+ "lib": ["esnext", "dom", "dom.iterable"],
5
6
  "types": [
6
7
  "ember-source/types",
7
8
  "@embroider/core/virtual",
@@ -1,6 +1,6 @@
1
1
  import { packageJson, js } from "ember-apply";
2
+ import { join } from "node:path";
2
3
  import { getLatest } from "#utils/npm.js";
3
- import { fileURLToPath } from "node:url";
4
4
  import { applyFolderTo } from "#utils/fs.js";
5
5
  import { removeConfiguredPlugin } from "#utils/babel.js";
6
6
 
@@ -49,7 +49,7 @@ async function updateBabelConfig(project) {
49
49
  * @param {import('#utils/project.js').Project} project
50
50
  */
51
51
  async function applyFiles(project) {
52
- let filePath = fileURLToPath(new URL("files", import.meta.url));
52
+ let filePath = join(import.meta.dirname, "files");
53
53
 
54
54
  await applyFolderTo(filePath, project);
55
55
  }
@@ -0,0 +1,41 @@
1
+ # minimal-library
2
+
3
+ An Ember v2 library (addon) built with
4
+ [`@nullvoxpopuli/ember-rolldown`](https://github.com/NullVoxPopuli/ember.nvp/tree/main/packages/rolldown)
5
+ and [tsdown](https://tsdown.dev/).
6
+
7
+ ## Development
8
+
9
+ ```bash
10
+ pnpm install
11
+ ```
12
+
13
+ Build the distributable (JS + `.d.ts` into `dist/`):
14
+
15
+ ```bash
16
+ pnpm build
17
+ ```
18
+
19
+ Rebuild on change while developing:
20
+
21
+ ```bash
22
+ pnpm start
23
+ ```
24
+
25
+ ## Structure
26
+
27
+ - `src/` — your library source. Author components in `.gts`/`.gjs`
28
+ (template-tag) and plain modules in `.ts`/`.js`.
29
+ - `src/index.ts` — the public entry point. Everything a consumer can import
30
+ must be re-exported from here (or added as an entry in `tsdown.config.js`).
31
+ - `dist/` — the built output that gets published (git-ignored).
32
+
33
+ Declarations are emitted with
34
+ [isolated declarations](https://www.typescriptlang.org/tsconfig/#isolatedDeclarations),
35
+ so every exported value needs an explicit type annotation — in particular,
36
+ annotate exported template-only components with `TOC<...>`.
37
+
38
+ ## Publishing
39
+
40
+ `dist/` is built automatically on `prepack`, so `npm publish` ships the
41
+ compiled output plus your `src/`.
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "minimal-library",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "description": "Your Ember library's description here",
6
+ "license": "MIT",
7
+ "author": "",
8
+ "type": "module",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./*": {
15
+ "types": "./dist/*.d.ts",
16
+ "default": "./dist/*.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "src"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsdown",
25
+ "prepack": "tsdown",
26
+ "start": "tsdown --watch"
27
+ },
28
+ "dependencies": {
29
+ "decorator-transforms": "^2.3.2"
30
+ },
31
+ "peerDependencies": {
32
+ "@glimmer/component": "^2.0.0",
33
+ "ember-source": ">= 6.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@ember/library-tsconfig": "^2.0.0",
37
+ "@glimmer/component": "^2.1.1",
38
+ "@nullvoxpopuli/ember-rolldown": "workspace:*",
39
+ "ember-source": "~7.0.0",
40
+ "tsdown": "^0.22.7",
41
+ "typescript": "^6.0.3"
42
+ },
43
+ "engines": {
44
+ "node": ">= 24"
45
+ },
46
+ "ember": {
47
+ "edition": "octane"
48
+ }
49
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "@ember/library-tsconfig",
3
+ "include": ["src"],
4
+ "compilerOptions": {
5
+ "rootDir": "./src",
6
+ "lib": ["esnext", "dom", "dom.iterable"],
7
+ "isolatedDeclarations": true,
8
+ "types": ["ember-source/types", "@glint/ember-tsc/types"]
9
+ }
10
+ }
@@ -0,0 +1,6 @@
1
+ import { defineConfig, ember } from "@nullvoxpopuli/ember-rolldown";
2
+
3
+ export default defineConfig({
4
+ entry: ["./src/index.ts"],
5
+ plugins: [ember()],
6
+ });