ember.nvp 1.3.0 → 1.4.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/.release-plan.json +13 -5
- package/CHANGELOG.md +18 -0
- package/package.json +1 -1
- package/packages/rolldown/README.md +62 -3
- package/packages/rolldown/package.json +1 -1
- package/packages/rolldown/src/babel.test.ts +125 -2
- package/packages/rolldown/src/babel.ts +52 -7
- package/packages/rolldown/src/isolated-declarations.ts +28 -3
- package/test/ember.nvp/publish-configs.test.ts +352 -0
- package/packages/rolldown/src/isolated-declarations.test.ts +0 -122
package/.release-plan.json
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
"solution": {
|
|
3
3
|
"ember.nvp": {
|
|
4
4
|
"impact": "minor",
|
|
5
|
-
"oldVersion": "1.
|
|
6
|
-
"newVersion": "1.
|
|
5
|
+
"oldVersion": "1.3.0",
|
|
6
|
+
"newVersion": "1.4.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"
|
|
12
16
|
}
|
|
13
17
|
],
|
|
14
18
|
"pkgJSONPath": "./package.json"
|
|
@@ -18,13 +22,17 @@
|
|
|
18
22
|
},
|
|
19
23
|
"@nullvoxpopuli/ember-rolldown": {
|
|
20
24
|
"impact": "minor",
|
|
21
|
-
"oldVersion": "2.
|
|
22
|
-
"newVersion": "2.
|
|
25
|
+
"oldVersion": "2.5.0",
|
|
26
|
+
"newVersion": "2.6.0",
|
|
23
27
|
"tagName": "latest",
|
|
24
28
|
"constraints": [
|
|
25
29
|
{
|
|
26
30
|
"impact": "minor",
|
|
27
31
|
"reason": "Appears in changelog section :rocket: Enhancement"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"impact": "patch",
|
|
35
|
+
"reason": "Appears in changelog section :house: Internal"
|
|
28
36
|
}
|
|
29
37
|
],
|
|
30
38
|
"pkgJSONPath": "./packages/rolldown/package.json"
|
|
@@ -33,5 +41,5 @@
|
|
|
33
41
|
"oldVersion": "1.1.0"
|
|
34
42
|
}
|
|
35
43
|
},
|
|
36
|
-
"description": "## Release (2026-08-03)\n\n* ember.nvp 1.
|
|
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"
|
|
37
45
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## Release (2026-08-03)
|
|
4
4
|
|
|
5
|
+
* ember.nvp 1.4.0 (minor)
|
|
6
|
+
* @nullvoxpopuli/ember-rolldown 2.6.0 (minor)
|
|
7
|
+
|
|
8
|
+
#### :rocket: Enhancement
|
|
9
|
+
* `@nullvoxpopuli/ember-rolldown`, `ember.nvp`
|
|
10
|
+
* [#111](https://github.com/NullVoxPopuli/ember.nvp/pull/111) rolldown: find publish configs in config/ too ([@NullVoxPopuli](https://github.com/NullVoxPopuli))
|
|
11
|
+
* `@nullvoxpopuli/ember-rolldown`
|
|
12
|
+
* [#109](https://github.com/NullVoxPopuli/ember.nvp/pull/109) rolldown: prefer babel.publish.config over babel.config ([@NullVoxPopuli](https://github.com/NullVoxPopuli))
|
|
13
|
+
|
|
14
|
+
#### :house: Internal
|
|
15
|
+
* `@nullvoxpopuli/ember-rolldown`, `ember.nvp`
|
|
16
|
+
* [#112](https://github.com/NullVoxPopuli/ember.nvp/pull/112) rolldown: test the isolated-declarations guard through generated libraries ([@NullVoxPopuli](https://github.com/NullVoxPopuli))
|
|
17
|
+
|
|
18
|
+
#### Committers: 1
|
|
19
|
+
- [@NullVoxPopuli](https://github.com/NullVoxPopuli)
|
|
20
|
+
|
|
21
|
+
## Release (2026-08-03)
|
|
22
|
+
|
|
5
23
|
* ember.nvp 1.3.0 (minor)
|
|
6
24
|
* @nullvoxpopuli/ember-rolldown 2.5.0 (minor)
|
|
7
25
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ember.nvp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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",
|
|
@@ -53,10 +53,12 @@ export default defineConfig({
|
|
|
53
53
|
});
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
A
|
|
56
|
+
A babel config is optional. Without one, `ember()` compiles templates
|
|
57
57
|
(to `precompileTemplate`), decorators (via
|
|
58
58
|
[decorator-transforms](https://github.com/ef4/decorator-transforms)), and
|
|
59
|
-
TypeScript; with one, your config runs instead.
|
|
59
|
+
TypeScript; with one, your config runs instead. A `babel.publish.config.*` is
|
|
60
|
+
preferred over a `babel.config.*` — see
|
|
61
|
+
[Publish vs. development babel config](#publish-vs-development-babel-config).
|
|
60
62
|
|
|
61
63
|
### Entrypoints
|
|
62
64
|
|
|
@@ -124,6 +126,21 @@ export default defineConfig({
|
|
|
124
126
|
});
|
|
125
127
|
```
|
|
126
128
|
|
|
129
|
+
Any path works, so the publish config can live wherever you keep build
|
|
130
|
+
configuration — `tsconfig: "./config/tsconfig.publish.json"`. (A directory works
|
|
131
|
+
too: `tsconfig: "./config"` picks up `config/tsconfig.json`.)
|
|
132
|
+
|
|
133
|
+
`tsconfig: false` is rejected while declarations are on: with no tsconfig there
|
|
134
|
+
is no `isolatedDeclarations`, and the fallback pipeline can't see compiled
|
|
135
|
+
`.gts`, so it would fail later with "Source file not found". Set `dts: false`
|
|
136
|
+
alongside it if the library ships no types.
|
|
137
|
+
|
|
138
|
+
A tsconfig's relative paths resolve against the file itself, so a config kept in
|
|
139
|
+
`config/` wants `"include": ["../src/**/*"]` and `"rootDir": "../src"`. The build
|
|
140
|
+
won't tell you if you get that wrong — tsdown drives declaration emit from
|
|
141
|
+
`entry`, not from the tsconfig's `include` — but `tsc`/`ember-tsc` and your
|
|
142
|
+
editor will, if you ever point them at that config.
|
|
143
|
+
|
|
127
144
|
The tradeoff: isolated-declaration errors in `src` then surface when you build
|
|
128
145
|
rather than in your editor, since the editor uses `tsconfig.json`. Run the build
|
|
129
146
|
(or `tsc --noEmit -p tsconfig.publish.json`) in CI so nothing lands unchecked.
|
|
@@ -198,7 +215,9 @@ it.
|
|
|
198
215
|
`.gts` specifiers in emitted `.d.ts` files.
|
|
199
216
|
- **`emberBabel()`** — runs babel with `babelHelpers: "bundled"`, but only on
|
|
200
217
|
the files that actually need it (template-tag, decorators, template imports);
|
|
201
|
-
everything else stays on rolldown's fast native (oxc) transform.
|
|
218
|
+
everything else stays on rolldown's fast native (oxc) transform. Uses your
|
|
219
|
+
`babel.publish.config.*` (root or `config/`) in preference to your
|
|
220
|
+
`babel.config.*`.
|
|
202
221
|
|
|
203
222
|
## Configuration
|
|
204
223
|
|
|
@@ -220,6 +239,46 @@ combined with a babel config file — a config lists
|
|
|
220
239
|
`babel-plugin-ember-template-compilation` itself, so its transforms belong
|
|
221
240
|
there.
|
|
222
241
|
|
|
242
|
+
### Publish vs. development babel config
|
|
243
|
+
|
|
244
|
+
A library's plain `babel.config.*` is usually its _development_ config: it
|
|
245
|
+
compiles `@embroider/macros` away, targets the wire format, and wires up
|
|
246
|
+
whatever the in-package demo app or test suite needs. None of that belongs in a
|
|
247
|
+
published artifact — macros must survive for the consuming app to evaluate, and
|
|
248
|
+
the wire format is private between one template compiler and one glimmer
|
|
249
|
+
runtime of the same version.
|
|
250
|
+
|
|
251
|
+
Babel's own resolution can't tell those apart, so `ember()` looks for a config
|
|
252
|
+
named for publishing first. With no explicit `configFile`, detection is:
|
|
253
|
+
|
|
254
|
+
1. `babel.publish.config.{mjs,cjs,js,mts,cts,ts,json}` in the package root
|
|
255
|
+
2. the same names in `config/`
|
|
256
|
+
3. otherwise babel's own resolution (`babel.config.*`, honoring `rootMode`)
|
|
257
|
+
4. otherwise no config file — templates, decorators and TypeScript are still
|
|
258
|
+
handled by the built-in defaults
|
|
259
|
+
|
|
260
|
+
So a library that keeps both configs needs no `babel` option at all:
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
my-addon/
|
|
264
|
+
babel.config.mjs # dev: macros compiled, wire format, test-app wiring
|
|
265
|
+
babel.publish.config.mjs # what ember() uses
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
...and so does one that keeps build configuration out of its root:
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
my-addon/
|
|
272
|
+
babel.config.mjs
|
|
273
|
+
config/
|
|
274
|
+
babel.publish.config.mjs # what ember() uses
|
|
275
|
+
tsconfig.publish.json # see below
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Set `configFile` explicitly to override that (`configFile: false` ignores config
|
|
279
|
+
files entirely, which is what you want when your publish config would just
|
|
280
|
+
restate the built-in defaults).
|
|
281
|
+
|
|
223
282
|
## App re-exports
|
|
224
283
|
|
|
225
284
|
For libraries whose modules must appear in the consuming app's namespace
|
|
@@ -1,6 +1,85 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { rolldown } from "rolldown";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
6
|
|
|
3
|
-
import { emberBabel } from "./babel.ts";
|
|
7
|
+
import { emberBabel, type BabelOptions } from "./babel.ts";
|
|
8
|
+
import { emberTransform } from "./transform.ts";
|
|
9
|
+
|
|
10
|
+
let restoreCwd: (() => void) | undefined;
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
restoreCwd?.();
|
|
14
|
+
restoreCwd = undefined;
|
|
15
|
+
});
|
|
16
|
+
|
|
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.
|
|
20
|
+
*/
|
|
21
|
+
function markerConfig(name: string): string {
|
|
22
|
+
return [
|
|
23
|
+
`export default {`,
|
|
24
|
+
` plugins: [`,
|
|
25
|
+
` function marker() {`,
|
|
26
|
+
` return {`,
|
|
27
|
+
` visitor: {`,
|
|
28
|
+
` StringLiteral(path) {`,
|
|
29
|
+
` if (path.node.value === 'MARKER') path.node.value = '${name}';`,
|
|
30
|
+
` },`,
|
|
31
|
+
` },`,
|
|
32
|
+
` };`,
|
|
33
|
+
` },`,
|
|
34
|
+
` ],`,
|
|
35
|
+
`};`,
|
|
36
|
+
].join("\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
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.
|
|
43
|
+
*/
|
|
44
|
+
async function bundleWithBabel(
|
|
45
|
+
files: Record<string, string>,
|
|
46
|
+
options?: BabelOptions,
|
|
47
|
+
): Promise<string> {
|
|
48
|
+
const dir = await mkdtemp(path.join(tmpdir(), "ember-rolldown-babel-"));
|
|
49
|
+
|
|
50
|
+
const withDefaults: Record<string, string> = {
|
|
51
|
+
"package.json": JSON.stringify({ name: "fixture", version: "0.0.0", type: "module" }),
|
|
52
|
+
"index.gts": [
|
|
53
|
+
`export const marker = 'MARKER';`,
|
|
54
|
+
`export default <template>hi</template>;`,
|
|
55
|
+
].join("\n"),
|
|
56
|
+
...files,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
for (const [relative, source] of Object.entries(withDefaults)) {
|
|
60
|
+
const full = path.join(dir, relative);
|
|
61
|
+
await mkdir(path.dirname(full), { recursive: true });
|
|
62
|
+
await writeFile(full, source, "utf8");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const previousCwd = process.cwd();
|
|
66
|
+
process.chdir(dir);
|
|
67
|
+
restoreCwd = () => process.chdir(previousCwd);
|
|
68
|
+
|
|
69
|
+
const build = await rolldown({
|
|
70
|
+
input: [path.join(dir, "index.gts")],
|
|
71
|
+
plugins: [emberTransform(), emberBabel(options)],
|
|
72
|
+
external: (id) => !(id.startsWith(".") || path.isAbsolute(id)),
|
|
73
|
+
onwarn() {},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const { output } = await build.generate({ format: "es" });
|
|
77
|
+
|
|
78
|
+
return output
|
|
79
|
+
.filter((chunk): chunk is typeof chunk & { code: string } => "code" in chunk)
|
|
80
|
+
.map((chunk) => chunk.code)
|
|
81
|
+
.join("\n");
|
|
82
|
+
}
|
|
4
83
|
|
|
5
84
|
describe("emberBabel", () => {
|
|
6
85
|
it("rejects templateTransforms combined with a config file", () => {
|
|
@@ -14,4 +93,48 @@ describe("emberBabel", () => {
|
|
|
14
93
|
emberBabel({ configFile: false, templateTransforms: ["some-transform"] }),
|
|
15
94
|
).not.toThrow();
|
|
16
95
|
});
|
|
96
|
+
|
|
97
|
+
describe("config detection", () => {
|
|
98
|
+
it("prefers babel.publish.config over babel.config", async () => {
|
|
99
|
+
const code = await bundleWithBabel({
|
|
100
|
+
"babel.config.mjs": markerConfig("dev"),
|
|
101
|
+
"babel.publish.config.mjs": markerConfig("publish"),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(code).toContain(`"publish"`);
|
|
105
|
+
expect(code).not.toContain(`"dev"`);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("falls back to babel.config when there is no publish config", async () => {
|
|
109
|
+
const code = await bundleWithBabel({ "babel.config.mjs": markerConfig("dev") });
|
|
110
|
+
|
|
111
|
+
expect(code).toContain(`"dev"`);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("uses an explicit configFile over either", async () => {
|
|
115
|
+
const code = await bundleWithBabel(
|
|
116
|
+
{
|
|
117
|
+
"babel.config.mjs": markerConfig("dev"),
|
|
118
|
+
"babel.publish.config.mjs": markerConfig("publish"),
|
|
119
|
+
"babel.explicit.config.mjs": markerConfig("explicit"),
|
|
120
|
+
},
|
|
121
|
+
{ configFile: "./babel.explicit.config.mjs" },
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(code).toContain(`"explicit"`);
|
|
125
|
+
expect(code).not.toContain(`"publish"`);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("ignores every config file when configFile is false", async () => {
|
|
129
|
+
const code = await bundleWithBabel(
|
|
130
|
+
{
|
|
131
|
+
"babel.config.mjs": markerConfig("dev"),
|
|
132
|
+
"babel.publish.config.mjs": markerConfig("publish"),
|
|
133
|
+
},
|
|
134
|
+
{ configFile: false },
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
expect(code).toContain(`"MARKER"`);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
17
140
|
});
|
|
@@ -3,6 +3,8 @@ import transformTypeScript from "@babel/plugin-transform-typescript";
|
|
|
3
3
|
import templateCompilation from "babel-plugin-ember-template-compilation";
|
|
4
4
|
import decoratorTransforms from "decorator-transforms";
|
|
5
5
|
import { loadPartialConfigSync } from "@babel/core";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { resolve } from "node:path";
|
|
6
8
|
import type { PluginItem } from "@babel/core";
|
|
7
9
|
import type { Transform } from "babel-plugin-ember-template-compilation";
|
|
8
10
|
import type { Plugin } from "rolldown";
|
|
@@ -12,8 +14,9 @@ export interface BabelOptions {
|
|
|
12
14
|
/**
|
|
13
15
|
* The babel config file to use.
|
|
14
16
|
*
|
|
15
|
-
* - `undefined`: auto-detect
|
|
16
|
-
* config
|
|
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`).
|
|
17
20
|
* - a string: use that config file.
|
|
18
21
|
* - `false`: ignore config files entirely.
|
|
19
22
|
*
|
|
@@ -46,14 +49,55 @@ export interface BabelOptions {
|
|
|
46
49
|
};
|
|
47
50
|
}
|
|
48
51
|
|
|
52
|
+
/** Tried in order, so an ESM config wins over a JSON one of the same name. */
|
|
53
|
+
const PUBLISH_CONFIG_EXTENSIONS = ["mjs", "cjs", "js", "mts", "cts", "ts", "json"];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Tried in order: next to package.json first, then `config/` for libraries that
|
|
57
|
+
* keep their build configuration out of the package root.
|
|
58
|
+
*/
|
|
59
|
+
const PUBLISH_CONFIG_DIRECTORIES = [".", "config"];
|
|
60
|
+
|
|
49
61
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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.
|
|
73
|
+
*/
|
|
74
|
+
function detectPublishConfigFile(): string | undefined {
|
|
75
|
+
for (const directory of PUBLISH_CONFIG_DIRECTORIES) {
|
|
76
|
+
for (const extension of PUBLISH_CONFIG_EXTENSIONS) {
|
|
77
|
+
const candidate = resolve(directory, `babel.publish.config.${extension}`);
|
|
78
|
+
|
|
79
|
+
if (existsSync(candidate)) return candidate;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
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.
|
|
52
92
|
*
|
|
53
93
|
* The sync API evaluates the config file; on node 24+ that works for native
|
|
54
94
|
* ESM configs too (require(esm)).
|
|
55
95
|
*/
|
|
56
96
|
function detectConfigFile(): string | undefined {
|
|
97
|
+
const publishConfig = detectPublishConfigFile();
|
|
98
|
+
|
|
99
|
+
if (publishConfig) return publishConfig;
|
|
100
|
+
|
|
57
101
|
const partial = loadPartialConfigSync({ cwd: process.cwd() });
|
|
58
102
|
|
|
59
103
|
return partial?.config ?? undefined;
|
|
@@ -112,9 +156,10 @@ function defaultPlugins(templateTransforms?: Transform[]): PluginItem[] {
|
|
|
112
156
|
* to only the files that actually need it (template-tag, decorators, template
|
|
113
157
|
* imports). Everything else stays on the native transform.
|
|
114
158
|
*
|
|
115
|
-
* The library's own babel config is used when it exists
|
|
116
|
-
* `
|
|
117
|
-
* config
|
|
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.
|
|
118
163
|
*
|
|
119
164
|
* Libraries default to `babelHelpers: "bundled"` so the emitted output is
|
|
120
165
|
* self-contained.
|
|
@@ -55,12 +55,15 @@ function resolveTsconfigPath(option: string | boolean | undefined): string | und
|
|
|
55
55
|
* sources -- isolated declarations then constrain exactly the code that gets
|
|
56
56
|
* declarations emitted for it, and nothing else.
|
|
57
57
|
*
|
|
58
|
-
* Projects with no tsconfig (JavaScript libraries
|
|
59
|
-
*
|
|
60
|
-
*
|
|
58
|
+
* Projects with no tsconfig (JavaScript libraries) or without the `typescript`
|
|
59
|
+
* package have no declarations to emit, so there is nothing to check.
|
|
60
|
+
* `tsconfig: false` is different: it is an explicit opt-out, and combined with
|
|
61
|
+
* declaration emit it produces a confusing "Source file not found" from the
|
|
62
|
+
* tsc-based pipeline, so it errors here instead.
|
|
61
63
|
*/
|
|
62
64
|
export function emberIsolatedDeclarations(): Plugin {
|
|
63
65
|
let tsconfigOption: string | boolean | undefined;
|
|
66
|
+
let emitsDeclarations = true;
|
|
64
67
|
|
|
65
68
|
return {
|
|
66
69
|
name: "ember:isolated-declarations",
|
|
@@ -69,9 +72,31 @@ export function emberIsolatedDeclarations(): Plugin {
|
|
|
69
72
|
// -- i.e. the cwd's tsconfig.json, the only thing it could mean there.
|
|
70
73
|
tsdownConfig(config: UserConfig) {
|
|
71
74
|
tsconfigOption = config.tsconfig;
|
|
75
|
+
// `emberConfig()` turns dts on unless the library turned it off, so an
|
|
76
|
+
// explicit `false` is the only "no declarations" signal. Hook order
|
|
77
|
+
// between plugins isn't guaranteed, so don't rely on it having run.
|
|
78
|
+
emitsDeclarations = config.dts !== false;
|
|
72
79
|
},
|
|
73
80
|
|
|
74
81
|
async buildStart() {
|
|
82
|
+
if (tsconfigOption === false) {
|
|
83
|
+
if (!emitsDeclarations) return;
|
|
84
|
+
|
|
85
|
+
// Without a tsconfig there is no `isolatedDeclarations`, so tsdown
|
|
86
|
+
// falls back to the tsc-based declaration pipeline -- which reads
|
|
87
|
+
// source files from disk and dies with "Source file not found" on
|
|
88
|
+
// every `.gts`/`.gjs`, since those only exist compiled in the module
|
|
89
|
+
// graph. Say so here instead of leaving that to be decoded.
|
|
90
|
+
this.error(
|
|
91
|
+
`\`tsconfig: false\` cannot be combined with declaration emit.\n\n` +
|
|
92
|
+
`Declarations are emitted by the isolated-declarations pipeline, which needs a ` +
|
|
93
|
+
`tsconfig setting "compilerOptions.isolatedDeclarations": true -- it is the only ` +
|
|
94
|
+
`pipeline that can see compiled .gts/.gjs modules.\n\n` +
|
|
95
|
+
`Either point tsdown's \`tsconfig\` option at such a config, or set \`dts: false\` ` +
|
|
96
|
+
`if this library ships no types.`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
75
100
|
const tsconfigPath = resolveTsconfigPath(tsconfigOption);
|
|
76
101
|
|
|
77
102
|
if (!tsconfigPath || !existsSync(tsconfigPath)) return;
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { describe, it, beforeAll, afterAll, expect } from "vitest";
|
|
2
|
+
import { generate, listFiles } from "#test-helpers";
|
|
3
|
+
import { writeLibrarySource } from "./library-src-fixtures.ts";
|
|
4
|
+
import { execa } from "execa";
|
|
5
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
import type { Project } from "ember.nvp";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A library whose package also holds dev-only code (a demo app, in-package
|
|
12
|
+
* tests) keeps two of each build config: a permissive `tsconfig.json` and
|
|
13
|
+
* `babel.config.js` that editors and `tsc --noEmit` use over everything, and
|
|
14
|
+
* publish-only counterparts covering just what gets published. Either publish
|
|
15
|
+
* config can sit in the package root or in `config/`.
|
|
16
|
+
*
|
|
17
|
+
* These tests generate a real library, put the configs in each of those
|
|
18
|
+
* places, and run the real `pnpm build`.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A development babel config: strips TypeScript, but leaves `<template>` as the
|
|
23
|
+
* `@ember/template-compiler` call content-tag produced. Publishing this output
|
|
24
|
+
* would tie the artifact to one exact ember-source.
|
|
25
|
+
*/
|
|
26
|
+
const devBabelConfig = `export default {
|
|
27
|
+
plugins: [
|
|
28
|
+
["@babel/plugin-transform-typescript", { allExtensions: true, allowDeclareFields: true }],
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A publish babel config: compiles templates to \`precompileTemplate\`, which is
|
|
35
|
+
* what a published library ships (the consuming app does the final compile).
|
|
36
|
+
*/
|
|
37
|
+
const publishBabelConfig = `export default {
|
|
38
|
+
plugins: [
|
|
39
|
+
["@babel/plugin-transform-typescript", { allExtensions: true, allowDeclareFields: true }],
|
|
40
|
+
["babel-plugin-ember-template-compilation", { targetFormat: "hbs" }],
|
|
41
|
+
["module:decorator-transforms", { runtime: { import: "decorator-transforms/runtime-esm" } }],
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The `tsconfig.json` such a library actually has: it covers the demo app as
|
|
48
|
+
* well as `src`, and deliberately has no `isolatedDeclarations` -- dev-only
|
|
49
|
+
* code shouldn't be held to a declaration-emit constraint. If the build reads
|
|
50
|
+
* this file rather than the publish tsconfig, the guard fails it, which is what
|
|
51
|
+
* makes every assertion below meaningful.
|
|
52
|
+
*/
|
|
53
|
+
const devTsconfig = `{
|
|
54
|
+
"extends": "@ember/library-tsconfig",
|
|
55
|
+
"include": ["src", "demo-app"],
|
|
56
|
+
"compilerOptions": {
|
|
57
|
+
"lib": ["esnext", "dom", "dom.iterable"],
|
|
58
|
+
"types": ["ember-source/types", "@glint/ember-tsc/types"]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Relative paths in a tsconfig resolve against the file itself, so the copy
|
|
65
|
+
* kept in `config/` has to reach back out to `src`.
|
|
66
|
+
*/
|
|
67
|
+
function publishTsconfig(directory: "." | "config"): string {
|
|
68
|
+
const src = directory === "config" ? "../src" : "./src";
|
|
69
|
+
|
|
70
|
+
return `{
|
|
71
|
+
"extends": "@ember/library-tsconfig",
|
|
72
|
+
"include": ["${src}"],
|
|
73
|
+
"compilerOptions": {
|
|
74
|
+
"rootDir": "${src}",
|
|
75
|
+
"lib": ["esnext", "dom", "dom.iterable"],
|
|
76
|
+
"isolatedDeclarations": true,
|
|
77
|
+
"types": ["ember-source/types", "@glint/ember-tsc/types"]
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** `tsconfigOption` is a literal expression: a quoted path, or `false`. */
|
|
84
|
+
function tsdownConfig(tsconfigOption: string): string {
|
|
85
|
+
return `import { defineConfig } from "tsdown";
|
|
86
|
+
import { ember } from "@nullvoxpopuli/ember-rolldown";
|
|
87
|
+
|
|
88
|
+
export default defineConfig({
|
|
89
|
+
entry: ["./src/index.ts"],
|
|
90
|
+
tsconfig: ${tsconfigOption},
|
|
91
|
+
plugins: [ember()],
|
|
92
|
+
});
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Every place a publish config can be found, so each layout starts clean. */
|
|
97
|
+
const BABEL_CONFIG_PATHS = ["babel.publish.config.js", "config/babel.publish.config.js"];
|
|
98
|
+
const TSCONFIG_PATHS = [
|
|
99
|
+
"tsconfig.publish.json",
|
|
100
|
+
"config/tsconfig.publish.json",
|
|
101
|
+
"config/tsconfig.json",
|
|
102
|
+
"tsconfig.base.json",
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
describe("publish configs", () => {
|
|
106
|
+
let project: Project;
|
|
107
|
+
|
|
108
|
+
beforeAll(async () => {
|
|
109
|
+
project = await generate({ type: "library", name: "my-lib", layers: ["typescript"] });
|
|
110
|
+
|
|
111
|
+
await writeLibrarySource(project, "typescript");
|
|
112
|
+
|
|
113
|
+
// A library that writes its own babel config declares the plugins it uses.
|
|
114
|
+
const manifest = JSON.parse((await project.read("package.json"))!);
|
|
115
|
+
|
|
116
|
+
manifest.devDependencies["@babel/core"] = "^7.28.10";
|
|
117
|
+
manifest.devDependencies["@babel/plugin-transform-typescript"] = "^7.28.10";
|
|
118
|
+
manifest.devDependencies["babel-plugin-ember-template-compilation"] = "^4.0.0";
|
|
119
|
+
|
|
120
|
+
await writeFile(
|
|
121
|
+
join(project.directory, "package.json"),
|
|
122
|
+
JSON.stringify(manifest, null, 2),
|
|
123
|
+
"utf-8",
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Dev-only source: exports with no type annotations, which the publish
|
|
127
|
+
// tsconfig must not cover.
|
|
128
|
+
await mkdir(join(project.directory, "demo-app"), { recursive: true });
|
|
129
|
+
await writeFile(
|
|
130
|
+
join(project.directory, "demo-app", "app.gts"),
|
|
131
|
+
`export const anything = { not: "annotated" };\nexport default <template>demo</template>;\n`,
|
|
132
|
+
"utf-8",
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
await writeFile(join(project.directory, "tsconfig.json"), devTsconfig, "utf-8");
|
|
136
|
+
await writeFile(join(project.directory, "babel.config.js"), devBabelConfig, "utf-8");
|
|
137
|
+
|
|
138
|
+
const install = await execa("pnpm install", { cwd: project.directory, shell: true });
|
|
139
|
+
expect(install.exitCode).toBe(0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
afterAll(async () => {
|
|
143
|
+
if (process.env.CI) return;
|
|
144
|
+
|
|
145
|
+
await rm(project.directory, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
/** Clears every publish config, so one test's layout can't satisfy the next. */
|
|
149
|
+
async function resetConfigs() {
|
|
150
|
+
for (const relative of [...BABEL_CONFIG_PATHS, ...TSCONFIG_PATHS]) {
|
|
151
|
+
await rm(join(project.directory, relative), { force: true });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await rm(join(project.directory, "dist"), { recursive: true, force: true });
|
|
155
|
+
await mkdir(join(project.directory, "config"), { recursive: true });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function runBuild() {
|
|
159
|
+
const build = await execa("pnpm build", {
|
|
160
|
+
cwd: project.directory,
|
|
161
|
+
shell: true,
|
|
162
|
+
reject: false,
|
|
163
|
+
all: true,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return { build, output: await project.read("dist/index.js") };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Puts the publish configs in the requested places and runs the real build. */
|
|
170
|
+
async function buildWith({
|
|
171
|
+
babel,
|
|
172
|
+
tsconfig,
|
|
173
|
+
}: {
|
|
174
|
+
babel: "." | "config" | false;
|
|
175
|
+
tsconfig: "." | "config";
|
|
176
|
+
}) {
|
|
177
|
+
await resetConfigs();
|
|
178
|
+
|
|
179
|
+
if (babel !== false) {
|
|
180
|
+
const relative = join(babel, "babel.publish.config.js");
|
|
181
|
+
|
|
182
|
+
await writeFile(join(project.directory, relative), publishBabelConfig, "utf-8");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const tsconfigRelative = join(tsconfig, "tsconfig.publish.json");
|
|
186
|
+
|
|
187
|
+
await writeFile(join(project.directory, tsconfigRelative), publishTsconfig(tsconfig), "utf-8");
|
|
188
|
+
await writeFile(
|
|
189
|
+
join(project.directory, "tsdown.config.js"),
|
|
190
|
+
tsdownConfig(`"./${tsconfigRelative}"`),
|
|
191
|
+
"utf-8",
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
return await runBuild();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (const babel of [".", "config"] as const) {
|
|
198
|
+
for (const tsconfig of [".", "config"] as const) {
|
|
199
|
+
const where = (directory: string) => (directory === "." ? "the package root" : "config/");
|
|
200
|
+
|
|
201
|
+
it(`builds with the babel config in ${where(babel)} and the tsconfig in ${where(tsconfig)}`, async () => {
|
|
202
|
+
const { build, output } = await buildWith({ babel, tsconfig });
|
|
203
|
+
|
|
204
|
+
expect(build.exitCode, build.all).toBe(0);
|
|
205
|
+
|
|
206
|
+
// The publish babel config ran: templates came out as
|
|
207
|
+
// precompileTemplate, which only its template-compilation step emits.
|
|
208
|
+
expect(output).toContain("precompileTemplate");
|
|
209
|
+
expect(output).not.toContain("createTemplateFactory");
|
|
210
|
+
|
|
211
|
+
// ...and the dev config, which has no template compilation, did not.
|
|
212
|
+
expect(output).not.toContain("@ember/template-compiler");
|
|
213
|
+
|
|
214
|
+
// Declarations emitted, so the guard read the publish tsconfig: the
|
|
215
|
+
// dev tsconfig.json covering demo-app/ has no isolatedDeclarations and
|
|
216
|
+
// would have failed the build.
|
|
217
|
+
expect(await listFiles(join(project.directory, "dist"))).toContain("index.d.ts");
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
it("falls back to the dev babel config when there is no publish one", async () => {
|
|
223
|
+
const { build, output } = await buildWith({ babel: false, tsconfig: "." });
|
|
224
|
+
|
|
225
|
+
expect(build.exitCode, build.all).toBe(0);
|
|
226
|
+
|
|
227
|
+
// Nothing compiled the template, so content-tag's output survives -- the
|
|
228
|
+
// artifact you don't want to publish, which is why the preference exists.
|
|
229
|
+
expect(output).toContain("@ember/template-compiler");
|
|
230
|
+
expect(output).not.toContain("precompileTemplate");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Writes `files` into the project and builds with `tsconfigOption` as tsdown's
|
|
235
|
+
* `tsconfig`, for the cases about *which* tsconfig the guard ends up reading.
|
|
236
|
+
* The publish babel config is always present so these fail for tsconfig
|
|
237
|
+
* reasons only.
|
|
238
|
+
*/
|
|
239
|
+
async function buildWithTsconfigOption(
|
|
240
|
+
tsconfigOption: string,
|
|
241
|
+
files: Record<string, string> = {},
|
|
242
|
+
) {
|
|
243
|
+
await resetConfigs();
|
|
244
|
+
|
|
245
|
+
await writeFile(
|
|
246
|
+
join(project.directory, "babel.publish.config.js"),
|
|
247
|
+
publishBabelConfig,
|
|
248
|
+
"utf-8",
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
for (const [relative, contents] of Object.entries(files)) {
|
|
252
|
+
await writeFile(join(project.directory, relative), contents, "utf-8");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
await writeFile(
|
|
256
|
+
join(project.directory, "tsdown.config.js"),
|
|
257
|
+
tsdownConfig(tsconfigOption),
|
|
258
|
+
"utf-8",
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
return await runBuild();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
it("fails when the publish tsconfig itself lacks isolatedDeclarations", async () => {
|
|
265
|
+
const { build } = await buildWithTsconfigOption(`"./tsconfig.publish.json"`, {
|
|
266
|
+
"tsconfig.publish.json": publishTsconfig(".").replace(
|
|
267
|
+
`"isolatedDeclarations": true,\n `,
|
|
268
|
+
"",
|
|
269
|
+
),
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
expect(build.exitCode).not.toBe(0);
|
|
273
|
+
expect(build.all).toContain('"compilerOptions.isolatedDeclarations": true');
|
|
274
|
+
expect(build.all).toContain("tsconfig.publish.json");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("accepts a directory, reading the tsconfig.json inside it", async () => {
|
|
278
|
+
const { build } = await buildWithTsconfigOption(`"./config"`, {
|
|
279
|
+
"config/tsconfig.json": publishTsconfig("config"),
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
expect(build.exitCode, build.all).toBe(0);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("takes the flag from an extends chain", async () => {
|
|
286
|
+
const { build } = await buildWithTsconfigOption(`"./tsconfig.publish.json"`, {
|
|
287
|
+
"tsconfig.base.json": `{ "compilerOptions": { "isolatedDeclarations": true } }\n`,
|
|
288
|
+
"tsconfig.publish.json": publishTsconfig(".").replace(
|
|
289
|
+
`"extends": "@ember/library-tsconfig"`,
|
|
290
|
+
`"extends": ["@ember/library-tsconfig", "./tsconfig.base.json"]`,
|
|
291
|
+
),
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
expect(build.exitCode, build.all).toBe(0);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("rejects tsconfig: false while declarations are on", async () => {
|
|
298
|
+
// No tsconfig means no isolatedDeclarations, so tsdown falls back to the
|
|
299
|
+
// tsc-based pipeline, which can't see compiled .gts and dies with
|
|
300
|
+
// "Source file not found". Fail with something actionable instead.
|
|
301
|
+
const { build } = await buildWithTsconfigOption("false");
|
|
302
|
+
|
|
303
|
+
expect(build.exitCode).not.toBe(0);
|
|
304
|
+
expect(build.all).toContain("`tsconfig: false` cannot be combined with declaration emit");
|
|
305
|
+
expect(build.all).not.toContain("Source file not found");
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("allows tsconfig: false for a library that ships no types", async () => {
|
|
309
|
+
await resetConfigs();
|
|
310
|
+
|
|
311
|
+
await writeFile(
|
|
312
|
+
join(project.directory, "babel.publish.config.js"),
|
|
313
|
+
publishBabelConfig,
|
|
314
|
+
"utf-8",
|
|
315
|
+
);
|
|
316
|
+
await writeFile(
|
|
317
|
+
join(project.directory, "tsdown.config.js"),
|
|
318
|
+
`import { defineConfig } from "tsdown";
|
|
319
|
+
import { ember } from "@nullvoxpopuli/ember-rolldown";
|
|
320
|
+
|
|
321
|
+
export default defineConfig({
|
|
322
|
+
entry: ["./src/index.ts"],
|
|
323
|
+
tsconfig: false,
|
|
324
|
+
dts: false,
|
|
325
|
+
plugins: [ember()],
|
|
326
|
+
});
|
|
327
|
+
`,
|
|
328
|
+
"utf-8",
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
const { build } = await runBuild();
|
|
332
|
+
|
|
333
|
+
expect(build.exitCode, build.all).toBe(0);
|
|
334
|
+
expect(await listFiles(join(project.directory, "dist"))).not.toContain("index.d.ts");
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("keeps dev-only source out of the build", async () => {
|
|
338
|
+
// demo-app/app.gts would be an isolated-declarations error, and builds fine
|
|
339
|
+
// only because the publish tsconfig doesn't cover it.
|
|
340
|
+
const { build } = await buildWith({ babel: ".", tsconfig: "." });
|
|
341
|
+
|
|
342
|
+
expect(build.exitCode, build.all).toBe(0);
|
|
343
|
+
expect(await listFiles(join(project.directory, "dist"))).toMatchInlineSnapshot(`
|
|
344
|
+
[
|
|
345
|
+
"index.d.ts",
|
|
346
|
+
"index.d.ts.map",
|
|
347
|
+
"index.js",
|
|
348
|
+
"index.js.map",
|
|
349
|
+
]
|
|
350
|
+
`);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { build } from "tsdown";
|
|
5
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
-
|
|
7
|
-
import { ember } from "../index.ts";
|
|
8
|
-
|
|
9
|
-
let restoreCwd: (() => void) | undefined;
|
|
10
|
-
|
|
11
|
-
afterEach(() => {
|
|
12
|
-
restoreCwd?.();
|
|
13
|
-
restoreCwd = undefined;
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Runs a real tsdown build of a single trivial entry in a temp dir, so the
|
|
18
|
-
* isolated-declarations guard reads the tsconfig files written by the test the
|
|
19
|
-
* same way it does under the tsdown CLI. Returns the build's rejection, or
|
|
20
|
-
* `undefined` when it succeeded.
|
|
21
|
-
*/
|
|
22
|
-
async function buildFixture(
|
|
23
|
-
files: Record<string, string>,
|
|
24
|
-
tsconfig?: string | boolean,
|
|
25
|
-
): Promise<Error | undefined> {
|
|
26
|
-
const dir = await mkdtemp(path.join(tmpdir(), "ember-rolldown-isolated-declarations-"));
|
|
27
|
-
|
|
28
|
-
const withDefaults: Record<string, string> = {
|
|
29
|
-
"package.json": JSON.stringify({ name: "fixture", version: "0.0.0", type: "module" }),
|
|
30
|
-
"src/index.ts": `export const greeting: string = 'hello';`,
|
|
31
|
-
...files,
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
for (const [relative, source] of Object.entries(withDefaults)) {
|
|
35
|
-
const full = path.join(dir, relative);
|
|
36
|
-
await mkdir(path.dirname(full), { recursive: true });
|
|
37
|
-
await writeFile(full, source, "utf8");
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const previousCwd = process.cwd();
|
|
41
|
-
process.chdir(dir);
|
|
42
|
-
restoreCwd = () => process.chdir(previousCwd);
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
await build({
|
|
46
|
-
entry: ["./src/index.ts"],
|
|
47
|
-
config: false,
|
|
48
|
-
logLevel: "silent",
|
|
49
|
-
tsconfig,
|
|
50
|
-
plugins: [ember()],
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
return undefined;
|
|
54
|
-
} catch (error) {
|
|
55
|
-
return error as Error;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const withFlag = JSON.stringify({
|
|
60
|
-
compilerOptions: { isolatedDeclarations: true, declaration: true },
|
|
61
|
-
});
|
|
62
|
-
const withoutFlag = JSON.stringify({ compilerOptions: { declaration: true } });
|
|
63
|
-
|
|
64
|
-
describe("isolated declarations guard", () => {
|
|
65
|
-
it("errors when the cwd tsconfig.json does not set the flag", async () => {
|
|
66
|
-
const error = await buildFixture({ "tsconfig.json": withoutFlag });
|
|
67
|
-
|
|
68
|
-
expect(error?.message).toContain(`must set "compilerOptions.isolatedDeclarations": true`);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("passes when the cwd tsconfig.json sets the flag", async () => {
|
|
72
|
-
expect(await buildFixture({ "tsconfig.json": withFlag })).toBeUndefined();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("checks the tsconfig the `tsconfig` option points at, not tsconfig.json", async () => {
|
|
76
|
-
// The shape a library with in-package dev code wants: a permissive
|
|
77
|
-
// tsconfig.json for editors and `tsc --noEmit` over everything, and a
|
|
78
|
-
// publish-only tsconfig -- the one the build uses -- carrying the flag.
|
|
79
|
-
const error = await buildFixture(
|
|
80
|
-
{ "tsconfig.json": withoutFlag, "tsconfig.publish.json": withFlag },
|
|
81
|
-
"./tsconfig.publish.json",
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
expect(error).toBeUndefined();
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("errors when the tsconfig the `tsconfig` option points at lacks the flag", async () => {
|
|
88
|
-
const error = await buildFixture(
|
|
89
|
-
{ "tsconfig.json": withFlag, "tsconfig.publish.json": withoutFlag },
|
|
90
|
-
"./tsconfig.publish.json",
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
expect(error?.message).toContain("tsconfig.publish.json");
|
|
94
|
-
expect(error?.message).toContain(`must set "compilerOptions.isolatedDeclarations": true`);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("resolves a directory `tsconfig` option to the tsconfig.json inside it", async () => {
|
|
98
|
-
const error = await buildFixture(
|
|
99
|
-
{ "tsconfig.json": withFlag, "config/tsconfig.json": withoutFlag },
|
|
100
|
-
"./config",
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
expect(error?.message).toContain(`must set "compilerOptions.isolatedDeclarations": true`);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("skips the check when there is no tsconfig at all", async () => {
|
|
107
|
-
expect(await buildFixture({})).toBeUndefined();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("skips the check when tsconfig is disabled", async () => {
|
|
111
|
-
expect(await buildFixture({ "tsconfig.json": withoutFlag }, false)).toBeUndefined();
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("inherits the flag through an extends chain", async () => {
|
|
115
|
-
const error = await buildFixture({
|
|
116
|
-
"tsconfig.base.json": withFlag,
|
|
117
|
-
"tsconfig.json": JSON.stringify({ extends: "./tsconfig.base.json" }),
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
expect(error).toBeUndefined();
|
|
121
|
-
});
|
|
122
|
-
});
|