ember.nvp 1.1.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 -21
- package/CHANGELOG.md +59 -0
- package/README.md +5 -0
- package/package.json +1 -1
- package/packages/rolldown/README.md +96 -9
- 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 +17 -0
- package/packages/rolldown/src/babel.ts +21 -3
- package/packages/rolldown/src/dts.test.ts +238 -38
- 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 +88 -2
- package/packages/rolldown/src/transform.ts +87 -33
- 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/scoped-css-library.test.ts +211 -0
|
@@ -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() {},
|
|
@@ -170,4 +173,87 @@ describe("emberTransform (full plugin via rolldown)", () => {
|
|
|
170
173
|
"
|
|
171
174
|
`);
|
|
172
175
|
});
|
|
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
|
+
|
|
225
|
+
it("emits sourcemaps for the specifier rewrite (no SOURCEMAP_BROKEN warnings)", async () => {
|
|
226
|
+
const dir = await mkdtemp(path.join(tmpdir(), "ember-rolldown-map-"));
|
|
227
|
+
|
|
228
|
+
const files = {
|
|
229
|
+
// index.ts contains a `.gts` specifier, so it goes through the rewrite.
|
|
230
|
+
"index.ts": `export { default as Foo } from './foo.gts';`,
|
|
231
|
+
"foo.gts": `<template>Foo</template>`,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
for (const [relative, source] of Object.entries(files)) {
|
|
235
|
+
await writeFile(path.join(dir, relative), source, "utf8");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const warnings: string[] = [];
|
|
239
|
+
const build = await rolldown({
|
|
240
|
+
input: path.join(dir, "index.ts"),
|
|
241
|
+
plugins: [emberTransform()],
|
|
242
|
+
external: (id) => !(id.startsWith(".") || path.isAbsolute(id)),
|
|
243
|
+
onwarn(warning) {
|
|
244
|
+
warnings.push(warning.code ?? String(warning));
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const { output } = await build.generate({ format: "es", sourcemap: true });
|
|
249
|
+
|
|
250
|
+
expect(warnings.filter((code) => code === "SOURCEMAP_BROKEN")).toEqual([]);
|
|
251
|
+
|
|
252
|
+
const chunk = output.find((entry) => entry.type === "chunk" && "map" in entry);
|
|
253
|
+
expect(chunk).toBeDefined();
|
|
254
|
+
// The composed map traces through the rewrite back to the original .gts
|
|
255
|
+
// source (content-tag's map survives the specifier-rewrite pass).
|
|
256
|
+
const sources = (chunk as { map?: { sources: string[] } }).map?.sources ?? [];
|
|
257
|
+
expect(sources.some((source) => source.endsWith("foo.gts"))).toBe(true);
|
|
258
|
+
});
|
|
173
259
|
});
|
|
@@ -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";
|
|
@@ -38,58 +38,96 @@ async function fixDtsExtensionsInDir(dir: string): Promise<void> {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* A sourcemap mapping every generated line to the same line of the input.
|
|
43
|
+
*
|
|
44
|
+
* `AAAA` is the [0, 0, 0, 0] VLQ segment (first line maps to source 0, line 0,
|
|
45
|
+
* column 0); `AACA` is [0, 0, +1, 0] (each following line advances the source
|
|
46
|
+
* line by one). Exact at line granularity, which is all the specifier rewrite
|
|
47
|
+
* can disturb.
|
|
48
|
+
*/
|
|
49
|
+
function lineIdentityMap(id: string, source: string) {
|
|
50
|
+
return {
|
|
51
|
+
version: 3,
|
|
52
|
+
sources: [id],
|
|
53
|
+
sourcesContent: [source],
|
|
54
|
+
names: [],
|
|
55
|
+
mappings: source
|
|
56
|
+
.split("\n")
|
|
57
|
+
.map((_, line) => (line === 0 ? "AAAA" : "AACA"))
|
|
58
|
+
.join(";"),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
41
62
|
/**
|
|
42
63
|
* Maps `.gts` -> `.ts` and `.gjs` -> `.js` (so rolldown can identify them as
|
|
43
64
|
* ts/js) and preprocesses `<template>` via content-tag.
|
|
44
65
|
*/
|
|
45
66
|
export function emberTransform(): Plugin {
|
|
46
|
-
const virtualFiles = new Set<string>();
|
|
47
|
-
|
|
48
67
|
return {
|
|
49
68
|
name: "ember:transform",
|
|
50
69
|
|
|
51
70
|
resolveId: {
|
|
52
71
|
order: "pre",
|
|
53
72
|
handler(id, importer) {
|
|
54
|
-
|
|
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
|
+
}
|
|
55
95
|
|
|
56
96
|
const fileName = path.join(path.dirname(importer), id);
|
|
57
97
|
|
|
58
|
-
if (id.endsWith(".gts")
|
|
59
|
-
virtualFiles.add(fileName);
|
|
98
|
+
if (id.endsWith(".gts")) {
|
|
60
99
|
return {
|
|
61
100
|
id: fileName.replace(/\.gts$/, ".ts"),
|
|
62
101
|
meta: { fileName },
|
|
63
102
|
};
|
|
64
103
|
}
|
|
65
104
|
|
|
66
|
-
if (id.endsWith(".gjs")
|
|
67
|
-
virtualFiles.add(fileName);
|
|
105
|
+
if (id.endsWith(".gjs")) {
|
|
68
106
|
return {
|
|
69
107
|
id: fileName.replace(/\.gjs$/, ".js"),
|
|
70
108
|
meta: { fileName },
|
|
71
109
|
};
|
|
72
110
|
}
|
|
73
111
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
112
|
+
// A `.ts`/`.js` specifier whose only backing file is a `.gts`/`.gjs`
|
|
113
|
+
// resolves to the virtual module. This must also serve `.d.ts`
|
|
114
|
+
// importers: rolldown-plugin-dts resolves a declaration module's
|
|
115
|
+
// imports through the plugin pipeline (`this.resolve`), and — when the
|
|
116
|
+
// resolution is a source file — loads it (registering its declaration)
|
|
117
|
+
// before mapping the import to the declaration id. Virtual modules
|
|
118
|
+
// exist nowhere else, so if we don't answer here the import either
|
|
119
|
+
// fails to resolve (no file on disk) or, worse, resolves to a
|
|
120
|
+
// declaration id that was never registered.
|
|
121
|
+
if (id.endsWith(".ts")) {
|
|
82
122
|
const gtsFileName = fileName.replace(/\.ts$/, ".gts");
|
|
83
123
|
if (existsSync(gtsFileName) && !existsSync(fileName)) {
|
|
84
|
-
virtualFiles.add(gtsFileName);
|
|
85
124
|
return { id: fileName, meta: { fileName: gtsFileName } };
|
|
86
125
|
}
|
|
87
126
|
}
|
|
88
127
|
|
|
89
|
-
if (id.endsWith(".js")
|
|
128
|
+
if (id.endsWith(".js")) {
|
|
90
129
|
const gjsFileName = fileName.replace(/\.js$/, ".gjs");
|
|
91
130
|
if (existsSync(gjsFileName) && !existsSync(fileName)) {
|
|
92
|
-
virtualFiles.add(gjsFileName);
|
|
93
131
|
return { id: fileName, meta: { fileName: gjsFileName } };
|
|
94
132
|
}
|
|
95
133
|
}
|
|
@@ -105,7 +143,18 @@ export function emberTransform(): Plugin {
|
|
|
105
143
|
},
|
|
106
144
|
async handler(id) {
|
|
107
145
|
const meta = this.getModuleInfo(id)?.meta ?? {};
|
|
108
|
-
|
|
146
|
+
let fileName = meta?.fileName;
|
|
147
|
+
|
|
148
|
+
// A virtual id can be loaded without having passed through our
|
|
149
|
+
// resolveId (rolldown-plugin-dts calls `this.load({ id })` with just
|
|
150
|
+
// the id), so no meta is attached: recover the backing file from disk.
|
|
151
|
+
if (!fileName && !existsSync(id)) {
|
|
152
|
+
if (id.endsWith(".ts") && existsSync(id.replace(/\.ts$/, ".gts"))) {
|
|
153
|
+
fileName = id.replace(/\.ts$/, ".gts");
|
|
154
|
+
} else if (id.endsWith(".js") && existsSync(id.replace(/\.js$/, ".gjs"))) {
|
|
155
|
+
fileName = id.replace(/\.js$/, ".gjs");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
109
158
|
|
|
110
159
|
if (fileName) {
|
|
111
160
|
this.addWatchFile(fileName);
|
|
@@ -132,21 +181,26 @@ export function emberTransform(): Plugin {
|
|
|
132
181
|
id: /\.(js|ts)$/,
|
|
133
182
|
},
|
|
134
183
|
handler(input, id) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
184
|
+
// Rewrite `.gts` specifiers to `.ts` — the (virtual) source id — in
|
|
185
|
+
// runtime modules AND declaration modules alike. Declaration modules
|
|
186
|
+
// must NOT be rewritten to `.d.ts`: rolldown-plugin-dts's resolver
|
|
187
|
+
// treats a source-file resolution as "load it (registering its
|
|
188
|
+
// declaration), then map to the declaration id", while a `.d.ts` id is
|
|
189
|
+
// returned as-is — unloadable when the module isn't registered yet
|
|
190
|
+
// (e.g. the `.gts` is only ever type-imported).
|
|
191
|
+
const output = input.replace(
|
|
192
|
+
/(['"`])((?:\.\.?\/|\/|@|[A-Za-z0-9_\-])[^'"]*?\.gts)\1/g,
|
|
193
|
+
(_m, q, p) => `${q}${p.replace(/\.gts$/, ".ts")}${q}`,
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
if (output === input) {
|
|
197
|
+
return null;
|
|
147
198
|
}
|
|
148
199
|
|
|
149
|
-
|
|
200
|
+
// The rewrite only shortens import specifiers in place, so a
|
|
201
|
+
// line-identity map is accurate to the line (and to the column for
|
|
202
|
+
// everything before the first rewritten specifier on a line).
|
|
203
|
+
return { code: output, map: lineIdentityMap(id, input) };
|
|
150
204
|
},
|
|
151
205
|
},
|
|
152
206
|
|
|
@@ -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
|
*
|