ember.nvp 0.5.0 → 0.7.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.
Files changed (91) hide show
  1. package/.github/workflows/ci.yml +17 -2
  2. package/.release-plan.json +84 -4
  3. package/CHANGELOG.md +68 -0
  4. package/README.md +29 -0
  5. package/package.json +4 -2
  6. package/packages/build-tooling-utils/README.md +37 -0
  7. package/packages/build-tooling-utils/index.ts +2 -0
  8. package/packages/build-tooling-utils/package.json +42 -0
  9. package/packages/build-tooling-utils/rolldown.config.js +18 -0
  10. package/packages/build-tooling-utils/src/extensions.ts +18 -0
  11. package/packages/build-tooling-utils/src/maybe-babel.ts +146 -0
  12. package/packages/rolldown/README.md +115 -0
  13. package/packages/rolldown/index.ts +56 -0
  14. package/packages/rolldown/package.json +59 -0
  15. package/packages/rolldown/rolldown.config.js +21 -0
  16. package/packages/rolldown/src/babel.ts +136 -0
  17. package/packages/rolldown/src/define-config.ts +32 -0
  18. package/packages/rolldown/src/externals.ts +83 -0
  19. package/packages/rolldown/src/isolated-declarations.ts +65 -0
  20. package/packages/rolldown/src/transform.ts +162 -0
  21. package/packages/vite/README.md +7 -21
  22. package/packages/vite/index.ts +1 -1
  23. package/packages/vite/package.json +4 -6
  24. package/packages/vite/src/ember.ts +129 -15
  25. package/packages/vite/src/test-html.ts +86 -0
  26. package/src/bases/minimal-app/files/app/router.ts +1 -0
  27. package/src/bases/minimal-app/files/tsconfig.json +1 -0
  28. package/src/bases/minimal-app/index.js +2 -2
  29. package/src/bases/minimal-library/files/README.md +41 -0
  30. package/src/bases/minimal-library/files/package.json +49 -0
  31. package/src/bases/minimal-library/files/tsconfig.json +10 -0
  32. package/src/bases/minimal-library/files/tsdown.config.js +6 -0
  33. package/src/bases/minimal-library/index.js +158 -2
  34. package/src/cli/args.js +7 -1
  35. package/src/cli/index.js +90 -16
  36. package/src/cli/questions/layers.js +5 -2
  37. package/src/cli/questions/project-type.js +0 -5
  38. package/src/cli/questions/write.js +169 -0
  39. package/src/index.js +1 -0
  40. package/src/layers/are-the-types-wrong/index.js +74 -0
  41. package/src/layers/git/files/gitignore +14 -1
  42. package/src/layers/git/index.js +6 -5
  43. package/src/layers/github-actions/index.js +12 -8
  44. package/src/layers/index.js +1 -1
  45. package/src/layers/inspector-support/index.js +101 -0
  46. package/src/layers/prettier/files/.prettierignore +4 -0
  47. package/src/layers/publint/index.js +68 -0
  48. package/src/layers/qunit/files/tests/unit/macros-test.ts +2 -2
  49. package/src/layers/qunit/index.js +107 -7
  50. package/src/layers/qunit/library-files/config/test/babel.config.js +41 -0
  51. package/src/layers/qunit/library-files/config/test/testem.cjs +26 -0
  52. package/src/layers/qunit/library-files/tests/test-helper.ts +36 -0
  53. package/src/layers/qunit/library-files/tests/unit/.gitkeep +0 -0
  54. package/src/layers/qunit/library-files/vite.config.mjs +6 -0
  55. package/src/layers/typescript/index.js +43 -7
  56. package/src/layers/vitest/files/vitest.config.mjs +11 -11
  57. package/src/layers/vitest/index.js +70 -32
  58. package/src/utils/babel.js +9 -6
  59. package/src/utils/fs.js +44 -3
  60. package/src/utils/inspector-support.js +196 -0
  61. package/src/utils/npm.js +21 -7
  62. package/src/utils/remove-types.js +5 -0
  63. package/src/utils/rewrite-imports.js +157 -0
  64. package/src/utils/stage.js +317 -0
  65. package/src/utils/tsdown-config.js +85 -0
  66. package/src/utils/types.ts +5 -1
  67. package/test/ember.nvp/base-library.test.ts +232 -0
  68. package/test/ember.nvp/generator-replace-or-update.test.ts +2 -3
  69. package/test/ember.nvp/inspector-support.test.ts +179 -0
  70. package/test/ember.nvp/layer-inspector-support.test.ts +69 -0
  71. package/test/ember.nvp/layer-publish-checks.test.ts +103 -0
  72. package/test/ember.nvp/layer-qunit.test.ts +376 -15
  73. package/test/ember.nvp/layer-vitest.test.ts +250 -0
  74. package/test/ember.nvp/layers.test.ts +16 -0
  75. package/test/ember.nvp/library-src-fixtures.ts +81 -0
  76. package/test/ember.nvp/permutations/app-1.test.ts +3 -0
  77. package/test/ember.nvp/permutations/app-2.test.ts +3 -0
  78. package/test/ember.nvp/permutations/library-1.test.ts +3 -0
  79. package/test/ember.nvp/permutations/library-2.test.ts +3 -0
  80. package/test/ember.nvp/permutations/shared.ts +239 -0
  81. package/test/ember.nvp/rewrite-imports.test.ts +235 -0
  82. package/test/ember.nvp/stage.test.ts +188 -0
  83. package/test/test-helpers/permutate.test.ts +17 -0
  84. package/test/test-helpers.ts +20 -7
  85. package/tsconfig.json +1 -0
  86. package/packages/vite/src/maybe-babel.ts +0 -219
  87. package/src/layers/qunit/files/run-tests.mjs +0 -30
  88. package/src/layers/vitest/files/tests/test-helper.ts +0 -5
  89. package/test/ember.nvp/permutations.test.ts +0 -200
  90. /package/src/{layers/vitest/files/tests/integration/.gitkeep → bases/minimal-library/files/src/index.ts} +0 -0
  91. /package/src/layers/{vitest/files/tests/unit → qunit/library-files/tests/rendering}/.gitkeep +0 -0
@@ -0,0 +1,317 @@
1
+ import { existsSync } from "node:fs";
2
+ import { cp, mkdir, mkdtemp, readdir, readFile, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, dirname, join, relative, sep } from "node:path";
5
+ import { styleText } from "node:util";
6
+ import { structuredPatch } from "diff";
7
+
8
+ /**
9
+ * Directories that belong to the target project's environment rather than
10
+ * its generated contents. They are never seeded into the stage, never
11
+ * diffed, and never deleted on commit.
12
+ *
13
+ * (`.git` has one exception: see {@link Stage#commit})
14
+ */
15
+ const ENVIRONMENT_DIRECTORIES = new Set(["node_modules", ".git"]);
16
+
17
+ /**
18
+ * @typedef {'added' | 'modified' | 'deleted'} ChangeStatus
19
+ * @typedef {{ path: string, status: ChangeStatus }} Change
20
+ */
21
+
22
+ /**
23
+ * A copy-on-write overlay for a project directory, in the spirit of
24
+ * Docker's layered file systems:
25
+ *
26
+ * - the target directory is the read-only lower layer
27
+ * - a real directory in the OS temp dir is the writable upper layer,
28
+ * seeded ("copied up") from the target
29
+ *
30
+ * All generation (bases, layers, consolidators) runs against the upper
31
+ * layer via `stage.directory`. Because the upper layer is a real
32
+ * directory, layers keep using `node:fs`, `ember-apply`, and even
33
+ * subprocesses (`git`, package managers) exactly as they would against
34
+ * the real project -- an in-process virtual fs can't offer that, since
35
+ * child processes can't see it.
36
+ *
37
+ * Nothing touches the target until {@link Stage#commit}; {@link Stage#discard}
38
+ * throws the upper layer away.
39
+ */
40
+ export class Stage {
41
+ /**
42
+ * @param {string} targetDirectory the directory the project should end up in
43
+ * @param {{ seed?: boolean }} [options]
44
+ * seed: copy the target's current contents into the stage first
45
+ * (a "copy up"). Defaults to true. Pass false to stage a from-scratch
46
+ * generation (e.g. the "replace" flow).
47
+ * @returns {Promise<Stage>}
48
+ */
49
+ static async create(targetDirectory, options = {}) {
50
+ const { seed = true } = options;
51
+ const directory = await mkdtemp(join(tmpdir(), "ember.nvp-stage-"));
52
+ const exists = existsSync(targetDirectory);
53
+
54
+ if (exists && seed) {
55
+ await cp(targetDirectory, directory, {
56
+ recursive: true,
57
+ force: true,
58
+ filter: (source) => !ENVIRONMENT_DIRECTORIES.has(basename(source)),
59
+ });
60
+ }
61
+
62
+ return new Stage(targetDirectory, directory, { isNew: !exists });
63
+ }
64
+
65
+ #target;
66
+ #directory;
67
+ #isNew;
68
+
69
+ /**
70
+ * Use {@link Stage.create} instead.
71
+ *
72
+ * @param {string} target
73
+ * @param {string} directory
74
+ * @param {{ isNew: boolean }} info
75
+ */
76
+ constructor(target, directory, info) {
77
+ this.#target = target;
78
+ this.#directory = directory;
79
+ this.#isNew = info.isNew;
80
+ }
81
+
82
+ /**
83
+ * The writable upper layer. Generate in here,
84
+ * e.g. `new Project(stage.directory, desires)`.
85
+ *
86
+ * @type {string}
87
+ */
88
+ get directory() {
89
+ return this.#directory;
90
+ }
91
+
92
+ /**
93
+ * The directory a commit writes to.
94
+ *
95
+ * @type {string}
96
+ */
97
+ get targetDirectory() {
98
+ return this.#target;
99
+ }
100
+
101
+ /**
102
+ * Whether the target directory existed when the stage was created.
103
+ *
104
+ * @type {boolean}
105
+ */
106
+ get isNew() {
107
+ return this.#isNew;
108
+ }
109
+
110
+ /**
111
+ * Compares the upper layer to the target.
112
+ *
113
+ * @returns {Promise<Change[]>}
114
+ */
115
+ async changes() {
116
+ const staged = await listFiles(this.#directory);
117
+ const existing = existsSync(this.#target) ? await listFiles(this.#target) : new Set();
118
+
119
+ /** @type {Change[]} */
120
+ const changes = [];
121
+
122
+ for (const path of staged) {
123
+ if (!existing.has(path)) {
124
+ changes.push({ path, status: "added" });
125
+ continue;
126
+ }
127
+
128
+ const [before, after] = await Promise.all([
129
+ readFile(join(this.#target, path)),
130
+ readFile(join(this.#directory, path)),
131
+ ]);
132
+
133
+ if (!before.equals(after)) {
134
+ changes.push({ path, status: "modified" });
135
+ }
136
+ }
137
+
138
+ for (const path of existing) {
139
+ if (!staged.has(path)) {
140
+ changes.push({ path, status: "deleted" });
141
+ }
142
+ }
143
+
144
+ return changes.sort((a, b) => a.path.localeCompare(b.path));
145
+ }
146
+
147
+ /**
148
+ * A unified diff of the staged changes against the target,
149
+ * colorized when stdout supports it.
150
+ *
151
+ * @param {Change[]} [changes] pass the result of {@link Stage#changes} to avoid recomputing
152
+ * @returns {Promise<string>}
153
+ */
154
+ async diff(changes) {
155
+ changes ??= await this.changes();
156
+
157
+ const patches = [];
158
+
159
+ for (const change of changes) {
160
+ const before =
161
+ change.status === "added" ? null : await readFile(join(this.#target, change.path));
162
+ const after =
163
+ change.status === "deleted" ? null : await readFile(join(this.#directory, change.path));
164
+
165
+ patches.push(renderPatch(change, before, after));
166
+ }
167
+
168
+ return patches.join("\n");
169
+ }
170
+
171
+ /**
172
+ * Applies the staged changes to the target directory, then removes the
173
+ * stage. Only paths reported by {@link Stage#changes} are touched --
174
+ * everything else in the target (untracked files, node_modules, .git)
175
+ * is left alone.
176
+ *
177
+ * One exception: when the stage has a `.git` directory and the target
178
+ * does not (the git layer ran `git init` inside the stage), the `.git`
179
+ * directory is carried over so a freshly generated project keeps its
180
+ * history.
181
+ *
182
+ * @param {Change[]} [changes] the changes to apply -- pass a subset of
183
+ * {@link Stage#changes} to apply only some of them (e.g. the ones a
184
+ * user accepted during review). Defaults to all staged changes.
185
+ * @returns {Promise<Change[]>} the changes that were written
186
+ */
187
+ async commit(changes) {
188
+ changes ??= await this.changes();
189
+
190
+ await mkdir(this.#target, { recursive: true });
191
+
192
+ for (const change of changes) {
193
+ const targetPath = join(this.#target, change.path);
194
+
195
+ if (change.status === "deleted") {
196
+ await rm(targetPath, { force: true });
197
+ continue;
198
+ }
199
+
200
+ await mkdir(dirname(targetPath), { recursive: true });
201
+ await cp(join(this.#directory, change.path), targetPath, { force: true });
202
+ }
203
+
204
+ const stagedGit = join(this.#directory, ".git");
205
+ const targetGit = join(this.#target, ".git");
206
+
207
+ if (existsSync(stagedGit) && !existsSync(targetGit)) {
208
+ await cp(stagedGit, targetGit, { recursive: true });
209
+ }
210
+
211
+ await this.discard();
212
+
213
+ return changes;
214
+ }
215
+
216
+ /**
217
+ * Removes the stage without writing anything to the target.
218
+ */
219
+ async discard() {
220
+ await rm(this.#directory, { recursive: true, force: true });
221
+ }
222
+ }
223
+
224
+ /**
225
+ * All files (as paths relative to `directory`), excluding environment
226
+ * directories. Includes dotfiles (`.gitignore`, `.github/`, ...), which
227
+ * `fs.glob` would skip.
228
+ *
229
+ * @param {string} directory
230
+ * @returns {Promise<Set<string>>}
231
+ */
232
+ async function listFiles(directory) {
233
+ /** @type {Set<string>} */
234
+ const files = new Set();
235
+
236
+ const entries = await readdir(directory, { withFileTypes: true, recursive: true });
237
+
238
+ for (const entry of entries) {
239
+ if (!entry.isFile()) continue;
240
+
241
+ const path = relative(directory, join(entry.parentPath, entry.name));
242
+
243
+ if (path.split(sep).some((segment) => ENVIRONMENT_DIRECTORIES.has(segment))) {
244
+ continue;
245
+ }
246
+
247
+ files.add(path);
248
+ }
249
+
250
+ return files;
251
+ }
252
+
253
+ /**
254
+ * @param {Change} change
255
+ * @param {Buffer | null} before
256
+ * @param {Buffer | null} after
257
+ * @returns {string}
258
+ */
259
+ function renderPatch(change, before, after) {
260
+ const header = styleText("bold", `${statusLabel(change.status)} ${change.path}`);
261
+
262
+ if (before?.includes(0) || after?.includes(0)) {
263
+ return `${header}\n${styleText("dim", "(binary file)")}\n`;
264
+ }
265
+
266
+ const patch = structuredPatch(
267
+ change.path,
268
+ change.path,
269
+ before?.toString() ?? "",
270
+ after?.toString() ?? "",
271
+ undefined,
272
+ undefined,
273
+ { context: 3 },
274
+ );
275
+
276
+ const lines = [
277
+ header,
278
+ styleText("red", `--- ${change.status === "added" ? "/dev/null" : `a/${change.path}`}`),
279
+ styleText("green", `+++ ${change.status === "deleted" ? "/dev/null" : `b/${change.path}`}`),
280
+ ];
281
+
282
+ for (const hunk of patch.hunks) {
283
+ lines.push(
284
+ styleText(
285
+ "cyan",
286
+ `@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`,
287
+ ),
288
+ );
289
+
290
+ for (const line of hunk.lines) {
291
+ if (line.startsWith("+")) {
292
+ lines.push(styleText("green", line));
293
+ } else if (line.startsWith("-")) {
294
+ lines.push(styleText("red", line));
295
+ } else {
296
+ lines.push(line);
297
+ }
298
+ }
299
+ }
300
+
301
+ return lines.join("\n") + "\n";
302
+ }
303
+
304
+ /**
305
+ * @param {ChangeStatus} status
306
+ * @returns {string}
307
+ */
308
+ function statusLabel(status) {
309
+ switch (status) {
310
+ case "added":
311
+ return styleText("green", "A");
312
+ case "modified":
313
+ return styleText("yellow", "M");
314
+ case "deleted":
315
+ return styleText("red", "D");
316
+ }
317
+ }
@@ -0,0 +1,85 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { toTree, print, statements } from "ember-estree";
3
+
4
+ /**
5
+ * Adds a property to the object passed to `defineConfig(...)` in the
6
+ * project's tsdown.config.js.
7
+ *
8
+ * No-ops when the property is already present (layers re-run), and when
9
+ * the config doesn't have the `defineConfig({ ... })` shape -- an existing
10
+ * project's hand-rolled config is the user's to manage.
11
+ *
12
+ * @param {import('#utils/project.js').Project} project
13
+ * @param {string} propertyName
14
+ * @param {string} propertySource the property as source, e.g. `publint: true`
15
+ */
16
+ export async function addTsdownConfigProperty(project, propertyName, propertySource) {
17
+ const configPath = project.path("tsdown.config.js");
18
+ const contents = await readFile(configPath, "utf-8");
19
+
20
+ /** @type {import('ember-estree').ASTNode | null} */
21
+ let configObject = null;
22
+
23
+ const tree = /** @type {import('ember-estree').FileNode} */ (
24
+ toTree(contents, {
25
+ filePath: configPath,
26
+ visitors: {
27
+ CallExpression(node) {
28
+ const callee = /** @type {import('ember-estree').ASTNode} */ (node.callee);
29
+
30
+ if (callee?.type !== "Identifier" || callee.name !== "defineConfig") return;
31
+
32
+ const [argument] = /** @type {import('ember-estree').ASTNode[]} */ (node.arguments ?? []);
33
+
34
+ if (argument?.type === "ObjectExpression") {
35
+ configObject = argument;
36
+ }
37
+ },
38
+ },
39
+ })
40
+ );
41
+
42
+ if (!configObject) return;
43
+
44
+ const found = /** @type {import('ember-estree').ASTNode} */ (configObject);
45
+ const properties = /** @type {import('ember-estree').ASTNode[]} */ (found.properties ?? []);
46
+
47
+ const alreadyPresent = properties.some((property) => {
48
+ const key = /** @type {import('ember-estree').ASTNode} */ (property.key);
49
+
50
+ return key?.type === "Identifier" && key.name === propertyName;
51
+ });
52
+
53
+ if (alreadyPresent) return;
54
+
55
+ const [statement] = statements`defineConfig({ ${propertySource} });`;
56
+
57
+ if (!statement) return;
58
+
59
+ const expression = /** @type {import('ember-estree').ASTNode} */ (statement.expression);
60
+ const [templateObject] = /** @type {import('ember-estree').ASTNode[]} */ (expression.arguments);
61
+ const [property] = /** @type {import('ember-estree').ASTNode[]} */ (
62
+ templateObject?.properties ?? []
63
+ );
64
+
65
+ if (!property) return;
66
+
67
+ properties.push(property);
68
+ found.properties = properties;
69
+
70
+ await writeFile(configPath, print(tree));
71
+ }
72
+
73
+ /**
74
+ * Whether the project's tsdown.config.js configures `propertyName`.
75
+ *
76
+ * @param {import('#utils/project.js').Project} project
77
+ * @param {string} propertyName
78
+ */
79
+ export async function hasTsdownConfigProperty(project, propertyName) {
80
+ if (!project.hasFile("tsdown.config.js")) return false;
81
+
82
+ const contents = await readFile(project.path("tsdown.config.js"), "utf-8");
83
+
84
+ return contents.includes(propertyName);
85
+ }
@@ -8,7 +8,11 @@ export interface Layer {
8
8
  */
9
9
  label: string;
10
10
  hint?: string;
11
- defaultValue?: () => unknown;
11
+ /**
12
+ * Whether the layer is pre-selected in the CLI (the user can still
13
+ * deselect it).
14
+ */
15
+ defaultValue?: (projectType: ProjectType) => unknown;
12
16
  /**
13
17
  * The function that applies the codemod
14
18
  *
@@ -0,0 +1,232 @@
1
+ import { describe, it, beforeAll, afterAll, expect } from "vitest";
2
+ import { generate, mktemp } from "#test-helpers";
3
+ import { writeLibrarySource } from "./library-src-fixtures.ts";
4
+ import { execa } from "execa";
5
+ import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
6
+ import { join, relative, sep } from "node:path";
7
+
8
+ import type { Project } from "ember.nvp";
9
+
10
+ /**
11
+ * The minimal-library base has to produce a *buildable* library: these tests
12
+ * generate both flavors (JavaScript and TypeScript), snapshot the interesting
13
+ * generated files, and run the real `pnpm build` (tsdown + rolldown) against
14
+ * them.
15
+ */
16
+
17
+ async function listFiles(directory: string): Promise<string[]> {
18
+ const entries = await readdir(directory, { withFileTypes: true, recursive: true });
19
+
20
+ return entries
21
+ .filter((entry) => entry.isFile())
22
+ .map((entry) => relative(directory, join(entry.parentPath, entry.name)))
23
+ .filter((path) => !path.split(sep).includes("node_modules"))
24
+ .sort();
25
+ }
26
+
27
+ async function read(project: Project, filePath: string): Promise<string> {
28
+ return readFile(join(project.directory, filePath), "utf-8");
29
+ }
30
+
31
+ async function installAndBuild(project: Project) {
32
+ let install = await execa("pnpm install", { cwd: project.directory, shell: true });
33
+ expect(install.exitCode).toBe(0);
34
+
35
+ let build = await execa("pnpm build", { cwd: project.directory, shell: true });
36
+ expect(build.exitCode).toBe(0);
37
+ }
38
+
39
+ describe("base: minimal-library", () => {
40
+ const dirs: string[] = [];
41
+
42
+ afterAll(async () => {
43
+ if (process.env.CI) return;
44
+
45
+ for (const dir of dirs) {
46
+ await rm(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
47
+ }
48
+ });
49
+
50
+ describe("JavaScript", () => {
51
+ let project: Project;
52
+
53
+ beforeAll(async () => {
54
+ project = await generate({ type: "library", name: "my-lib", layers: [] });
55
+ dirs.push(project.directory);
56
+ });
57
+
58
+ it("generates the expected files (src is empty)", async () => {
59
+ expect(await listFiles(project.directory)).toMatchInlineSnapshot(`
60
+ [
61
+ ".gitignore",
62
+ "README.md",
63
+ "package.json",
64
+ "src/index.js",
65
+ "tsdown.config.js",
66
+ ]
67
+ `);
68
+
69
+ expect(await read(project, "src/index.js")).toBe("");
70
+ });
71
+
72
+ it("is publishable", async () => {
73
+ let manifest = JSON.parse(await read(project, "package.json"));
74
+
75
+ // The template is private (it lives in a workspace); the generated
76
+ // library must not be
77
+ expect(manifest).not.toHaveProperty("private");
78
+ });
79
+
80
+ it("has no TypeScript leftovers", async () => {
81
+ let manifest = JSON.parse(await read(project, "package.json"));
82
+
83
+ expect(manifest.devDependencies).not.toHaveProperty("typescript");
84
+ expect(manifest.devDependencies).not.toHaveProperty("@ember/library-tsconfig");
85
+ // No declarations are emitted without types
86
+ expect(JSON.stringify(manifest.exports)).not.toContain("types");
87
+
88
+ expect(await read(project, "tsdown.config.js")).toMatchInlineSnapshot(`
89
+ "import { defineConfig, ember } from "@nullvoxpopuli/ember-rolldown";
90
+
91
+ export default defineConfig({
92
+ entry: ["./src/index.js"],
93
+ dts: false,
94
+ plugins: [ember()],
95
+ });
96
+ "
97
+ `);
98
+ });
99
+
100
+ it("removes the TS plugin from an existing project's babel config", async () => {
101
+ // The base doesn't emit a babel.config.js, but generation can run
102
+ // over an existing project that has one
103
+ let directory = await mktemp("existing-lib");
104
+
105
+ dirs.push(directory);
106
+
107
+ await mkdir(directory, { recursive: true });
108
+ await writeFile(
109
+ join(directory, "babel.config.js"),
110
+ `export default {
111
+ plugins: [
112
+ ["@babel/plugin-transform-typescript", { allExtensions: true }],
113
+ ["module:decorator-transforms"],
114
+ ],
115
+ };
116
+ `,
117
+ );
118
+
119
+ let existing = await generate({ directory, type: "library", name: "existing-lib" });
120
+
121
+ let babelConfig = await readFile(join(existing.directory, "babel.config.js"), "utf-8");
122
+
123
+ expect(babelConfig).not.toContain("@babel/plugin-transform-typescript");
124
+ expect(babelConfig).toContain("module:decorator-transforms");
125
+ });
126
+
127
+ it("builds (with example source written in)", async () => {
128
+ await writeLibrarySource(project, "javascript");
129
+ await installAndBuild(project);
130
+
131
+ let output = await read(project, "dist/index.js");
132
+
133
+ // Published libraries ship precompileTemplate (the consuming app does
134
+ // the final compile), never wire format
135
+ expect(output).toContain("precompileTemplate");
136
+ expect(output).not.toContain("createTemplateFactory");
137
+
138
+ expect(await listFiles(join(project.directory, "dist"))).toMatchInlineSnapshot(`
139
+ [
140
+ "index.js",
141
+ "index.js.map",
142
+ ]
143
+ `);
144
+ });
145
+ });
146
+
147
+ describe("TypeScript", () => {
148
+ let project: Project;
149
+
150
+ beforeAll(async () => {
151
+ project = await generate({ type: "library", name: "my-lib", layers: ["typescript"] });
152
+ dirs.push(project.directory);
153
+ });
154
+
155
+ it("generates the expected files (src is empty)", async () => {
156
+ expect(await listFiles(project.directory)).toMatchInlineSnapshot(`
157
+ [
158
+ ".gitignore",
159
+ "README.md",
160
+ "package.json",
161
+ "src/index.ts",
162
+ "tsconfig.json",
163
+ "tsdown.config.js",
164
+ ]
165
+ `);
166
+
167
+ expect(await read(project, "src/index.ts")).toBe("");
168
+ });
169
+
170
+ it("type checks", async () => {
171
+ await writeLibrarySource(project, "typescript");
172
+
173
+ let install = await execa("pnpm install", { cwd: project.directory, shell: true });
174
+ expect(install.exitCode).toBe(0);
175
+
176
+ let types = await execa("pnpm lint:types", { cwd: project.directory, shell: true });
177
+ expect(types.exitCode).toBe(0);
178
+ });
179
+
180
+ it("builds, including declarations", async () => {
181
+ await installAndBuild(project);
182
+
183
+ let output = await read(project, "dist/index.js");
184
+
185
+ expect(output).toContain("precompileTemplate");
186
+ expect(output).not.toContain("createTemplateFactory");
187
+
188
+ let declarations = await read(project, "dist/index.d.ts");
189
+
190
+ expect(declarations).toContain("declare class Greeting extends Component<GreetingSignature>");
191
+ expect(declarations).toContain("declare const Badge: TOC<BadgeSignature>");
192
+ expect(declarations).toContain("declare function add(a: number, b: number): number");
193
+
194
+ expect(await listFiles(join(project.directory, "dist"))).toMatchInlineSnapshot(`
195
+ [
196
+ "index.d.ts",
197
+ "index.d.ts.map",
198
+ "index.js",
199
+ "index.js.map",
200
+ ]
201
+ `);
202
+ });
203
+
204
+ it("errors when tsconfig lacks isolatedDeclarations", async () => {
205
+ let tsconfigPath = join(project.directory, "tsconfig.json");
206
+ let original = await readFile(tsconfigPath, "utf-8");
207
+
208
+ try {
209
+ await writeFile(
210
+ tsconfigPath,
211
+ JSON.stringify({
212
+ extends: "@ember/library-tsconfig",
213
+ include: ["src"],
214
+ compilerOptions: { allowJs: true, rootDir: "./src" },
215
+ }),
216
+ );
217
+
218
+ let build = await execa("pnpm build", {
219
+ cwd: project.directory,
220
+ shell: true,
221
+ reject: false,
222
+ all: true,
223
+ });
224
+
225
+ expect(build.exitCode).not.toBe(0);
226
+ expect(build.all).toContain('"compilerOptions.isolatedDeclarations": true');
227
+ } finally {
228
+ await writeFile(tsconfigPath, original);
229
+ }
230
+ });
231
+ });
232
+ });
@@ -1,10 +1,9 @@
1
1
  import { describe, it, expect, afterAll } from "vitest";
2
2
  import { generate, layers, expectIsSetup } from "#test-helpers";
3
3
  import { generateProject } from "ember.nvp";
4
- import { writeFile } from "node:fs/promises";
4
+ import { rm, writeFile } from "node:fs/promises";
5
5
  import { existsSync } from "node:fs";
6
6
  import { join } from "node:path";
7
- import { rimraf } from "rimraf";
8
7
 
9
8
  /**
10
9
  * The replace-or-update question only matters when a project already exists
@@ -21,7 +20,7 @@ describe("generating over an existing project", () => {
21
20
  if (process.env.CI) return;
22
21
 
23
22
  for (const dir of dirs) {
24
- await rimraf(dir, { maxRetries: 3, retryDelay: 100 });
23
+ await rm(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
25
24
  }
26
25
  });
27
26