ember.nvp 1.2.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 +10 -6
- package/CHANGELOG.md +60 -0
- package/README.md +5 -0
- package/package.json +1 -1
- package/packages/rolldown/README.md +116 -8
- 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/babel.test.ts +125 -2
- package/packages/rolldown/src/babel.ts +52 -7
- package/packages/rolldown/src/externals.test.ts +118 -0
- package/packages/rolldown/src/externals.ts +45 -0
- package/packages/rolldown/src/isolated-declarations.ts +81 -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/test/ember.nvp/publish-configs.test.ts +352 -0
|
@@ -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
|
},
|
|
@@ -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,59 @@ 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 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.
|
|
24
63
|
*/
|
|
25
64
|
export function emberIsolatedDeclarations(): Plugin {
|
|
65
|
+
let tsconfigOption: string | boolean | undefined;
|
|
66
|
+
let emitsDeclarations = true;
|
|
67
|
+
|
|
26
68
|
return {
|
|
27
69
|
name: "ember:isolated-declarations",
|
|
28
70
|
|
|
71
|
+
// Not called under plain rolldown, which leaves `tsconfigOption` undefined
|
|
72
|
+
// -- i.e. the cwd's tsconfig.json, the only thing it could mean there.
|
|
73
|
+
tsdownConfig(config: UserConfig) {
|
|
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;
|
|
79
|
+
},
|
|
80
|
+
|
|
29
81
|
async buildStart() {
|
|
30
|
-
|
|
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
|
+
|
|
100
|
+
const tsconfigPath = resolveTsconfigPath(tsconfigOption);
|
|
31
101
|
|
|
32
|
-
if (!existsSync(tsconfigPath)) return;
|
|
102
|
+
if (!tsconfigPath || !existsSync(tsconfigPath)) return;
|
|
33
103
|
|
|
34
104
|
let ts: typeof import("typescript");
|
|
35
105
|
|
|
@@ -57,7 +127,10 @@ export function emberIsolatedDeclarations(): Plugin {
|
|
|
57
127
|
`isolated-declarations pipeline, which reads compiled modules from the bundler's ` +
|
|
58
128
|
`module graph -- the tsc-based pipeline reads from disk and cannot see them.\n\n` +
|
|
59
129
|
`Isolated declarations require exported values to have explicit type annotations, ` +
|
|
60
|
-
`e.g. \`export const X: TOC<Sig> = <template
|
|
130
|
+
`e.g. \`export const X: TOC<Sig> = <template>...\`.\n\n` +
|
|
131
|
+
`If this tsconfig also covers dev-only code (a demo app, in-package tests) that ` +
|
|
132
|
+
`should not be constrained this way, point tsdown's \`tsconfig\` option at a ` +
|
|
133
|
+
`publish-only tsconfig that sets the flag and covers just the published sources.`,
|
|
61
134
|
);
|
|
62
135
|
}
|
|
63
136
|
},
|
|
@@ -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