ember.nvp 1.2.0 → 1.3.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 +5 -9
- package/CHANGELOG.md +42 -0
- package/README.md +5 -0
- package/package.json +1 -1
- package/packages/rolldown/README.md +54 -5
- package/packages/rolldown/package.json +1 -1
- package/packages/rolldown/src/app-reexports.test.ts +35 -2
- package/packages/rolldown/src/app-reexports.ts +14 -3
- package/packages/rolldown/src/externals.test.ts +118 -0
- package/packages/rolldown/src/externals.ts +45 -0
- package/packages/rolldown/src/isolated-declarations.test.ts +122 -0
- package/packages/rolldown/src/isolated-declarations.ts +56 -8
- package/packages/rolldown/src/transform.test.ts +53 -2
- package/packages/rolldown/src/transform.ts +23 -2
- package/src/layers/github-actions/index.js +3 -0
- package/src/layers/prettier/index.js +12 -0
- package/src/layers/readme/index.js +176 -0
- package/src/layers/typescript/index.js +11 -0
- package/src/layers/vitest/index.js +11 -0
- package/src/utils/project.js +7 -0
- package/src/utils/types.ts +5 -0
- package/test/ember.nvp/layer-readme.test.ts +195 -0
- package/test/ember.nvp/layers.test.ts +1 -0
package/.release-plan.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"solution": {
|
|
3
3
|
"ember.nvp": {
|
|
4
4
|
"impact": "minor",
|
|
5
|
-
"oldVersion": "1.
|
|
6
|
-
"newVersion": "1.
|
|
5
|
+
"oldVersion": "1.2.0",
|
|
6
|
+
"newVersion": "1.3.0",
|
|
7
7
|
"tagName": "latest",
|
|
8
8
|
"constraints": [
|
|
9
9
|
{
|
|
@@ -18,17 +18,13 @@
|
|
|
18
18
|
},
|
|
19
19
|
"@nullvoxpopuli/ember-rolldown": {
|
|
20
20
|
"impact": "minor",
|
|
21
|
-
"oldVersion": "2.
|
|
22
|
-
"newVersion": "2.
|
|
21
|
+
"oldVersion": "2.4.0",
|
|
22
|
+
"newVersion": "2.5.0",
|
|
23
23
|
"tagName": "latest",
|
|
24
24
|
"constraints": [
|
|
25
25
|
{
|
|
26
26
|
"impact": "minor",
|
|
27
27
|
"reason": "Appears in changelog section :rocket: Enhancement"
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
"impact": "patch",
|
|
31
|
-
"reason": "Appears in changelog section :bug: Bug Fix"
|
|
32
28
|
}
|
|
33
29
|
],
|
|
34
30
|
"pkgJSONPath": "./packages/rolldown/package.json"
|
|
@@ -37,5 +33,5 @@
|
|
|
37
33
|
"oldVersion": "1.1.0"
|
|
38
34
|
}
|
|
39
35
|
},
|
|
40
|
-
"description": "## Release (2026-
|
|
36
|
+
"description": "## Release (2026-08-03)\n\n* ember.nvp 1.3.0 (minor)\n* @nullvoxpopuli/ember-rolldown 2.5.0 (minor)\n\n#### :rocket: Enhancement\n* `ember.nvp`\n * [#106](https://github.com/NullVoxPopuli/ember.nvp/pull/106) README layer ([@tcjr](https://github.com/tcjr))\n* `@nullvoxpopuli/ember-rolldown`\n * [#107](https://github.com/NullVoxPopuli/ember.nvp/pull/107) rolldown: check the tsconfig the build uses, not always tsconfig.json ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n\n#### Committers: 2\n- Tom Carter ([@tcjr](https://github.com/tcjr))\n- [@NullVoxPopuli](https://github.com/NullVoxPopuli)\n"
|
|
41
37
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Release (2026-08-03)
|
|
4
|
+
|
|
5
|
+
* ember.nvp 1.3.0 (minor)
|
|
6
|
+
* @nullvoxpopuli/ember-rolldown 2.5.0 (minor)
|
|
7
|
+
|
|
8
|
+
#### :rocket: Enhancement
|
|
9
|
+
* `ember.nvp`
|
|
10
|
+
* [#106](https://github.com/NullVoxPopuli/ember.nvp/pull/106) README layer ([@tcjr](https://github.com/tcjr))
|
|
11
|
+
* `@nullvoxpopuli/ember-rolldown`
|
|
12
|
+
* [#107](https://github.com/NullVoxPopuli/ember.nvp/pull/107) rolldown: check the tsconfig the build uses, not always tsconfig.json ([@NullVoxPopuli](https://github.com/NullVoxPopuli))
|
|
13
|
+
|
|
14
|
+
#### Committers: 2
|
|
15
|
+
- Tom Carter ([@tcjr](https://github.com/tcjr))
|
|
16
|
+
- [@NullVoxPopuli](https://github.com/NullVoxPopuli)
|
|
17
|
+
|
|
18
|
+
## Release (2026-07-24)
|
|
19
|
+
|
|
20
|
+
* @nullvoxpopuli/ember-rolldown 2.4.0 (minor)
|
|
21
|
+
|
|
22
|
+
#### :rocket: Enhancement
|
|
23
|
+
* `@nullvoxpopuli/ember-rolldown`
|
|
24
|
+
* [#104](https://github.com/NullVoxPopuli/ember.nvp/pull/104) rolldown: support .gts/.gjs build entries ([@NullVoxPopuli](https://github.com/NullVoxPopuli))
|
|
25
|
+
|
|
26
|
+
#### Committers: 1
|
|
27
|
+
- [@NullVoxPopuli](https://github.com/NullVoxPopuli)
|
|
28
|
+
|
|
29
|
+
## Release (2026-07-22)
|
|
30
|
+
|
|
31
|
+
* @nullvoxpopuli/ember-rolldown 2.3.0 (minor)
|
|
32
|
+
|
|
33
|
+
#### :rocket: Enhancement
|
|
34
|
+
* `@nullvoxpopuli/ember-rolldown`
|
|
35
|
+
* [#101](https://github.com/NullVoxPopuli/ember.nvp/pull/101) appReexports: accept string[] include and options as second argument ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))
|
|
36
|
+
|
|
37
|
+
#### :bug: Bug Fix
|
|
38
|
+
* `@nullvoxpopuli/ember-rolldown`
|
|
39
|
+
* [#103](https://github.com/NullVoxPopuli/ember.nvp/pull/103) rolldown: externalize the modules ember-source provides (renamed-modules) ([@NullVoxPopuli](https://github.com/NullVoxPopuli))
|
|
40
|
+
|
|
41
|
+
#### Committers: 2
|
|
42
|
+
- @NullVoxPopuli's reduced-access machine account for AI usage ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))
|
|
43
|
+
- [@NullVoxPopuli](https://github.com/NullVoxPopuli)
|
|
44
|
+
|
|
3
45
|
## Release (2026-07-22)
|
|
4
46
|
|
|
5
47
|
* ember.nvp 1.2.0 (minor)
|
package/README.md
CHANGED
|
@@ -198,6 +198,11 @@ export default {
|
|
|
198
198
|
|
|
199
199
|
await packageJson.addScripts({ "my-script": 'echo "Hello"' }, project.directory);
|
|
200
200
|
},
|
|
201
|
+
|
|
202
|
+
// Optionally add something to the README.md file
|
|
203
|
+
readme() {
|
|
204
|
+
return "### My Feature\n\nHere's what my feature brings to the table.";
|
|
205
|
+
},
|
|
201
206
|
};
|
|
202
207
|
```
|
|
203
208
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ember.nvp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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",
|
|
@@ -58,11 +58,30 @@ A `babel.config.js` is optional. Without one, `ember()` compiles templates
|
|
|
58
58
|
[decorator-transforms](https://github.com/ef4/decorator-transforms)), and
|
|
59
59
|
TypeScript; with one, your config runs instead.
|
|
60
60
|
|
|
61
|
+
### Entrypoints
|
|
62
|
+
|
|
63
|
+
Entries may be any extension — `.ts`, `.js`, `.gts`, `.gjs`:
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
export default defineConfig({
|
|
67
|
+
entry: ["./src/index.ts", "./src/components/menu.gts"],
|
|
68
|
+
plugins: [ember()],
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Whatever the source extension, the emitted `.js` and `.d.ts` paths mirror the
|
|
73
|
+
entry paths (that's how tsdown's dts support works): the example emits
|
|
74
|
+
`dist/index.js` + `dist/index.d.ts` and `dist/components/menu.js` +
|
|
75
|
+
`dist/components/menu.d.ts`. Type imports resolve against those emitted
|
|
76
|
+
paths, so your entries — together with your `exports` map — _are_ your
|
|
77
|
+
public API: a module that isn't an entry has no stable `dist/` path of its
|
|
78
|
+
own and is only reachable through the entrypoints that re-export it.
|
|
79
|
+
|
|
61
80
|
### Declarations
|
|
62
81
|
|
|
63
82
|
Declarations are emitted with isolated declarations — the only declaration
|
|
64
83
|
pipeline that can see `<template>` (`.gts`/`.gjs`) modules, which exist only
|
|
65
|
-
inside the bundler's module graph.
|
|
84
|
+
inside the bundler's module graph. The tsconfig your build uses must enable it
|
|
66
85
|
(`ember()` errors otherwise):
|
|
67
86
|
|
|
68
87
|
```jsonc
|
|
@@ -82,6 +101,33 @@ import type { TOC } from "@ember/component/template-only";
|
|
|
82
101
|
export const Badge: TOC<BadgeSignature> = <template>...</template>;
|
|
83
102
|
```
|
|
84
103
|
|
|
104
|
+
#### Keeping the constraint off dev-only code
|
|
105
|
+
|
|
106
|
+
`isolatedDeclarations` is a constraint on how _published_ code is written, so it
|
|
107
|
+
should cover only the code you emit declarations for. If your package also holds
|
|
108
|
+
dev-only code — a demo app, in-package tests — putting the flag on the single
|
|
109
|
+
`tsconfig.json` that covers everything would force explicit annotations on demo
|
|
110
|
+
components and test helpers that never get a `.d.ts` emitted for them.
|
|
111
|
+
|
|
112
|
+
The tsconfig `ember()` checks is the one
|
|
113
|
+
[tsdown's `tsconfig` option](https://tsdown.dev/options/tsconfig) points at, so
|
|
114
|
+
point the build at a publish-only config and leave `tsconfig.json` alone for
|
|
115
|
+
editors and `tsc --noEmit`:
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
// tsdown.config.js
|
|
119
|
+
export default defineConfig({
|
|
120
|
+
entry: ["./src/index.ts"],
|
|
121
|
+
// include: ["src/**/*"], isolatedDeclarations: true
|
|
122
|
+
tsconfig: "./tsconfig.publish.json",
|
|
123
|
+
plugins: [ember()],
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The tradeoff: isolated-declaration errors in `src` then surface when you build
|
|
128
|
+
rather than in your editor, since the editor uses `tsconfig.json`. Run the build
|
|
129
|
+
(or `tsc --noEmit -p tsconfig.publish.json`) in CI so nothing lands unchecked.
|
|
130
|
+
|
|
85
131
|
## CSS
|
|
86
132
|
|
|
87
133
|
Components that import co-located CSS (`import './popup.css'`) need
|
|
@@ -140,8 +186,9 @@ it.
|
|
|
140
186
|
|
|
141
187
|
`ember()` returns an array of rolldown plugins:
|
|
142
188
|
|
|
143
|
-
- **`emberIsolatedDeclarations()`** — errors when
|
|
144
|
-
|
|
189
|
+
- **`emberIsolatedDeclarations()`** — errors when the tsconfig the build uses
|
|
190
|
+
(tsdown's `tsconfig` option, defaulting to `tsconfig.json`) is present without
|
|
191
|
+
`isolatedDeclarations: true`.
|
|
145
192
|
- **`emberExternals()`** — keeps your `dependencies`, `peerDependencies`, and
|
|
146
193
|
the ember virtual packages (e.g. `@ember/component`, `@glimmer/tracking`, the
|
|
147
194
|
template compiler) external, so the consuming app resolves them.
|
|
@@ -194,12 +241,14 @@ export default defineConfig({
|
|
|
194
241
|
|
|
195
242
|
With no arguments, top-level services (`services/*`) are re-exported — under
|
|
196
243
|
strict mode, components and helpers are imported, but services are still
|
|
197
|
-
injected by name. A string
|
|
198
|
-
full control:
|
|
244
|
+
injected by name. A string or array of strings is the include glob(s),
|
|
245
|
+
optionally followed by the remaining options; an object gives full control:
|
|
199
246
|
|
|
200
247
|
```js
|
|
201
248
|
appReexports(); // services/*
|
|
202
249
|
appReexports("components/**"); // one include glob
|
|
250
|
+
appReexports(["services/*", "helpers/*"]);
|
|
251
|
+
appReexports("components/**", { exclude: ["components/-private/**"] });
|
|
203
252
|
appReexports({ include: ["services/*", "helpers/*"], exclude: [...] });
|
|
204
253
|
```
|
|
205
254
|
|
|
@@ -51,13 +51,16 @@ describe("appReexports", () => {
|
|
|
51
51
|
await rm(projectDir, { recursive: true, force: true });
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
async function build(
|
|
54
|
+
async function build(
|
|
55
|
+
include?: string | string[] | AppReexportsOptions,
|
|
56
|
+
options?: Omit<AppReexportsOptions, "include">,
|
|
57
|
+
) {
|
|
55
58
|
const bundle = await rolldown({
|
|
56
59
|
input: {
|
|
57
60
|
"components/foo": "./src/components/foo.js",
|
|
58
61
|
"utils/math": "./src/utils/math.js",
|
|
59
62
|
},
|
|
60
|
-
plugins: [appReexports(options)],
|
|
63
|
+
plugins: [appReexports(include, options)],
|
|
61
64
|
});
|
|
62
65
|
|
|
63
66
|
await bundle.write({ dir: "dist" });
|
|
@@ -100,6 +103,36 @@ describe("appReexports", () => {
|
|
|
100
103
|
expect(Object.keys(manifest["ember-addon"]["app-js"])).toEqual(["./components/foo.js"]);
|
|
101
104
|
});
|
|
102
105
|
|
|
106
|
+
it("accepts an array of include globs", async () => {
|
|
107
|
+
await build(["components/**", "utils/**"]);
|
|
108
|
+
|
|
109
|
+
const manifest = await readJson("package.json");
|
|
110
|
+
|
|
111
|
+
expect(Object.keys(manifest["ember-addon"]["app-js"])).toEqual([
|
|
112
|
+
"./components/foo.js",
|
|
113
|
+
"./utils/math.js",
|
|
114
|
+
]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("accepts options as the second argument alongside a string or array include", async () => {
|
|
118
|
+
await build("**/*.js", { exclude: ["utils/**"] });
|
|
119
|
+
|
|
120
|
+
let manifest = await readJson("package.json");
|
|
121
|
+
|
|
122
|
+
expect(Object.keys(manifest["ember-addon"]["app-js"])).toEqual(["./components/foo.js"]);
|
|
123
|
+
|
|
124
|
+
await build(["components/**", "utils/**"], {
|
|
125
|
+
mapFilename: (name) => name.replace("math", "arithmetic"),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
manifest = await readJson("package.json");
|
|
129
|
+
|
|
130
|
+
expect(manifest["ember-addon"]["app-js"]).toEqual({
|
|
131
|
+
"./components/foo.js": "./dist/_app_/components/foo.js",
|
|
132
|
+
"./utils/arithmetic.js": "./dist/_app_/utils/arithmetic.js",
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
103
136
|
it("defaults to top-level services", async () => {
|
|
104
137
|
await mkdir(join(projectDir, "src/services/nested"), { recursive: true });
|
|
105
138
|
await writeFile(join(projectDir, "src/services/session.js"), "export default class {}\n");
|
|
@@ -65,16 +65,27 @@ export interface AppReexportsOptions {
|
|
|
65
65
|
* ```
|
|
66
66
|
*
|
|
67
67
|
* With no arguments, top-level services (`services/*`) are re-exported. A
|
|
68
|
-
* string
|
|
68
|
+
* string or array of strings is the include glob(s), optionally followed by
|
|
69
|
+
* the remaining options; an object gives full control:
|
|
69
70
|
*
|
|
70
71
|
* ```js
|
|
71
72
|
* appReexports(); // services/*
|
|
72
73
|
* appReexports("components/**"); // one include glob
|
|
74
|
+
* appReexports(["services/*", "helpers/*"]);
|
|
75
|
+
* appReexports("components/**", { exclude: ["components/-private/**"] });
|
|
73
76
|
* appReexports({ include: ["services/*", "helpers/*"], exclude: [...] });
|
|
74
77
|
* ```
|
|
75
78
|
*/
|
|
76
|
-
export function appReexports(
|
|
77
|
-
|
|
79
|
+
export function appReexports(
|
|
80
|
+
includeOrOptions: string | string[] | AppReexportsOptions = {},
|
|
81
|
+
options: Omit<AppReexportsOptions, "include"> = {},
|
|
82
|
+
): Plugin {
|
|
83
|
+
const resolved =
|
|
84
|
+
typeof includeOrOptions === "string"
|
|
85
|
+
? { ...options, include: [includeOrOptions] }
|
|
86
|
+
: Array.isArray(includeOrOptions)
|
|
87
|
+
? { ...options, include: includeOrOptions }
|
|
88
|
+
: includeOrOptions;
|
|
78
89
|
const include = resolved.include ?? ["services/*"];
|
|
79
90
|
|
|
80
91
|
return {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { rolldown } from "rolldown";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { emberExternals } from "./externals.ts";
|
|
8
|
+
|
|
9
|
+
let restoreCwd: (() => void) | undefined;
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
restoreCwd?.();
|
|
13
|
+
restoreCwd = undefined;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* An ember-source stub whose `ember-addon.renamed-modules` provides
|
|
18
|
+
* `@glimmer/runtime` (file-path keys, like the real manifest).
|
|
19
|
+
*/
|
|
20
|
+
const EMBER_SOURCE_STUB = {
|
|
21
|
+
name: "ember-source",
|
|
22
|
+
version: "0.0.0",
|
|
23
|
+
"ember-addon": {
|
|
24
|
+
"renamed-modules": {
|
|
25
|
+
"@glimmer/runtime/index.js": "ember-source/@glimmer/runtime/index.js",
|
|
26
|
+
"@ember/-internals/glimmer/index.js": "ember-source/@ember/-internals/glimmer/index.js",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Writes a fixture package (manifest + optional stub ember-source in its
|
|
33
|
+
* node_modules) and cds into it — the plugin reads both from the library's
|
|
34
|
+
* own context, like the tsdown CLI.
|
|
35
|
+
*/
|
|
36
|
+
async function fixture(manifest: object, { emberSource = true } = {}): Promise<string> {
|
|
37
|
+
const dir = await mkdtemp(path.join(tmpdir(), "ember-rolldown-externals-"));
|
|
38
|
+
await writeFile(path.join(dir, "package.json"), JSON.stringify(manifest));
|
|
39
|
+
|
|
40
|
+
if (emberSource) {
|
|
41
|
+
const stubDir = path.join(dir, "node_modules/ember-source");
|
|
42
|
+
await mkdir(stubDir, { recursive: true });
|
|
43
|
+
await writeFile(path.join(stubDir, "package.json"), JSON.stringify(EMBER_SOURCE_STUB));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const previousCwd = process.cwd();
|
|
47
|
+
process.chdir(dir);
|
|
48
|
+
restoreCwd = () => process.chdir(previousCwd);
|
|
49
|
+
|
|
50
|
+
return dir;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Runs the plugin's own hooks directly against the current fixture. */
|
|
54
|
+
function resolveWith(source: string): unknown {
|
|
55
|
+
const plugin = emberExternals();
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
|
|
57
|
+
(plugin.buildStart as any).call({ addWatchFile() {} });
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
|
|
59
|
+
return (plugin.resolveId as any).handler(source);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe("emberExternals", () => {
|
|
63
|
+
it("externalizes declared dependencies and the ember virtual packages", async () => {
|
|
64
|
+
await fixture({ dependencies: { "some-dep": "*" } });
|
|
65
|
+
expect(resolveWith("some-dep")).toBe(false);
|
|
66
|
+
expect(resolveWith("@ember/component")).toBe(false);
|
|
67
|
+
expect(resolveWith("@glimmer/tracking")).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("externalizes the modules ember-source provides via renamed-modules", async () => {
|
|
71
|
+
// Private API like @glimmer/runtime exists only inside the app's
|
|
72
|
+
// ember-source (its ember-addon.renamed-modules) — @embroider/core's
|
|
73
|
+
// emberVirtualPackages doesn't list it.
|
|
74
|
+
await fixture({});
|
|
75
|
+
expect(resolveWith("@glimmer/runtime")).toBe(false);
|
|
76
|
+
expect(resolveWith("@ember/-internals/glimmer")).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("does not blanket-externalize @glimmer packages ember-source doesn't provide", async () => {
|
|
80
|
+
// e.g. @glimmer/component is a real package a library may want bundled;
|
|
81
|
+
// only declared deps / virtual packages / renamed-modules are external.
|
|
82
|
+
await fixture({});
|
|
83
|
+
expect(resolveWith("@glimmer/not-provided")).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("leaves undeclared packages alone, with or without ember-source", async () => {
|
|
87
|
+
await fixture({});
|
|
88
|
+
expect(resolveWith("left-pad")).toBeUndefined();
|
|
89
|
+
|
|
90
|
+
await fixture({}, { emberSource: false });
|
|
91
|
+
expect(resolveWith("left-pad")).toBeUndefined();
|
|
92
|
+
// No ember-source in the graph -> no renamed-modules to consult.
|
|
93
|
+
expect(resolveWith("@glimmer/runtime")).toBeUndefined();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("keeps a renamed-modules import external through a real build", async () => {
|
|
97
|
+
const dir = await fixture({ name: "build-fixture" });
|
|
98
|
+
await writeFile(
|
|
99
|
+
path.join(dir, "index.ts"),
|
|
100
|
+
`import { curry } from '@glimmer/runtime';\nexport const curried: unknown = curry;\n`,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const warnings: string[] = [];
|
|
104
|
+
const build = await rolldown({
|
|
105
|
+
input: path.join(dir, "index.ts"),
|
|
106
|
+
plugins: [emberExternals()],
|
|
107
|
+
onwarn(warning) {
|
|
108
|
+
warnings.push(warning.code ?? String(warning));
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
const { output } = await build.generate({ format: "es" });
|
|
112
|
+
|
|
113
|
+
expect(warnings.filter((code) => code === "UNRESOLVED_IMPORT")).toEqual([]);
|
|
114
|
+
|
|
115
|
+
const code = output.filter((chunk) => "code" in chunk).map((chunk) => chunk.code)[0] ?? "";
|
|
116
|
+
expect(code).toContain(`from "@glimmer/runtime"`);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
|
|
3
5
|
import type { Plugin } from "rolldown";
|
|
4
6
|
|
|
@@ -33,6 +35,38 @@ function resolvableDependencies(): Set<string> {
|
|
|
33
35
|
return deps;
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
/**
|
|
39
|
+
* The module names ember-source provides by renaming them into itself — the
|
|
40
|
+
* keys of its `ember-addon.renamed-modules`, normalized from file paths to
|
|
41
|
+
* import specifiers (`@glimmer/runtime/index.js` → `@glimmer/runtime`).
|
|
42
|
+
*
|
|
43
|
+
* This is the authoritative list of ember's provided modules, including
|
|
44
|
+
* private API that @embroider/core's emberVirtualPackages doesn't cover
|
|
45
|
+
* (e.g. `@glimmer/runtime`). It deliberately does NOT include real packages
|
|
46
|
+
* like `@glimmer/component`, which a library may want bundled.
|
|
47
|
+
*
|
|
48
|
+
* Resolved from the library's own dependency graph; empty when ember-source
|
|
49
|
+
* isn't resolvable there.
|
|
50
|
+
*/
|
|
51
|
+
function emberSourceRenamedModules(): Set<string> {
|
|
52
|
+
const provided = new Set<string>();
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const require = createRequire(path.resolve("package.json"));
|
|
56
|
+
const manifest = require("ember-source/package.json") as {
|
|
57
|
+
"ember-addon"?: { "renamed-modules"?: Record<string, string> };
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
for (const key of Object.keys(manifest["ember-addon"]?.["renamed-modules"] ?? {})) {
|
|
61
|
+
provided.add(key.replace(/\.js$/, "").replace(/\/index$/, ""));
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
// The library doesn't have ember-source in its graph; nothing to provide.
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return provided;
|
|
68
|
+
}
|
|
69
|
+
|
|
36
70
|
/**
|
|
37
71
|
* Keeps the library's declared dependencies and the ember virtual packages
|
|
38
72
|
* (things like `@ember/component`, `@glimmer/tracking`, the template compiler,
|
|
@@ -41,6 +75,7 @@ function resolvableDependencies(): Set<string> {
|
|
|
41
75
|
*/
|
|
42
76
|
export function emberExternals(): Plugin {
|
|
43
77
|
let deps: Set<string>;
|
|
78
|
+
let renamedModules: Set<string>;
|
|
44
79
|
|
|
45
80
|
return {
|
|
46
81
|
name: "ember:externals",
|
|
@@ -48,6 +83,7 @@ export function emberExternals(): Plugin {
|
|
|
48
83
|
buildStart() {
|
|
49
84
|
this.addWatchFile("package.json");
|
|
50
85
|
deps = resolvableDependencies();
|
|
86
|
+
renamedModules = emberSourceRenamedModules();
|
|
51
87
|
},
|
|
52
88
|
|
|
53
89
|
resolveId: {
|
|
@@ -76,6 +112,15 @@ export function emberExternals(): Plugin {
|
|
|
76
112
|
return false;
|
|
77
113
|
}
|
|
78
114
|
|
|
115
|
+
// Modules ember-source provides by renaming them into itself
|
|
116
|
+
// (`ember-addon.renamed-modules`) — private API like
|
|
117
|
+
// `@glimmer/runtime` that only exists inside the app's ember-source.
|
|
118
|
+
// Matched on the full specifier: the renamed modules are module
|
|
119
|
+
// paths, not packages.
|
|
120
|
+
if (renamedModules.has(source)) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
79
124
|
return undefined;
|
|
80
125
|
},
|
|
81
126
|
},
|
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
});
|
|
@@ -1,10 +1,39 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
3
|
|
|
4
4
|
import type { Plugin } from "rolldown";
|
|
5
|
+
import type { UserConfig } from "tsdown";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
+
* Resolve the tsconfig the build will actually use, mirroring tsdown's own
|
|
9
|
+
* `tsconfig` option (`string | boolean`):
|
|
10
|
+
*
|
|
11
|
+
* - `undefined` / `true`: `tsconfig.json` in the cwd.
|
|
12
|
+
* - a path to a file: that file.
|
|
13
|
+
* - a path to a directory: `tsconfig.json` inside it.
|
|
14
|
+
* - `false`: no tsconfig at all.
|
|
15
|
+
*
|
|
16
|
+
* Under plain rolldown there is no such option, so the cwd's `tsconfig.json`
|
|
17
|
+
* is all there is.
|
|
18
|
+
*/
|
|
19
|
+
function resolveTsconfigPath(option: string | boolean | undefined): string | undefined {
|
|
20
|
+
if (option === false) return undefined;
|
|
21
|
+
|
|
22
|
+
if (typeof option === "string") {
|
|
23
|
+
const asPath = resolve(option);
|
|
24
|
+
|
|
25
|
+
if (existsSync(asPath) && statSync(asPath).isDirectory()) {
|
|
26
|
+
return resolve(asPath, "tsconfig.json");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return asPath;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return resolve("tsconfig.json");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Errors the build when the tsconfig it builds with does not set
|
|
8
37
|
* `isolatedDeclarations: true`.
|
|
9
38
|
*
|
|
10
39
|
* This is required: declarations for `.gts`/`.gjs` (template tag) modules can
|
|
@@ -18,18 +47,34 @@ import type { Plugin } from "rolldown";
|
|
|
18
47
|
* same rules the build enforces (exported values need explicit type
|
|
19
48
|
* annotations, e.g. `export const X: TOC<Sig> = <template>...`).
|
|
20
49
|
*
|
|
21
|
-
*
|
|
22
|
-
* `
|
|
23
|
-
*
|
|
50
|
+
* The tsconfig checked is the one tsdown's `tsconfig` option points at, not
|
|
51
|
+
* necessarily `tsconfig.json`. A library whose package also holds dev-only code
|
|
52
|
+
* (a demo app, in-package tests) can therefore keep a permissive
|
|
53
|
+
* `tsconfig.json` covering everything for editors and `tsc --noEmit`, and point
|
|
54
|
+
* the build at a `tsconfig.publish.json` that covers only the publishable
|
|
55
|
+
* sources -- isolated declarations then constrain exactly the code that gets
|
|
56
|
+
* declarations emitted for it, and nothing else.
|
|
57
|
+
*
|
|
58
|
+
* Projects with no tsconfig (JavaScript libraries, or `tsconfig: false`) or
|
|
59
|
+
* without the `typescript` package have no declarations to emit, so there is
|
|
60
|
+
* nothing to check.
|
|
24
61
|
*/
|
|
25
62
|
export function emberIsolatedDeclarations(): Plugin {
|
|
63
|
+
let tsconfigOption: string | boolean | undefined;
|
|
64
|
+
|
|
26
65
|
return {
|
|
27
66
|
name: "ember:isolated-declarations",
|
|
28
67
|
|
|
68
|
+
// Not called under plain rolldown, which leaves `tsconfigOption` undefined
|
|
69
|
+
// -- i.e. the cwd's tsconfig.json, the only thing it could mean there.
|
|
70
|
+
tsdownConfig(config: UserConfig) {
|
|
71
|
+
tsconfigOption = config.tsconfig;
|
|
72
|
+
},
|
|
73
|
+
|
|
29
74
|
async buildStart() {
|
|
30
|
-
const tsconfigPath =
|
|
75
|
+
const tsconfigPath = resolveTsconfigPath(tsconfigOption);
|
|
31
76
|
|
|
32
|
-
if (!existsSync(tsconfigPath)) return;
|
|
77
|
+
if (!tsconfigPath || !existsSync(tsconfigPath)) return;
|
|
33
78
|
|
|
34
79
|
let ts: typeof import("typescript");
|
|
35
80
|
|
|
@@ -57,7 +102,10 @@ export function emberIsolatedDeclarations(): Plugin {
|
|
|
57
102
|
`isolated-declarations pipeline, which reads compiled modules from the bundler's ` +
|
|
58
103
|
`module graph -- the tsc-based pipeline reads from disk and cannot see them.\n\n` +
|
|
59
104
|
`Isolated declarations require exported values to have explicit type annotations, ` +
|
|
60
|
-
`e.g. \`export const X: TOC<Sig> = <template
|
|
105
|
+
`e.g. \`export const X: TOC<Sig> = <template>...\`.\n\n` +
|
|
106
|
+
`If this tsconfig also covers dev-only code (a demo app, in-package tests) that ` +
|
|
107
|
+
`should not be constrained this way, point tsdown's \`tsconfig\` option at a ` +
|
|
108
|
+
`publish-only tsconfig that sets the flag and covers just the published sources.`,
|
|
61
109
|
);
|
|
62
110
|
}
|
|
63
111
|
},
|
|
@@ -13,7 +13,10 @@ import { emberTransform } from "./transform.ts";
|
|
|
13
13
|
* `.gts`/`.gjs`/`.ts` handling and doesn't need real dependencies installed.
|
|
14
14
|
* Returns the concatenated code of every emitted chunk.
|
|
15
15
|
*/
|
|
16
|
-
async function bundle(
|
|
16
|
+
async function bundle(
|
|
17
|
+
files: Record<string, string>,
|
|
18
|
+
{ input = ["index.ts"] }: { input?: string[] } = {},
|
|
19
|
+
): Promise<string> {
|
|
17
20
|
const dir = await mkdtemp(path.join(tmpdir(), "ember-rolldown-build-"));
|
|
18
21
|
|
|
19
22
|
for (const [relative, source] of Object.entries(files)) {
|
|
@@ -25,7 +28,7 @@ async function bundle(files: Record<string, string>): Promise<string> {
|
|
|
25
28
|
const isLocal = (id: string) => id.startsWith(".") || id.startsWith("/") || path.isAbsolute(id);
|
|
26
29
|
|
|
27
30
|
const build = await rolldown({
|
|
28
|
-
input: path.join(dir,
|
|
31
|
+
input: input.map((relative) => path.join(dir, relative)),
|
|
29
32
|
plugins: [emberTransform()],
|
|
30
33
|
external: (id) => !isLocal(id),
|
|
31
34
|
onwarn() {},
|
|
@@ -171,6 +174,54 @@ describe("emberTransform (full plugin via rolldown)", () => {
|
|
|
171
174
|
`);
|
|
172
175
|
});
|
|
173
176
|
|
|
177
|
+
it("compiles a .gts module used directly as an entry (no importer)", async () => {
|
|
178
|
+
const code = await bundle(
|
|
179
|
+
{
|
|
180
|
+
// Both an entry .gts and a .ts entry importing it: the shared module
|
|
181
|
+
// must resolve to ONE id (the entry resolution realpaths, matching
|
|
182
|
+
// rolldown's own resolver) so it lands in the entry chunk and the
|
|
183
|
+
// index chunk imports it, rather than duplicating the code.
|
|
184
|
+
"index.ts": `export { default as Foo } from './foo.gts';`,
|
|
185
|
+
"foo.gts": [
|
|
186
|
+
`import Component from '@glimmer/component';`,
|
|
187
|
+
`export default class Foo extends Component {`,
|
|
188
|
+
` <template>Entry</template>`,
|
|
189
|
+
`}`,
|
|
190
|
+
].join("\n"),
|
|
191
|
+
},
|
|
192
|
+
{ input: ["index.ts", "foo.gts"] },
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
expect(code).not.toContain("<template>");
|
|
196
|
+
expect(code).toContain("template(");
|
|
197
|
+
expect(code).toContain(`from "@glimmer/component"`);
|
|
198
|
+
// deduplicated: the class body appears exactly once, in foo's own chunk
|
|
199
|
+
expect(code.match(/extends Component/g)).toHaveLength(1);
|
|
200
|
+
expect(code).toContain(`from "./foo.js"`);
|
|
201
|
+
|
|
202
|
+
expect(code).toMatchInlineSnapshot(`
|
|
203
|
+
"import { template } from "@ember/template-compiler";
|
|
204
|
+
import Component from "@glimmer/component";
|
|
205
|
+
//#region foo.ts
|
|
206
|
+
var Foo = class extends Component {
|
|
207
|
+
static {
|
|
208
|
+
template(\`Entry\`, {
|
|
209
|
+
component: this,
|
|
210
|
+
eval() {
|
|
211
|
+
return eval(arguments[0]);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
//#endregion
|
|
217
|
+
export { Foo as default };
|
|
218
|
+
|
|
219
|
+
import Foo from "./foo.js";
|
|
220
|
+
export { Foo };
|
|
221
|
+
"
|
|
222
|
+
`);
|
|
223
|
+
});
|
|
224
|
+
|
|
174
225
|
it("emits sourcemaps for the specifier rewrite (no SOURCEMAP_BROKEN warnings)", async () => {
|
|
175
226
|
const dir = await mkdtemp(path.join(tmpdir(), "ember-rolldown-map-"));
|
|
176
227
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Preprocessor } from "content-tag";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
3
3
|
import { readFile, writeFile, readdir } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import type { Plugin } from "rolldown";
|
|
@@ -70,7 +70,28 @@ export function emberTransform(): Plugin {
|
|
|
70
70
|
resolveId: {
|
|
71
71
|
order: "pre",
|
|
72
72
|
handler(id, importer) {
|
|
73
|
-
|
|
73
|
+
// Entries have no importer: their ids come straight from the build
|
|
74
|
+
// config (tsdown expands entry globs to on-disk paths). Resolve a
|
|
75
|
+
// `.gts`/`.gjs` entry to the same virtual `.ts`/`.js` id an imported
|
|
76
|
+
// module gets, so the load hook compiles it via content-tag instead
|
|
77
|
+
// of the raw `<template>` source hitting the parser. Entries may be
|
|
78
|
+
// any extension — the emitted `.js`/`.d.ts` paths mirror the entry
|
|
79
|
+
// paths either way (that's how tsdown's dts support works).
|
|
80
|
+
if (!importer) {
|
|
81
|
+
if (!id.endsWith(".gts") && !id.endsWith(".gjs")) return null;
|
|
82
|
+
if (!existsSync(id)) return null;
|
|
83
|
+
|
|
84
|
+
// rolldown's default resolver realpaths entry ids (e.g. macOS
|
|
85
|
+
// /var -> /private/var). Match it, or this same file imported from
|
|
86
|
+
// another entry resolves to a second module id and gets duplicated
|
|
87
|
+
// into both chunks.
|
|
88
|
+
const fileName = realpathSync(path.resolve(id));
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
id: fileName.replace(/\.gts$/, ".ts").replace(/\.gjs$/, ".js"),
|
|
92
|
+
meta: { fileName },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
74
95
|
|
|
75
96
|
const fileName = path.join(path.dirname(importer), id);
|
|
76
97
|
|
|
@@ -62,4 +62,16 @@ export default {
|
|
|
62
62
|
|
|
63
63
|
return true;
|
|
64
64
|
},
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {import('#utils/project.js').Project} project
|
|
68
|
+
*/
|
|
69
|
+
readme(project) {
|
|
70
|
+
return `### Prettier
|
|
71
|
+
|
|
72
|
+
Code formatting is managed with [Prettier](https://prettier.io/).
|
|
73
|
+
|
|
74
|
+
- \`${project.runPrefix} format\` - Format code
|
|
75
|
+
- \`${project.runPrefix} lint:prettier\` - Check code formatting`;
|
|
76
|
+
},
|
|
65
77
|
};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { formatLabel } from "#utils/cli.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {import('#utils/project.js').Project} project
|
|
8
|
+
* @param {string} layerDocsMarkdown
|
|
9
|
+
*/
|
|
10
|
+
function appReadme(project, layerDocsMarkdown) {
|
|
11
|
+
return `# ${project.name}
|
|
12
|
+
|
|
13
|
+
An Ember application created with \`ember.nvp\`.
|
|
14
|
+
|
|
15
|
+
## Getting Started
|
|
16
|
+
|
|
17
|
+
### Prerequisites
|
|
18
|
+
|
|
19
|
+
- Node.js >= 24
|
|
20
|
+
- ${project.packageManager}
|
|
21
|
+
|
|
22
|
+
### Installation
|
|
23
|
+
|
|
24
|
+
~~~sh
|
|
25
|
+
${project.packageManager} install
|
|
26
|
+
~~~
|
|
27
|
+
|
|
28
|
+
### Development
|
|
29
|
+
|
|
30
|
+
To start the local development server:
|
|
31
|
+
|
|
32
|
+
~~~sh
|
|
33
|
+
${project.runPrefix} dev
|
|
34
|
+
~~~
|
|
35
|
+
|
|
36
|
+
or
|
|
37
|
+
|
|
38
|
+
~~~sh
|
|
39
|
+
${project.runPrefix} start
|
|
40
|
+
~~~
|
|
41
|
+
${layerDocsMarkdown}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {import('#utils/project.js').Project} project
|
|
46
|
+
* @param {string} layerDocsMarkdown
|
|
47
|
+
*/
|
|
48
|
+
function extensionReadme(project, layerDocsMarkdown) {
|
|
49
|
+
return `# ${project.name}
|
|
50
|
+
|
|
51
|
+
A browser extension using Ember created with \`ember.nvp\`.
|
|
52
|
+
|
|
53
|
+
## Getting Started
|
|
54
|
+
|
|
55
|
+
### Prerequisites
|
|
56
|
+
|
|
57
|
+
- Node.js >= 24
|
|
58
|
+
- ${project.packageManager}
|
|
59
|
+
|
|
60
|
+
### Development
|
|
61
|
+
|
|
62
|
+
To start development:
|
|
63
|
+
|
|
64
|
+
~~~sh
|
|
65
|
+
${project.runPrefix} dev
|
|
66
|
+
~~~
|
|
67
|
+
|
|
68
|
+
or
|
|
69
|
+
|
|
70
|
+
~~~sh
|
|
71
|
+
${project.runPrefix} start
|
|
72
|
+
~~~
|
|
73
|
+
|
|
74
|
+
### Building
|
|
75
|
+
|
|
76
|
+
To build the library:
|
|
77
|
+
|
|
78
|
+
~~~sh
|
|
79
|
+
${project.runPrefix} build
|
|
80
|
+
~~~
|
|
81
|
+
|
|
82
|
+
or
|
|
83
|
+
|
|
84
|
+
~~~sh
|
|
85
|
+
${project.runPrefix} build:watch
|
|
86
|
+
~~~
|
|
87
|
+
${layerDocsMarkdown}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {import('#utils/project.js').Project} project
|
|
92
|
+
* @param {string} layerDocsMarkdown
|
|
93
|
+
*/
|
|
94
|
+
function libraryReadme(project, layerDocsMarkdown) {
|
|
95
|
+
return `# ${project.name}
|
|
96
|
+
|
|
97
|
+
An Ember library/addon created with \`ember.nvp\`.
|
|
98
|
+
|
|
99
|
+
## Getting Started
|
|
100
|
+
|
|
101
|
+
### Prerequisites
|
|
102
|
+
|
|
103
|
+
- Node.js >= 24
|
|
104
|
+
- ${project.packageManager}
|
|
105
|
+
|
|
106
|
+
### Development & Building
|
|
107
|
+
|
|
108
|
+
To build the library:
|
|
109
|
+
|
|
110
|
+
~~~sh
|
|
111
|
+
${project.runPrefix} build
|
|
112
|
+
~~~
|
|
113
|
+
${layerDocsMarkdown}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export default {
|
|
117
|
+
label: formatLabel("README.md", "generate project documentation"),
|
|
118
|
+
hint: "project README",
|
|
119
|
+
|
|
120
|
+
async defaultValue() {
|
|
121
|
+
return true;
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {import('#utils/project.js').Project} project
|
|
126
|
+
*/
|
|
127
|
+
async run(project) {
|
|
128
|
+
// Collect layer docs
|
|
129
|
+
const layerDocs = [];
|
|
130
|
+
for (const layer of project.desires.layers) {
|
|
131
|
+
if (layer.name === "readme" || !layer.readme) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let doc;
|
|
136
|
+
if (typeof layer.readme === "function") {
|
|
137
|
+
doc = await layer.readme(project);
|
|
138
|
+
} else if (typeof layer.readme === "string") {
|
|
139
|
+
doc = layer.readme;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (doc && typeof doc === "string" && doc.trim().length > 0) {
|
|
143
|
+
layerDocs.push(doc.trim());
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let layerDocsMarkdown = "";
|
|
148
|
+
if (layerDocs.length > 0) {
|
|
149
|
+
layerDocsMarkdown = `\n## Features & Tooling\n\n${layerDocs.join("\n\n")}\n`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Merge the layers' snippets into the main readme template
|
|
153
|
+
let content;
|
|
154
|
+
switch (project.type) {
|
|
155
|
+
case "extension":
|
|
156
|
+
content = extensionReadme(project, layerDocsMarkdown);
|
|
157
|
+
break;
|
|
158
|
+
case "library":
|
|
159
|
+
content = libraryReadme(project, layerDocsMarkdown);
|
|
160
|
+
break;
|
|
161
|
+
default:
|
|
162
|
+
content = appReadme(project, layerDocsMarkdown);
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const targetPath = join(project.directory, "README.md");
|
|
167
|
+
await writeFile(targetPath, `${content.trim()}\n`, "utf-8");
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @param {import('#utils/project.js').Project} project
|
|
172
|
+
*/
|
|
173
|
+
async isSetup(project) {
|
|
174
|
+
return existsSync(join(project.directory, "README.md"));
|
|
175
|
+
},
|
|
176
|
+
};
|
|
@@ -120,6 +120,17 @@ export default {
|
|
|
120
120
|
|
|
121
121
|
return reasons.length === 0;
|
|
122
122
|
},
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {import('#utils/project.js').Project} project
|
|
126
|
+
*/
|
|
127
|
+
readme(project) {
|
|
128
|
+
return `### TypeScript
|
|
129
|
+
|
|
130
|
+
This project uses TypeScript and Glint for static type checking.
|
|
131
|
+
|
|
132
|
+
- \`${project.runPrefix} lint:types\` - Typecheck code with Glint/TypeScript`;
|
|
133
|
+
},
|
|
123
134
|
};
|
|
124
135
|
|
|
125
136
|
/**
|
|
@@ -101,4 +101,15 @@ export default {
|
|
|
101
101
|
|
|
102
102
|
return reasons.length === 0;
|
|
103
103
|
},
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {import('#utils/project.js').Project} project
|
|
107
|
+
*/
|
|
108
|
+
readme(project) {
|
|
109
|
+
return `### Testing (Vitest)
|
|
110
|
+
|
|
111
|
+
Testing is configured with [Vitest](https://vitest.dev/).
|
|
112
|
+
|
|
113
|
+
- \`${project.runPrefix} test\` - Run tests`;
|
|
114
|
+
},
|
|
104
115
|
};
|
package/src/utils/project.js
CHANGED
package/src/utils/types.ts
CHANGED
|
@@ -13,6 +13,11 @@ export interface Layer {
|
|
|
13
13
|
* deselect it).
|
|
14
14
|
*/
|
|
15
15
|
defaultValue?: (projectType: ProjectType) => unknown;
|
|
16
|
+
/**
|
|
17
|
+
* Optional README documentation snippet or function. Whatever is returned
|
|
18
|
+
* will show up in the generated project's README file.
|
|
19
|
+
*/
|
|
20
|
+
readme?: string | ((project: Project) => string | undefined | Promise<string | undefined>);
|
|
16
21
|
/**
|
|
17
22
|
* The function that applies the codemod
|
|
18
23
|
*
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { generate } from "#test-helpers";
|
|
3
|
+
import { rm } from "node:fs/promises";
|
|
4
|
+
import type { Project } from "ember.nvp";
|
|
5
|
+
|
|
6
|
+
describe("layer: readme", () => {
|
|
7
|
+
const dirs: string[] = [];
|
|
8
|
+
|
|
9
|
+
afterAll(async () => {
|
|
10
|
+
if (process.env.CI) return;
|
|
11
|
+
|
|
12
|
+
for (const dir of dirs) {
|
|
13
|
+
await rm(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("App project README generation", () => {
|
|
18
|
+
let project: Project;
|
|
19
|
+
|
|
20
|
+
beforeAll(async () => {
|
|
21
|
+
project = await generate({
|
|
22
|
+
type: "app",
|
|
23
|
+
name: "my-app",
|
|
24
|
+
packageManager: "pnpm",
|
|
25
|
+
layers: ["readme", "git", "prettier", "typescript"],
|
|
26
|
+
});
|
|
27
|
+
dirs.push(project.directory);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("generates README.md with project info and selected layer docs", async () => {
|
|
31
|
+
expect(await project.read("README.md")).toMatchInlineSnapshot(`
|
|
32
|
+
"# my-app
|
|
33
|
+
|
|
34
|
+
An Ember application created with \`ember.nvp\`.
|
|
35
|
+
|
|
36
|
+
## Getting Started
|
|
37
|
+
|
|
38
|
+
### Prerequisites
|
|
39
|
+
|
|
40
|
+
- Node.js >= 24
|
|
41
|
+
- pnpm
|
|
42
|
+
|
|
43
|
+
### Installation
|
|
44
|
+
|
|
45
|
+
~~~sh
|
|
46
|
+
pnpm install
|
|
47
|
+
~~~
|
|
48
|
+
|
|
49
|
+
### Development
|
|
50
|
+
|
|
51
|
+
To start the local development server:
|
|
52
|
+
|
|
53
|
+
~~~sh
|
|
54
|
+
pnpm dev
|
|
55
|
+
~~~
|
|
56
|
+
|
|
57
|
+
or
|
|
58
|
+
|
|
59
|
+
~~~sh
|
|
60
|
+
pnpm start
|
|
61
|
+
~~~
|
|
62
|
+
|
|
63
|
+
## Features & Tooling
|
|
64
|
+
|
|
65
|
+
### Prettier
|
|
66
|
+
|
|
67
|
+
Code formatting is managed with [Prettier](https://prettier.io/).
|
|
68
|
+
|
|
69
|
+
- \`pnpm format\` - Format code
|
|
70
|
+
- \`pnpm lint:prettier\` - Check code formatting
|
|
71
|
+
|
|
72
|
+
### TypeScript
|
|
73
|
+
|
|
74
|
+
This project uses TypeScript and Glint for static type checking.
|
|
75
|
+
|
|
76
|
+
- \`pnpm lint:types\` - Typecheck code with Glint/TypeScript
|
|
77
|
+
"
|
|
78
|
+
`);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("Library project README generation with npm", () => {
|
|
83
|
+
let project: Project;
|
|
84
|
+
|
|
85
|
+
beforeAll(async () => {
|
|
86
|
+
project = await generate({
|
|
87
|
+
type: "library",
|
|
88
|
+
name: "my-lib",
|
|
89
|
+
packageManager: "npm",
|
|
90
|
+
layers: ["readme", "prettier"],
|
|
91
|
+
});
|
|
92
|
+
dirs.push(project.directory);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("generates README.md with npm-specific commands for library", async () => {
|
|
96
|
+
expect(await project.read("README.md")).toMatchInlineSnapshot(`
|
|
97
|
+
"# my-lib
|
|
98
|
+
|
|
99
|
+
An Ember library/addon created with \`ember.nvp\`.
|
|
100
|
+
|
|
101
|
+
## Getting Started
|
|
102
|
+
|
|
103
|
+
### Prerequisites
|
|
104
|
+
|
|
105
|
+
- Node.js >= 24
|
|
106
|
+
- npm
|
|
107
|
+
|
|
108
|
+
### Development & Building
|
|
109
|
+
|
|
110
|
+
To build the library:
|
|
111
|
+
|
|
112
|
+
~~~sh
|
|
113
|
+
npm run build
|
|
114
|
+
~~~
|
|
115
|
+
|
|
116
|
+
## Features & Tooling
|
|
117
|
+
|
|
118
|
+
### Prettier
|
|
119
|
+
|
|
120
|
+
Code formatting is managed with [Prettier](https://prettier.io/).
|
|
121
|
+
|
|
122
|
+
- \`npm run format\` - Format code
|
|
123
|
+
- \`npm run lint:prettier\` - Check code formatting
|
|
124
|
+
"
|
|
125
|
+
`);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("Extension project README generation", () => {
|
|
130
|
+
let project: Project;
|
|
131
|
+
|
|
132
|
+
beforeAll(async () => {
|
|
133
|
+
project = await generate({
|
|
134
|
+
type: "extension",
|
|
135
|
+
name: "my-extension",
|
|
136
|
+
packageManager: "pnpm",
|
|
137
|
+
layers: ["readme", "prettier"],
|
|
138
|
+
});
|
|
139
|
+
dirs.push(project.directory);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("generates README.md for browser extension project", async () => {
|
|
143
|
+
expect(await project.read("README.md")).toMatchInlineSnapshot(`
|
|
144
|
+
"# my-extension
|
|
145
|
+
|
|
146
|
+
A browser extension using Ember created with \`ember.nvp\`.
|
|
147
|
+
|
|
148
|
+
## Getting Started
|
|
149
|
+
|
|
150
|
+
### Prerequisites
|
|
151
|
+
|
|
152
|
+
- Node.js >= 24
|
|
153
|
+
- pnpm
|
|
154
|
+
|
|
155
|
+
### Development
|
|
156
|
+
|
|
157
|
+
To start development:
|
|
158
|
+
|
|
159
|
+
~~~sh
|
|
160
|
+
pnpm dev
|
|
161
|
+
~~~
|
|
162
|
+
|
|
163
|
+
or
|
|
164
|
+
|
|
165
|
+
~~~sh
|
|
166
|
+
pnpm start
|
|
167
|
+
~~~
|
|
168
|
+
|
|
169
|
+
### Building
|
|
170
|
+
|
|
171
|
+
To build the library:
|
|
172
|
+
|
|
173
|
+
~~~sh
|
|
174
|
+
pnpm build
|
|
175
|
+
~~~
|
|
176
|
+
|
|
177
|
+
or
|
|
178
|
+
|
|
179
|
+
~~~sh
|
|
180
|
+
pnpm build:watch
|
|
181
|
+
~~~
|
|
182
|
+
|
|
183
|
+
## Features & Tooling
|
|
184
|
+
|
|
185
|
+
### Prettier
|
|
186
|
+
|
|
187
|
+
Code formatting is managed with [Prettier](https://prettier.io/).
|
|
188
|
+
|
|
189
|
+
- \`pnpm format\` - Format code
|
|
190
|
+
- \`pnpm lint:prettier\` - Check code formatting
|
|
191
|
+
"
|
|
192
|
+
`);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
});
|