ember.nvp 1.0.0 → 1.1.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.
- package/.github/workflows/ci.yml +6 -0
- package/.release-plan.json +23 -15
- package/CHANGELOG.md +30 -0
- package/package.json +1 -1
- package/packages/rolldown/README.md +62 -0
- package/packages/rolldown/package.json +13 -4
- package/packages/rolldown/rolldown.config.js +5 -3
- package/packages/rolldown/src/app-reexports.test.ts +220 -0
- package/packages/rolldown/src/app-reexports.ts +152 -0
- package/packages/rolldown/src/config.ts +1 -1
- package/packages/rolldown/src/dts.test.ts +166 -0
- package/packages/rolldown/src/transform.test.ts +173 -0
- package/src/bases/minimal-extension/extension.test.ts +251 -0
- package/src/bases/minimal-extension/files/.env.development +8 -0
- package/src/bases/minimal-extension/files/README.md +36 -0
- package/src/bases/minimal-extension/files/app/app.ts +21 -0
- package/src/bases/minimal-extension/files/app/boot.ts +8 -0
- package/src/bases/minimal-extension/files/app/config.ts +22 -0
- package/src/bases/minimal-extension/files/app/router.ts +10 -0
- package/src/bases/minimal-extension/files/app/templates/application.gts +5 -0
- package/src/bases/minimal-extension/files/babel.config.js +49 -0
- package/src/bases/minimal-extension/files/index.html +17 -0
- package/src/bases/minimal-extension/files/package.json +58 -0
- package/src/bases/minimal-extension/files/public/manifest.json +9 -0
- package/src/bases/minimal-extension/files/tsconfig.json +13 -0
- package/src/bases/minimal-extension/files/vite.config.mjs +6 -0
- package/src/bases/minimal-extension/index.js +142 -0
- package/src/cli/generator.js +4 -0
- package/src/cli/questions/project-type.js +9 -2
- package/src/layers/renovate/index.js +4 -3
- package/src/layers/typescript/index.js +6 -0
- package/src/utils/types.ts +1 -1
- package/test/ember.nvp/base-library.test.ts +39 -32
- package/test/ember.nvp/layer-publish-checks.test.ts +4 -13
- package/test/ember.nvp/layer-qunit.test.ts +13 -30
- package/test/ember.nvp/layer-vitest.test.ts +4 -18
- package/test/ember.nvp/permutations/app-checks.test.ts +3 -0
- package/test/ember.nvp/permutations/extension-1.test.ts +3 -0
- package/test/ember.nvp/permutations/extension-2.test.ts +3 -0
- package/test/ember.nvp/permutations/extension-checks.test.ts +3 -0
- package/test/ember.nvp/permutations/library-checks.test.ts +3 -0
- package/test/ember.nvp/permutations/shared.ts +68 -19
- package/test/test-helpers.ts +25 -30
- package/vitest.config.js +1 -1
package/.github/workflows/ci.yml
CHANGED
|
@@ -45,6 +45,12 @@ jobs:
|
|
|
45
45
|
args: test/ember.nvp/permutations/library-1.test.ts
|
|
46
46
|
- name: library permutations 2/2
|
|
47
47
|
args: test/ember.nvp/permutations/library-2.test.ts
|
|
48
|
+
- name: extension permutations 1/2
|
|
49
|
+
args: test/ember.nvp/permutations/extension-1.test.ts
|
|
50
|
+
- name: extension permutations 2/2
|
|
51
|
+
args: test/ember.nvp/permutations/extension-2.test.ts
|
|
52
|
+
- name: check-layer permutations
|
|
53
|
+
args: test/ember.nvp/permutations/app-checks.test.ts test/ember.nvp/permutations/library-checks.test.ts test/ember.nvp/permutations/extension-checks.test.ts
|
|
48
54
|
- name: everything else
|
|
49
55
|
args: --exclude 'test/ember.nvp/permutations/**'
|
|
50
56
|
|
package/.release-plan.json
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"solution": {
|
|
3
3
|
"ember.nvp": {
|
|
4
|
-
"impact": "
|
|
5
|
-
"oldVersion": "0.
|
|
6
|
-
"newVersion": "1.
|
|
4
|
+
"impact": "minor",
|
|
5
|
+
"oldVersion": "1.0.0",
|
|
6
|
+
"newVersion": "1.1.0",
|
|
7
7
|
"tagName": "latest",
|
|
8
8
|
"constraints": [
|
|
9
|
-
{
|
|
10
|
-
"impact": "major",
|
|
11
|
-
"reason": "Appears in changelog section :boom: Breaking Change"
|
|
12
|
-
},
|
|
13
9
|
{
|
|
14
10
|
"impact": "minor",
|
|
15
11
|
"reason": "Appears in changelog section :rocket: Enhancement"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"impact": "patch",
|
|
15
|
+
"reason": "Appears in changelog section :memo: Documentation"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"impact": "patch",
|
|
19
|
+
"reason": "Appears in changelog section :house: Internal"
|
|
16
20
|
}
|
|
17
21
|
],
|
|
18
22
|
"pkgJSONPath": "./package.json"
|
|
@@ -21,18 +25,22 @@
|
|
|
21
25
|
"oldVersion": "1.1.0"
|
|
22
26
|
},
|
|
23
27
|
"@nullvoxpopuli/ember-rolldown": {
|
|
24
|
-
"impact": "
|
|
25
|
-
"oldVersion": "
|
|
26
|
-
"newVersion": "2.
|
|
28
|
+
"impact": "minor",
|
|
29
|
+
"oldVersion": "2.0.0",
|
|
30
|
+
"newVersion": "2.1.0",
|
|
27
31
|
"tagName": "latest",
|
|
28
32
|
"constraints": [
|
|
29
|
-
{
|
|
30
|
-
"impact": "major",
|
|
31
|
-
"reason": "Appears in changelog section :boom: Breaking Change"
|
|
32
|
-
},
|
|
33
33
|
{
|
|
34
34
|
"impact": "minor",
|
|
35
35
|
"reason": "Appears in changelog section :rocket: Enhancement"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"impact": "patch",
|
|
39
|
+
"reason": "Appears in changelog section :memo: Documentation"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"impact": "patch",
|
|
43
|
+
"reason": "Appears in changelog section :house: Internal"
|
|
36
44
|
}
|
|
37
45
|
],
|
|
38
46
|
"pkgJSONPath": "./packages/rolldown/package.json"
|
|
@@ -41,5 +49,5 @@
|
|
|
41
49
|
"oldVersion": "1.1.0"
|
|
42
50
|
}
|
|
43
51
|
},
|
|
44
|
-
"description": "## Release (2026-07-
|
|
52
|
+
"description": "## Release (2026-07-22)\n\n* ember.nvp 1.1.0 (minor)\n* @nullvoxpopuli/ember-rolldown 2.1.0 (minor)\n\n#### :rocket: Enhancement\n* `@nullvoxpopuli/ember-rolldown`\n * [#97](https://github.com/NullVoxPopuli/ember.nvp/pull/97) Add appReexports plugin as a separate ember-rolldown entrypoint ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))\n * [#95](https://github.com/NullVoxPopuli/ember.nvp/pull/95) enable declaration maps in the rolldown plugin ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n* `ember.nvp`\n * [#91](https://github.com/NullVoxPopuli/ember.nvp/pull/91) Add a browser extension project type (minimal-extension base) ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))\n\n#### :memo: Documentation\n* `@nullvoxpopuli/ember-rolldown`, `ember.nvp`\n * [#96](https://github.com/NullVoxPopuli/ember.nvp/pull/96) rolldown: document + test co-located CSS support ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n\n#### :house: Internal\n* `@nullvoxpopuli/ember-rolldown`, `ember.nvp`\n * [#93](https://github.com/NullVoxPopuli/ember.nvp/pull/93) More tests ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n* `ember.nvp`\n * [#92](https://github.com/NullVoxPopuli/ember.nvp/pull/92) Move check layers into their own permutation matrix ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))\n * [#90](https://github.com/NullVoxPopuli/ember.nvp/pull/90) Remove pinYukuParser test helper ([@Copilot](https://github.com/apps/copilot-swe-agent))\n * [#89](https://github.com/NullVoxPopuli/ember.nvp/pull/89) refactor(test): extract shared listFiles/read helpers to #test-helpers ([@Copilot](https://github.com/apps/copilot-swe-agent))\n * [#87](https://github.com/NullVoxPopuli/ember.nvp/pull/87) Convert Greeting declaration check to inline snapshot assertion ([@Copilot](https://github.com/apps/copilot-swe-agent))\n\n#### Committers: 3\n- @NullVoxPopuli's reduced-access machine account for AI usage ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))\n- Copilot [Bot] ([@copilot-swe-agent](https://github.com/apps/copilot-swe-agent))\n- [@NullVoxPopuli](https://github.com/NullVoxPopuli)\n"
|
|
45
53
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Release (2026-07-22)
|
|
4
|
+
|
|
5
|
+
* ember.nvp 1.1.0 (minor)
|
|
6
|
+
* @nullvoxpopuli/ember-rolldown 2.1.0 (minor)
|
|
7
|
+
|
|
8
|
+
#### :rocket: Enhancement
|
|
9
|
+
* `@nullvoxpopuli/ember-rolldown`
|
|
10
|
+
* [#97](https://github.com/NullVoxPopuli/ember.nvp/pull/97) Add appReexports plugin as a separate ember-rolldown entrypoint ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))
|
|
11
|
+
* [#95](https://github.com/NullVoxPopuli/ember.nvp/pull/95) enable declaration maps in the rolldown plugin ([@NullVoxPopuli](https://github.com/NullVoxPopuli))
|
|
12
|
+
* `ember.nvp`
|
|
13
|
+
* [#91](https://github.com/NullVoxPopuli/ember.nvp/pull/91) Add a browser extension project type (minimal-extension base) ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))
|
|
14
|
+
|
|
15
|
+
#### :memo: Documentation
|
|
16
|
+
* `@nullvoxpopuli/ember-rolldown`, `ember.nvp`
|
|
17
|
+
* [#96](https://github.com/NullVoxPopuli/ember.nvp/pull/96) rolldown: document + test co-located CSS support ([@NullVoxPopuli](https://github.com/NullVoxPopuli))
|
|
18
|
+
|
|
19
|
+
#### :house: Internal
|
|
20
|
+
* `@nullvoxpopuli/ember-rolldown`, `ember.nvp`
|
|
21
|
+
* [#93](https://github.com/NullVoxPopuli/ember.nvp/pull/93) More tests ([@NullVoxPopuli](https://github.com/NullVoxPopuli))
|
|
22
|
+
* `ember.nvp`
|
|
23
|
+
* [#92](https://github.com/NullVoxPopuli/ember.nvp/pull/92) Move check layers into their own permutation matrix ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))
|
|
24
|
+
* [#90](https://github.com/NullVoxPopuli/ember.nvp/pull/90) Remove pinYukuParser test helper ([@Copilot](https://github.com/apps/copilot-swe-agent))
|
|
25
|
+
* [#89](https://github.com/NullVoxPopuli/ember.nvp/pull/89) refactor(test): extract shared listFiles/read helpers to #test-helpers ([@Copilot](https://github.com/apps/copilot-swe-agent))
|
|
26
|
+
* [#87](https://github.com/NullVoxPopuli/ember.nvp/pull/87) Convert Greeting declaration check to inline snapshot assertion ([@Copilot](https://github.com/apps/copilot-swe-agent))
|
|
27
|
+
|
|
28
|
+
#### Committers: 3
|
|
29
|
+
- @NullVoxPopuli's reduced-access machine account for AI usage ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))
|
|
30
|
+
- Copilot [Bot] ([@copilot-swe-agent](https://github.com/apps/copilot-swe-agent))
|
|
31
|
+
- [@NullVoxPopuli](https://github.com/NullVoxPopuli)
|
|
32
|
+
|
|
3
33
|
## Release (2026-07-18)
|
|
4
34
|
|
|
5
35
|
* ember.nvp 1.0.0 (major)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ember.nvp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.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",
|
|
@@ -82,6 +82,27 @@ import type { TOC } from "@ember/component/template-only";
|
|
|
82
82
|
export const Badge: TOC<BadgeSignature> = <template>...</template>;
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
## CSS
|
|
86
|
+
|
|
87
|
+
Components that import co-located CSS (`import './popup.css'`) need
|
|
88
|
+
[`@tsdown/css`](https://www.npmjs.com/package/@tsdown/css) installed in your
|
|
89
|
+
library — tsdown auto-detects it and bundles every imported stylesheet into a
|
|
90
|
+
single CSS file in `dist/`. Install the version matching your `tsdown`
|
|
91
|
+
version (they're released in lockstep):
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npm add --save-dev @tsdown/css
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Without it, tsdown's css-guard fails the build on the first CSS import — and
|
|
98
|
+
because the importing component module never loads, any declaration that
|
|
99
|
+
references that component dangles, surfacing as misleading
|
|
100
|
+
`UNLOADABLE_DEPENDENCY` errors on `<component>.d.ts` files.
|
|
101
|
+
|
|
102
|
+
Scoping/transforming the CSS itself (e.g.
|
|
103
|
+
[ember-scoped-css](https://github.com/soxhub/ember-scoped-css)) is separate
|
|
104
|
+
from bundling and runs via its own plugins.
|
|
105
|
+
|
|
85
106
|
## What `ember()` does
|
|
86
107
|
|
|
87
108
|
`ember()` returns an array of rolldown plugins:
|
|
@@ -114,6 +135,47 @@ ember({
|
|
|
114
135
|
|
|
115
136
|
Each option is documented on `BabelOptions`.
|
|
116
137
|
|
|
138
|
+
## App re-exports
|
|
139
|
+
|
|
140
|
+
For libraries whose modules must appear in the consuming app's namespace
|
|
141
|
+
(classic resolution: `{{a-component}}`, services and helpers looked up by
|
|
142
|
+
name), `appReexports()` does the same job as `@embroider/addon-dev`'s
|
|
143
|
+
`appReexports` rollup plugin. It's a separate import because most libraries
|
|
144
|
+
don't need it:
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
import { defineConfig } from "tsdown";
|
|
148
|
+
import { ember } from "@nullvoxpopuli/ember-rolldown";
|
|
149
|
+
import { appReexports } from "@nullvoxpopuli/ember-rolldown/app-reexports";
|
|
150
|
+
|
|
151
|
+
export default defineConfig({
|
|
152
|
+
entry: ["./src/index.ts", "./src/services/session.ts"],
|
|
153
|
+
plugins: [ember(), appReexports()],
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
With no arguments, top-level services (`services/*`) are re-exported — under
|
|
158
|
+
strict mode, components and helpers are imported, but services are still
|
|
159
|
+
injected by name. A string argument is an include glob, and an object gives
|
|
160
|
+
full control:
|
|
161
|
+
|
|
162
|
+
```js
|
|
163
|
+
appReexports(); // services/*
|
|
164
|
+
appReexports("components/**"); // one include glob
|
|
165
|
+
appReexports({ include: ["services/*", "helpers/*"], exclude: [...] });
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
For every built file matching `include` (minus `exclude` and `.d.ts` files),
|
|
169
|
+
it writes a module under `dist/_app_/` re-exporting from the library's own
|
|
170
|
+
name, and records the set in `package.json` under `ember-addon.app-js`.
|
|
171
|
+
`mapFilename` renames a re-export; `exports` picks which bindings it forwards
|
|
172
|
+
(default `["default"]`).
|
|
173
|
+
|
|
174
|
+
Unlike the embroider plugin, nothing is written unless its content actually
|
|
175
|
+
differs from what is on disk — `_app_/` modules are compared byte-for-byte
|
|
176
|
+
and `package.json` is only rewritten when the `app-js` map itself changed —
|
|
177
|
+
so rebuilds don't re-trigger file watchers.
|
|
178
|
+
|
|
117
179
|
## Credit
|
|
118
180
|
|
|
119
181
|
The `emberExternals` and `emberTransform` plugins are derived from
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nullvoxpopuli/ember-rolldown",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Meta plugin for building ember v2 libraries/addons with rolldown & tsdown.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.ts",
|
|
9
|
+
"./app-reexports": "./src/app-reexports.ts"
|
|
10
|
+
},
|
|
7
11
|
"publishConfig": {
|
|
8
|
-
"main": "dist/index.js"
|
|
12
|
+
"main": "dist/index.js",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./dist/index.js",
|
|
15
|
+
"./app-reexports": "./dist/app-reexports.js"
|
|
16
|
+
}
|
|
9
17
|
},
|
|
10
18
|
"keywords": [
|
|
11
19
|
"rolldown",
|
|
@@ -52,8 +60,9 @@
|
|
|
52
60
|
}
|
|
53
61
|
},
|
|
54
62
|
"devDependencies": {
|
|
55
|
-
"
|
|
56
|
-
"
|
|
63
|
+
"@tsdown/css": "^0.22.13",
|
|
64
|
+
"rolldown": "^1.2.0",
|
|
65
|
+
"tsdown": "^0.22.9",
|
|
57
66
|
"typescript": "^6.0.3"
|
|
58
67
|
}
|
|
59
68
|
}
|
|
@@ -8,11 +8,13 @@ const deps = [
|
|
|
8
8
|
];
|
|
9
9
|
|
|
10
10
|
export default defineConfig({
|
|
11
|
-
input:
|
|
11
|
+
input: {
|
|
12
|
+
index: "index.ts",
|
|
13
|
+
"app-reexports": "src/app-reexports.ts",
|
|
14
|
+
},
|
|
12
15
|
output: {
|
|
13
16
|
format: "esm",
|
|
14
|
-
|
|
15
|
-
codeSplitting: false,
|
|
17
|
+
dir: "dist",
|
|
16
18
|
},
|
|
17
19
|
external(id) {
|
|
18
20
|
if (id.startsWith("node:")) return true;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, readFile, rm, stat, utimes, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { rolldown, type Plugin } from "rolldown";
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
7
|
+
|
|
8
|
+
import { appReexports, type AppReexportsOptions } from "./app-reexports.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The plugin reads and writes package.json relative to the current working
|
|
12
|
+
* directory (matching the other plugins in this package), so each test runs
|
|
13
|
+
* inside its own temp project.
|
|
14
|
+
*/
|
|
15
|
+
describe("appReexports", () => {
|
|
16
|
+
let projectDir: string;
|
|
17
|
+
const originalCwd = process.cwd();
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
projectDir = await mkdtemp(join(tmpdir(), "app-reexports-"));
|
|
21
|
+
|
|
22
|
+
await writeFile(
|
|
23
|
+
join(projectDir, "package.json"),
|
|
24
|
+
JSON.stringify(
|
|
25
|
+
{
|
|
26
|
+
name: "my-lib",
|
|
27
|
+
version: "1.0.0",
|
|
28
|
+
"ember-addon": { version: 2, type: "addon" },
|
|
29
|
+
},
|
|
30
|
+
null,
|
|
31
|
+
2,
|
|
32
|
+
) + "\n",
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
await mkdir(join(projectDir, "src/components"), { recursive: true });
|
|
36
|
+
await mkdir(join(projectDir, "src/utils"), { recursive: true });
|
|
37
|
+
await writeFile(
|
|
38
|
+
join(projectDir, "src/components/foo.js"),
|
|
39
|
+
"export default function foo() {}\n",
|
|
40
|
+
);
|
|
41
|
+
await writeFile(
|
|
42
|
+
join(projectDir, "src/utils/math.js"),
|
|
43
|
+
"export function add(a, b) { return a + b; }\n",
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
process.chdir(projectDir);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(async () => {
|
|
50
|
+
process.chdir(originalCwd);
|
|
51
|
+
await rm(projectDir, { recursive: true, force: true });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
async function build(options?: string | AppReexportsOptions) {
|
|
55
|
+
const bundle = await rolldown({
|
|
56
|
+
input: {
|
|
57
|
+
"components/foo": "./src/components/foo.js",
|
|
58
|
+
"utils/math": "./src/utils/math.js",
|
|
59
|
+
},
|
|
60
|
+
plugins: [appReexports(options)],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await bundle.write({ dir: "dist" });
|
|
64
|
+
await bundle.close();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function readJson(path: string) {
|
|
68
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
it("writes _app_ re-exports and records them in package.json", async () => {
|
|
72
|
+
await build({ include: ["components/**"] });
|
|
73
|
+
|
|
74
|
+
expect(await readFile("dist/_app_/components/foo.js", "utf8")).toMatchInlineSnapshot(`
|
|
75
|
+
"export { default } from "my-lib/components/foo";
|
|
76
|
+
"
|
|
77
|
+
`);
|
|
78
|
+
|
|
79
|
+
const manifest = await readJson("package.json");
|
|
80
|
+
|
|
81
|
+
expect(manifest["ember-addon"]).toMatchInlineSnapshot(`
|
|
82
|
+
{
|
|
83
|
+
"app-js": {
|
|
84
|
+
"./components/foo.js": "./dist/_app_/components/foo.js",
|
|
85
|
+
},
|
|
86
|
+
"type": "addon",
|
|
87
|
+
"version": 2,
|
|
88
|
+
}
|
|
89
|
+
`);
|
|
90
|
+
|
|
91
|
+
// utils/math.js didn't match include
|
|
92
|
+
await expect(stat("dist/_app_/utils/math.js")).rejects.toThrow();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("accepts a string argument as the include glob", async () => {
|
|
96
|
+
await build("components/**");
|
|
97
|
+
|
|
98
|
+
const manifest = await readJson("package.json");
|
|
99
|
+
|
|
100
|
+
expect(Object.keys(manifest["ember-addon"]["app-js"])).toEqual(["./components/foo.js"]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("defaults to top-level services", async () => {
|
|
104
|
+
await mkdir(join(projectDir, "src/services/nested"), { recursive: true });
|
|
105
|
+
await writeFile(join(projectDir, "src/services/session.js"), "export default class {}\n");
|
|
106
|
+
await writeFile(join(projectDir, "src/services/nested/deep.js"), "export default class {}\n");
|
|
107
|
+
|
|
108
|
+
const bundle = await rolldown({
|
|
109
|
+
input: {
|
|
110
|
+
"components/foo": "./src/components/foo.js",
|
|
111
|
+
"services/session": "./src/services/session.js",
|
|
112
|
+
"services/nested/deep": "./src/services/nested/deep.js",
|
|
113
|
+
},
|
|
114
|
+
plugins: [appReexports()],
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await bundle.write({ dir: "dist" });
|
|
118
|
+
await bundle.close();
|
|
119
|
+
|
|
120
|
+
const manifest = await readJson("package.json");
|
|
121
|
+
|
|
122
|
+
// services/* is not deep: nested services (and everything else) stay out
|
|
123
|
+
expect(manifest["ember-addon"]["app-js"]).toEqual({
|
|
124
|
+
"./services/session.js": "./dist/_app_/services/session.js",
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("respects exclude", async () => {
|
|
129
|
+
await build({ include: ["**/*.js"], exclude: ["utils/**"] });
|
|
130
|
+
|
|
131
|
+
const manifest = await readJson("package.json");
|
|
132
|
+
|
|
133
|
+
expect(Object.keys(manifest["ember-addon"]["app-js"])).toEqual(["./components/foo.js"]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("supports mapFilename and custom exports", async () => {
|
|
137
|
+
await build({
|
|
138
|
+
include: ["utils/**"],
|
|
139
|
+
mapFilename: (name) => name.replace("math", "arithmetic"),
|
|
140
|
+
exports: () => ["add"],
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(await readFile("dist/_app_/utils/arithmetic.js", "utf8")).toMatchInlineSnapshot(`
|
|
144
|
+
"export { add } from "my-lib/utils/math";
|
|
145
|
+
"
|
|
146
|
+
`);
|
|
147
|
+
|
|
148
|
+
const manifest = await readJson("package.json");
|
|
149
|
+
|
|
150
|
+
expect(manifest["ember-addon"]["app-js"]).toMatchInlineSnapshot(`
|
|
151
|
+
{
|
|
152
|
+
"./utils/arithmetic.js": "./dist/_app_/utils/arithmetic.js",
|
|
153
|
+
}
|
|
154
|
+
`);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("never re-exports .d.ts files", async () => {
|
|
158
|
+
const emitDeclaration: Plugin = {
|
|
159
|
+
name: "test:emit-declaration",
|
|
160
|
+
generateBundle() {
|
|
161
|
+
this.emitFile({
|
|
162
|
+
type: "asset",
|
|
163
|
+
fileName: "components/foo.d.ts",
|
|
164
|
+
source: "declare const foo: () => void;\nexport default foo;\n",
|
|
165
|
+
});
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const bundle = await rolldown({
|
|
170
|
+
input: { "components/foo": "./src/components/foo.js" },
|
|
171
|
+
plugins: [emitDeclaration, appReexports({ include: ["**/*"] })],
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
await bundle.write({ dir: "dist" });
|
|
175
|
+
await bundle.close();
|
|
176
|
+
|
|
177
|
+
const manifest = await readJson("package.json");
|
|
178
|
+
|
|
179
|
+
expect(Object.keys(manifest["ember-addon"]["app-js"])).toEqual(["./components/foo.js"]);
|
|
180
|
+
await expect(stat("dist/_app_/components/foo.d.ts")).rejects.toThrow();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("does not touch files whose content would be unchanged", async () => {
|
|
184
|
+
await build({ include: ["components/**"] });
|
|
185
|
+
|
|
186
|
+
const epoch = new Date(0);
|
|
187
|
+
|
|
188
|
+
await utimes("package.json", epoch, epoch);
|
|
189
|
+
await utimes("dist/_app_/components/foo.js", epoch, epoch);
|
|
190
|
+
|
|
191
|
+
await build({ include: ["components/**"] });
|
|
192
|
+
|
|
193
|
+
expect((await stat("package.json")).mtimeMs).toBe(0);
|
|
194
|
+
expect((await stat("dist/_app_/components/foo.js")).mtimeMs).toBe(0);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("leaves an equivalent package.json alone, regardless of formatting", async () => {
|
|
198
|
+
await build({ include: ["components/**"] });
|
|
199
|
+
|
|
200
|
+
// Same app-js map, different formatting: the plugin must not reformat.
|
|
201
|
+
const reformatted =
|
|
202
|
+
JSON.stringify(await readJson("package.json"), null, 4).replace(/\n/g, "\r\n") + "\r\n";
|
|
203
|
+
|
|
204
|
+
await writeFile("package.json", reformatted);
|
|
205
|
+
await build({ include: ["components/**"] });
|
|
206
|
+
|
|
207
|
+
expect(await readFile("package.json", "utf8")).toBe(reformatted);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("rewrites package.json when the app-js map changes", async () => {
|
|
211
|
+
await build({ include: ["components/**"] });
|
|
212
|
+
await build({ include: ["utils/**"] });
|
|
213
|
+
|
|
214
|
+
const manifest = await readJson("package.json");
|
|
215
|
+
|
|
216
|
+
expect(manifest["ember-addon"]["app-js"]).toEqual({
|
|
217
|
+
"./utils/math.js": "./dist/_app_/utils/math.js",
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, extname, join, matchesGlob, relative, resolve, sep } from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { Plugin } from "rolldown";
|
|
5
|
+
|
|
6
|
+
export interface AppReexportsOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Globs, matched against each built filename (relative to the output
|
|
9
|
+
* directory), selecting which modules get an app re-export.
|
|
10
|
+
*
|
|
11
|
+
* Defaults to `["services/*"]` -- top-level services only. Under strict
|
|
12
|
+
* mode, components and helpers are imported, but services are still
|
|
13
|
+
* injected by name, so they are the one thing an app must always be able
|
|
14
|
+
* to resolve.
|
|
15
|
+
*
|
|
16
|
+
* `.d.ts` files never match.
|
|
17
|
+
*/
|
|
18
|
+
include?: string[];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Globs removing files that `include` matched.
|
|
22
|
+
*/
|
|
23
|
+
exclude?: string[];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Rename a re-export: receives the built filename, returns the filename the
|
|
27
|
+
* app sees. Defaults to the built filename unchanged.
|
|
28
|
+
*/
|
|
29
|
+
mapFilename?: (filename: string) => string;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Which bindings a file's re-export module forwards: an array of names
|
|
33
|
+
* (default `["default"]`), or a raw export clause string such as `"*"`.
|
|
34
|
+
*/
|
|
35
|
+
exports?: (filename: string) => string[] | string | undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Merges built modules into the consuming app's namespace, so classic
|
|
40
|
+
* resolution (`{{a-component}}`, service/helper lookup by name) finds them --
|
|
41
|
+
* the same job as `@embroider/addon-dev`'s `appReexports` rollup plugin.
|
|
42
|
+
*
|
|
43
|
+
* For every built file matching `include` (minus `exclude` and `.d.ts`), it
|
|
44
|
+
* writes a tiny module under `<outDir>/_app_/` re-exporting from the
|
|
45
|
+
* library's own name, and records the full set in `package.json` under
|
|
46
|
+
* `ember-addon.app-js`.
|
|
47
|
+
*
|
|
48
|
+
* Unlike the embroider plugin, nothing is written unless its content actually
|
|
49
|
+
* differs from what is on disk: the `_app_/` modules are compared
|
|
50
|
+
* byte-for-byte, and `package.json` is only rewritten when the `app-js` map
|
|
51
|
+
* itself changed (so an equivalent map in different formatting is left
|
|
52
|
+
* alone). This keeps rebuilds from re-triggering file watchers.
|
|
53
|
+
*
|
|
54
|
+
* This is a separate import, because most libraries don't need it:
|
|
55
|
+
*
|
|
56
|
+
* ```js
|
|
57
|
+
* import { defineConfig } from "tsdown";
|
|
58
|
+
* import { ember } from "@nullvoxpopuli/ember-rolldown";
|
|
59
|
+
* import { appReexports } from "@nullvoxpopuli/ember-rolldown/app-reexports";
|
|
60
|
+
*
|
|
61
|
+
* export default defineConfig({
|
|
62
|
+
* entry: ["./src/index.ts", "./src/services/session.ts"],
|
|
63
|
+
* plugins: [ember(), appReexports()],
|
|
64
|
+
* });
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* With no arguments, top-level services (`services/*`) are re-exported. A
|
|
68
|
+
* string argument is an include glob; an object gives full control:
|
|
69
|
+
*
|
|
70
|
+
* ```js
|
|
71
|
+
* appReexports(); // services/*
|
|
72
|
+
* appReexports("components/**"); // one include glob
|
|
73
|
+
* appReexports({ include: ["services/*", "helpers/*"], exclude: [...] });
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export function appReexports(options: string | AppReexportsOptions = {}): Plugin {
|
|
77
|
+
const resolved = typeof options === "string" ? { include: [options] } : options;
|
|
78
|
+
const include = resolved.include ?? ["services/*"];
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
name: "ember:app-reexports",
|
|
82
|
+
|
|
83
|
+
writeBundle(outputOptions, bundle) {
|
|
84
|
+
const manifest = readJsonSync("package.json");
|
|
85
|
+
const outDir = outputOptions.dir ?? dirname(outputOptions.file ?? "dist");
|
|
86
|
+
const appJs: Record<string, string> = {};
|
|
87
|
+
|
|
88
|
+
// Sorted so the app-js map (and therefore package.json) is stable
|
|
89
|
+
// across builds regardless of chunk emission order.
|
|
90
|
+
for (const builtFilename of Object.keys(bundle).sort()) {
|
|
91
|
+
if (!isIncluded(builtFilename, include, resolved.exclude)) continue;
|
|
92
|
+
|
|
93
|
+
const appFilename = resolved.mapFilename?.(builtFilename) ?? builtFilename;
|
|
94
|
+
const names = resolved.exports?.(builtFilename) || ["default"];
|
|
95
|
+
const clause = typeof names === "string" ? names : `{ ${names.join(", ")} }`;
|
|
96
|
+
const withoutExtension = builtFilename.slice(0, -extname(builtFilename).length);
|
|
97
|
+
const reexportPath = join(outDir, "_app_", appFilename);
|
|
98
|
+
|
|
99
|
+
appJs[`./${appFilename}`] = `./${toPosix(relative(process.cwd(), resolve(reexportPath)))}`;
|
|
100
|
+
|
|
101
|
+
writeFileIfChanged(
|
|
102
|
+
reexportPath,
|
|
103
|
+
`export ${clause} from "${manifest.name}/${withoutExtension}";\n`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const existing = manifest["ember-addon"]?.["app-js"] ?? {};
|
|
108
|
+
|
|
109
|
+
if (!sameStringRecord(existing, appJs)) {
|
|
110
|
+
manifest["ember-addon"] = { ...manifest["ember-addon"], "app-js": appJs };
|
|
111
|
+
writeFileSync("package.json", JSON.stringify(manifest, null, 2) + "\n");
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function readJsonSync(path: string) {
|
|
118
|
+
return JSON.parse(readFileSync(path, { encoding: "utf8" }));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isIncluded(filename: string, include: string[], exclude?: string[]): boolean {
|
|
122
|
+
if (matchesGlob(filename, "**/*.d.ts")) return false;
|
|
123
|
+
if (!include.some((glob) => matchesGlob(filename, glob))) return false;
|
|
124
|
+
if (exclude?.some((glob) => matchesGlob(filename, glob))) return false;
|
|
125
|
+
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function writeFileIfChanged(path: string, content: string) {
|
|
130
|
+
let existing: string | undefined;
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
existing = readFileSync(path, { encoding: "utf8" });
|
|
134
|
+
} catch {
|
|
135
|
+
// Doesn't exist yet (or isn't readable) -- write it.
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (existing === content) return;
|
|
139
|
+
|
|
140
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
141
|
+
writeFileSync(path, content);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function sameStringRecord(a: Record<string, string>, b: Record<string, string>): boolean {
|
|
145
|
+
const aKeys = Object.keys(a);
|
|
146
|
+
|
|
147
|
+
return aKeys.length === Object.keys(b).length && aKeys.every((key) => a[key] === b[key]);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function toPosix(path: string): string {
|
|
151
|
+
return path.split(sep).join("/");
|
|
152
|
+
}
|
|
@@ -34,7 +34,7 @@ export function emberConfig(): TsdownPlugin {
|
|
|
34
34
|
tsdownConfig(config: UserConfig) {
|
|
35
35
|
config.sourcemap ??= true;
|
|
36
36
|
config.clean ??= true;
|
|
37
|
-
config.dts ??= true;
|
|
37
|
+
config.dts ??= { sourcemap: true };
|
|
38
38
|
config.outExtensions ??= () => ({ js: ".js", dts: ".d.ts" });
|
|
39
39
|
config.report ??= false;
|
|
40
40
|
|