ember.nvp 0.6.0 → 1.0.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/.github/workflows/ci.yml +17 -2
- package/.release-plan.json +29 -5
- package/CHANGELOG.md +73 -0
- package/package.json +3 -2
- package/packages/build-tooling-utils/README.md +37 -0
- package/packages/build-tooling-utils/index.ts +2 -0
- package/packages/build-tooling-utils/package.json +42 -0
- package/packages/build-tooling-utils/rolldown.config.js +18 -0
- package/packages/build-tooling-utils/src/extensions.ts +18 -0
- package/packages/build-tooling-utils/src/maybe-babel.ts +146 -0
- package/packages/rolldown/README.md +120 -0
- package/packages/rolldown/index.ts +60 -0
- package/packages/rolldown/package.json +59 -0
- package/packages/rolldown/rolldown.config.js +21 -0
- package/packages/rolldown/src/babel.ts +136 -0
- package/packages/rolldown/src/config.ts +50 -0
- package/packages/rolldown/src/externals.ts +83 -0
- package/packages/rolldown/src/isolated-declarations.ts +65 -0
- package/packages/rolldown/src/transform.ts +162 -0
- package/packages/vite/README.md +7 -21
- package/packages/vite/index.ts +1 -1
- package/packages/vite/package.json +4 -6
- package/packages/vite/src/ember.ts +129 -15
- package/packages/vite/src/test-html.ts +86 -0
- package/src/bases/minimal-app/files/app/router.ts +1 -0
- package/src/bases/minimal-app/files/tsconfig.json +1 -0
- package/src/bases/minimal-app/index.js +2 -2
- package/src/bases/minimal-library/files/README.md +41 -0
- package/src/bases/minimal-library/files/package.json +49 -0
- package/src/bases/minimal-library/files/tsconfig.json +10 -0
- package/src/bases/minimal-library/files/tsdown.config.js +7 -0
- package/src/bases/minimal-library/index.js +158 -2
- package/src/cli/index.js +21 -2
- package/src/cli/questions/layers.js +5 -2
- package/src/cli/questions/project-type.js +0 -5
- package/src/layers/are-the-types-wrong/index.js +74 -0
- package/src/layers/git/files/gitignore +14 -1
- package/src/layers/git/index.js +6 -5
- package/src/layers/github-actions/index.js +12 -8
- package/src/layers/index.js +1 -1
- package/src/layers/inspector-support/index.js +101 -0
- package/src/layers/prettier/files/.prettierignore +4 -0
- package/src/layers/publint/index.js +68 -0
- package/src/layers/qunit/files/tests/unit/macros-test.ts +2 -2
- package/src/layers/qunit/index.js +107 -7
- package/src/layers/qunit/library-files/config/test/babel.config.js +41 -0
- package/src/layers/qunit/library-files/config/test/testem.cjs +26 -0
- package/src/layers/qunit/library-files/tests/test-helper.ts +36 -0
- package/src/layers/qunit/library-files/tests/unit/.gitkeep +0 -0
- package/src/layers/qunit/library-files/vite.config.mjs +6 -0
- package/src/layers/typescript/index.js +43 -7
- package/src/layers/vitest/files/vitest.config.mjs +11 -11
- package/src/layers/vitest/index.js +70 -32
- package/src/utils/babel.js +9 -6
- package/src/utils/fs.js +44 -3
- package/src/utils/inspector-support.js +196 -0
- package/src/utils/npm.js +21 -7
- package/src/utils/remove-types.js +5 -0
- package/src/utils/rewrite-imports.js +157 -0
- package/src/utils/tsdown-config.js +85 -0
- package/src/utils/types.ts +5 -1
- package/test/ember.nvp/base-library.test.ts +236 -0
- package/test/ember.nvp/generator-replace-or-update.test.ts +2 -3
- package/test/ember.nvp/inspector-support.test.ts +179 -0
- package/test/ember.nvp/layer-inspector-support.test.ts +69 -0
- package/test/ember.nvp/layer-publish-checks.test.ts +109 -0
- package/test/ember.nvp/layer-qunit.test.ts +381 -16
- package/test/ember.nvp/layer-vitest.test.ts +250 -0
- package/test/ember.nvp/layers.test.ts +16 -0
- package/test/ember.nvp/library-src-fixtures.ts +81 -0
- package/test/ember.nvp/permutations/app-1.test.ts +3 -0
- package/test/ember.nvp/permutations/app-2.test.ts +3 -0
- package/test/ember.nvp/permutations/library-1.test.ts +3 -0
- package/test/ember.nvp/permutations/library-2.test.ts +3 -0
- package/test/ember.nvp/permutations/shared.ts +239 -0
- package/test/ember.nvp/rewrite-imports.test.ts +235 -0
- package/test/test-helpers/permutate.test.ts +17 -0
- package/test/test-helpers.ts +46 -8
- package/tsconfig.json +1 -0
- package/packages/vite/src/maybe-babel.ts +0 -219
- package/src/layers/qunit/files/run-tests.mjs +0 -30
- package/src/layers/vitest/files/tests/test-helper.ts +0 -5
- package/test/ember.nvp/permutations.test.ts +0 -200
- /package/src/{layers/vitest/files/tests/integration/.gitkeep → bases/minimal-library/files/src/index.ts} +0 -0
- /package/src/layers/{vitest/files/tests/unit → qunit/library-files/tests/rendering}/.gitkeep +0 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { dirname, extname } from "node:path";
|
|
3
|
+
import { toTree, print } from "ember-estree";
|
|
4
|
+
import enhancedResolve from "enhanced-resolve";
|
|
5
|
+
|
|
6
|
+
const { ResolverFactory, CachedInputFileSystem } = enhancedResolve;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The module files this util operates on. Doubles as the candidate
|
|
10
|
+
* extensions the resolver may append when a request has none (or when we
|
|
11
|
+
* retry a request without its original extension) -- the single source
|
|
12
|
+
* of truth for both.
|
|
13
|
+
*/
|
|
14
|
+
const MODULE_EXTENSIONS = [".js", ".gjs", ".ts", ".gts"];
|
|
15
|
+
|
|
16
|
+
function createResolver() {
|
|
17
|
+
return ResolverFactory.createResolver({
|
|
18
|
+
// short-lived cache: files are being written while generation runs
|
|
19
|
+
fileSystem: new CachedInputFileSystem(fs, 100),
|
|
20
|
+
useSyncFileSystemCalls: true,
|
|
21
|
+
extensions: MODULE_EXTENSIONS,
|
|
22
|
+
// no conditionNames: we only resolve local paths and sub-path
|
|
23
|
+
// self-imports (whose targets in the emitted `imports` field are
|
|
24
|
+
// plain strings, which match regardless of conditions)
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Imports must match files. Generation renames files (e.g. type removal
|
|
30
|
+
* turns .ts into .js), so for every local specifier (./, ../, and #
|
|
31
|
+
* subpath-imports -- expanded against the project's package.json
|
|
32
|
+
* `imports` field) we check what actually exists in the emitted project
|
|
33
|
+
* tree:
|
|
34
|
+
*
|
|
35
|
+
* - the specifier resolves to a file and carries an extension? leave it
|
|
36
|
+
* alone.
|
|
37
|
+
* - it resolves but has no extension? append the resolved file's
|
|
38
|
+
* extension (fully-specified local imports resolve faster), unless the
|
|
39
|
+
* fully-specified form no longer maps to the same file (e.g. `#config`,
|
|
40
|
+
* whose `imports` target carries the extension itself).
|
|
41
|
+
* - it doesn't resolve, but the same specifier with whatever extension
|
|
42
|
+
* IS on disk does? rewrite it to that.
|
|
43
|
+
* - nothing matches? leave it alone -- we can't know better, and the
|
|
44
|
+
* project's own build will report it loudly.
|
|
45
|
+
*
|
|
46
|
+
* Non-module files are returned untouched.
|
|
47
|
+
*
|
|
48
|
+
* ember-estree parses gjs natively and its visitors fire on actual module
|
|
49
|
+
* specifiers only (static imports, re-exports, and dynamic import()) --
|
|
50
|
+
* matched specifiers are rewritten on the AST and the File is printed
|
|
51
|
+
* back out (comments included). Formatting is the project's own concern:
|
|
52
|
+
* new projects run their configured lint:fix / format right away (it's
|
|
53
|
+
* in the CLI's "Next steps").
|
|
54
|
+
*
|
|
55
|
+
* Files with no specifiers to rewrite are returned untouched.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} code the source as it will be emitted
|
|
58
|
+
* @param {string} emittedPath the path the file will be written to
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
export function rewriteImportsToMatchFiles(code, emittedPath) {
|
|
62
|
+
if (!MODULE_EXTENSIONS.includes(extname(emittedPath))) return code;
|
|
63
|
+
|
|
64
|
+
let issuerDirectory = dirname(emittedPath);
|
|
65
|
+
let resolver = createResolver();
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {string} specifier
|
|
69
|
+
* @returns {string | null} the resolved file path, if any
|
|
70
|
+
*/
|
|
71
|
+
function resolveFrom(specifier) {
|
|
72
|
+
try {
|
|
73
|
+
return resolver.resolveSync({}, issuerDirectory, specifier) || null;
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let changed = false;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {unknown} source
|
|
83
|
+
*/
|
|
84
|
+
function consider(source) {
|
|
85
|
+
if (!source || typeof source !== "object") return;
|
|
86
|
+
|
|
87
|
+
let node = /** @type {import('ember-estree').ASTNode} */ (source);
|
|
88
|
+
|
|
89
|
+
if (node.type !== "Literal" || typeof node.value !== "string") return;
|
|
90
|
+
|
|
91
|
+
let specifier = node.value;
|
|
92
|
+
let isLocal =
|
|
93
|
+
specifier.startsWith("./") || specifier.startsWith("../") || specifier.startsWith("#");
|
|
94
|
+
|
|
95
|
+
if (!isLocal) return;
|
|
96
|
+
|
|
97
|
+
let currentExtension = extname(specifier);
|
|
98
|
+
let resolved = resolveFrom(specifier);
|
|
99
|
+
|
|
100
|
+
if (resolved) {
|
|
101
|
+
// fully specified and correct
|
|
102
|
+
if (currentExtension) return;
|
|
103
|
+
|
|
104
|
+
// extensionless: append the resolved file's extension, as long as
|
|
105
|
+
// the fully-specified form still maps to the same file (`#config`
|
|
106
|
+
// maps with the extension in the target, so `#config.js` wouldn't)
|
|
107
|
+
let candidate = specifier + extname(resolved);
|
|
108
|
+
|
|
109
|
+
if (resolveFrom(candidate) !== resolved) return;
|
|
110
|
+
|
|
111
|
+
rewrite(node, candidate);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!currentExtension) return;
|
|
116
|
+
|
|
117
|
+
// check which file actually exists for this specifier
|
|
118
|
+
let base = specifier.slice(0, -currentExtension.length);
|
|
119
|
+
let resolvedBase = resolveFrom(base);
|
|
120
|
+
|
|
121
|
+
if (!resolvedBase) return;
|
|
122
|
+
|
|
123
|
+
let replacement = base + extname(resolvedBase);
|
|
124
|
+
|
|
125
|
+
if (replacement === specifier) return;
|
|
126
|
+
|
|
127
|
+
rewrite(node, replacement);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @param {import('ember-estree').ASTNode} node
|
|
132
|
+
* @param {string} replacement
|
|
133
|
+
*/
|
|
134
|
+
function rewrite(node, replacement) {
|
|
135
|
+
node.value = replacement;
|
|
136
|
+
node.raw = JSON.stringify(replacement);
|
|
137
|
+
changed = true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// (not templateOnly, so this is always a FileNode)
|
|
141
|
+
let tree = /** @type {import('ember-estree').FileNode} */ (
|
|
142
|
+
toTree(code, {
|
|
143
|
+
filePath: emittedPath,
|
|
144
|
+
visitors: {
|
|
145
|
+
ImportDeclaration: (node) => consider(node.source),
|
|
146
|
+
ExportNamedDeclaration: (node) => consider(node.source),
|
|
147
|
+
ExportAllDeclaration: (node) => consider(node.source),
|
|
148
|
+
// dynamic import()
|
|
149
|
+
ImportExpression: (node) => consider(node.source),
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
if (!changed) return code;
|
|
155
|
+
|
|
156
|
+
return print(tree);
|
|
157
|
+
}
|
|
@@ -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
|
+
}
|
package/src/utils/types.ts
CHANGED
|
@@ -8,7 +8,11 @@ export interface Layer {
|
|
|
8
8
|
*/
|
|
9
9
|
label: string;
|
|
10
10
|
hint?: string;
|
|
11
|
-
|
|
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,236 @@
|
|
|
1
|
+
import { describe, it, beforeAll, afterAll, expect } from "vitest";
|
|
2
|
+
import { generate, mktemp, pinYukuParser } 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
|
+
await pinYukuParser(project);
|
|
33
|
+
|
|
34
|
+
let install = await execa("pnpm install", { cwd: project.directory, shell: true });
|
|
35
|
+
expect(install.exitCode).toBe(0);
|
|
36
|
+
|
|
37
|
+
let build = await execa("pnpm build", { cwd: project.directory, shell: true });
|
|
38
|
+
expect(build.exitCode).toBe(0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("base: minimal-library", () => {
|
|
42
|
+
const dirs: string[] = [];
|
|
43
|
+
|
|
44
|
+
afterAll(async () => {
|
|
45
|
+
if (process.env.CI) return;
|
|
46
|
+
|
|
47
|
+
for (const dir of dirs) {
|
|
48
|
+
await rm(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("JavaScript", () => {
|
|
53
|
+
let project: Project;
|
|
54
|
+
|
|
55
|
+
beforeAll(async () => {
|
|
56
|
+
project = await generate({ type: "library", name: "my-lib", layers: [] });
|
|
57
|
+
dirs.push(project.directory);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("generates the expected files (src is empty)", async () => {
|
|
61
|
+
expect(await listFiles(project.directory)).toMatchInlineSnapshot(`
|
|
62
|
+
[
|
|
63
|
+
".gitignore",
|
|
64
|
+
"README.md",
|
|
65
|
+
"package.json",
|
|
66
|
+
"src/index.js",
|
|
67
|
+
"tsdown.config.js",
|
|
68
|
+
]
|
|
69
|
+
`);
|
|
70
|
+
|
|
71
|
+
expect(await read(project, "src/index.js")).toBe("");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("is publishable", async () => {
|
|
75
|
+
let manifest = JSON.parse(await read(project, "package.json"));
|
|
76
|
+
|
|
77
|
+
// The template is private (it lives in a workspace); the generated
|
|
78
|
+
// library must not be
|
|
79
|
+
expect(manifest).not.toHaveProperty("private");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("has no TypeScript leftovers", async () => {
|
|
83
|
+
let manifest = JSON.parse(await read(project, "package.json"));
|
|
84
|
+
|
|
85
|
+
expect(manifest.devDependencies).not.toHaveProperty("typescript");
|
|
86
|
+
expect(manifest.devDependencies).not.toHaveProperty("@ember/library-tsconfig");
|
|
87
|
+
// No declarations are emitted without types
|
|
88
|
+
expect(JSON.stringify(manifest.exports)).not.toContain("types");
|
|
89
|
+
|
|
90
|
+
expect(await read(project, "tsdown.config.js")).toMatchInlineSnapshot(`
|
|
91
|
+
"import { defineConfig } from "tsdown";
|
|
92
|
+
import { ember } from "@nullvoxpopuli/ember-rolldown";
|
|
93
|
+
|
|
94
|
+
export default defineConfig({
|
|
95
|
+
entry: ["./src/index.js"],
|
|
96
|
+
dts: false,
|
|
97
|
+
plugins: [ember()],
|
|
98
|
+
});
|
|
99
|
+
"
|
|
100
|
+
`);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("removes the TS plugin from an existing project's babel config", async () => {
|
|
104
|
+
// The base doesn't emit a babel.config.js, but generation can run
|
|
105
|
+
// over an existing project that has one
|
|
106
|
+
let directory = await mktemp("existing-lib");
|
|
107
|
+
|
|
108
|
+
dirs.push(directory);
|
|
109
|
+
|
|
110
|
+
await mkdir(directory, { recursive: true });
|
|
111
|
+
await writeFile(
|
|
112
|
+
join(directory, "babel.config.js"),
|
|
113
|
+
`export default {
|
|
114
|
+
plugins: [
|
|
115
|
+
["@babel/plugin-transform-typescript", { allExtensions: true }],
|
|
116
|
+
["module:decorator-transforms"],
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
`,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
let existing = await generate({ directory, type: "library", name: "existing-lib" });
|
|
123
|
+
|
|
124
|
+
let babelConfig = await readFile(join(existing.directory, "babel.config.js"), "utf-8");
|
|
125
|
+
|
|
126
|
+
expect(babelConfig).not.toContain("@babel/plugin-transform-typescript");
|
|
127
|
+
expect(babelConfig).toContain("module:decorator-transforms");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("builds (with example source written in)", async () => {
|
|
131
|
+
await writeLibrarySource(project, "javascript");
|
|
132
|
+
await installAndBuild(project);
|
|
133
|
+
|
|
134
|
+
let output = await read(project, "dist/index.js");
|
|
135
|
+
|
|
136
|
+
// Published libraries ship precompileTemplate (the consuming app does
|
|
137
|
+
// the final compile), never wire format
|
|
138
|
+
expect(output).toContain("precompileTemplate");
|
|
139
|
+
expect(output).not.toContain("createTemplateFactory");
|
|
140
|
+
|
|
141
|
+
expect(await listFiles(join(project.directory, "dist"))).toMatchInlineSnapshot(`
|
|
142
|
+
[
|
|
143
|
+
"index.js",
|
|
144
|
+
"index.js.map",
|
|
145
|
+
]
|
|
146
|
+
`);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("TypeScript", () => {
|
|
151
|
+
let project: Project;
|
|
152
|
+
|
|
153
|
+
beforeAll(async () => {
|
|
154
|
+
project = await generate({ type: "library", name: "my-lib", layers: ["typescript"] });
|
|
155
|
+
dirs.push(project.directory);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("generates the expected files (src is empty)", async () => {
|
|
159
|
+
expect(await listFiles(project.directory)).toMatchInlineSnapshot(`
|
|
160
|
+
[
|
|
161
|
+
".gitignore",
|
|
162
|
+
"README.md",
|
|
163
|
+
"package.json",
|
|
164
|
+
"src/index.ts",
|
|
165
|
+
"tsconfig.json",
|
|
166
|
+
"tsdown.config.js",
|
|
167
|
+
]
|
|
168
|
+
`);
|
|
169
|
+
|
|
170
|
+
expect(await read(project, "src/index.ts")).toBe("");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("type checks", async () => {
|
|
174
|
+
await writeLibrarySource(project, "typescript");
|
|
175
|
+
await pinYukuParser(project);
|
|
176
|
+
|
|
177
|
+
let install = await execa("pnpm install", { cwd: project.directory, shell: true });
|
|
178
|
+
expect(install.exitCode).toBe(0);
|
|
179
|
+
|
|
180
|
+
let types = await execa("pnpm lint:types", { cwd: project.directory, shell: true });
|
|
181
|
+
expect(types.exitCode).toBe(0);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("builds, including declarations", async () => {
|
|
185
|
+
await installAndBuild(project);
|
|
186
|
+
|
|
187
|
+
let output = await read(project, "dist/index.js");
|
|
188
|
+
|
|
189
|
+
expect(output).toContain("precompileTemplate");
|
|
190
|
+
expect(output).not.toContain("createTemplateFactory");
|
|
191
|
+
|
|
192
|
+
let declarations = await read(project, "dist/index.d.ts");
|
|
193
|
+
|
|
194
|
+
expect(declarations).toContain("declare class Greeting extends Component<GreetingSignature>");
|
|
195
|
+
expect(declarations).toContain("declare const Badge: TOC<BadgeSignature>");
|
|
196
|
+
expect(declarations).toContain("declare function add(a: number, b: number): number");
|
|
197
|
+
|
|
198
|
+
expect(await listFiles(join(project.directory, "dist"))).toMatchInlineSnapshot(`
|
|
199
|
+
[
|
|
200
|
+
"index.d.ts",
|
|
201
|
+
"index.d.ts.map",
|
|
202
|
+
"index.js",
|
|
203
|
+
"index.js.map",
|
|
204
|
+
]
|
|
205
|
+
`);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("errors when tsconfig lacks isolatedDeclarations", async () => {
|
|
209
|
+
let tsconfigPath = join(project.directory, "tsconfig.json");
|
|
210
|
+
let original = await readFile(tsconfigPath, "utf-8");
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
await writeFile(
|
|
214
|
+
tsconfigPath,
|
|
215
|
+
JSON.stringify({
|
|
216
|
+
extends: "@ember/library-tsconfig",
|
|
217
|
+
include: ["src"],
|
|
218
|
+
compilerOptions: { allowJs: true, rootDir: "./src" },
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
let build = await execa("pnpm build", {
|
|
223
|
+
cwd: project.directory,
|
|
224
|
+
shell: true,
|
|
225
|
+
reject: false,
|
|
226
|
+
all: true,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
expect(build.exitCode).not.toBe(0);
|
|
230
|
+
expect(build.all).toContain('"compilerOptions.isolatedDeclarations": true');
|
|
231
|
+
} finally {
|
|
232
|
+
await writeFile(tsconfigPath, original);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|
|
@@ -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
|
|
23
|
+
await rm(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
25
24
|
}
|
|
26
25
|
});
|
|
27
26
|
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { hasInspectorSupport, wireInspectorSupport } from "#utils/inspector-support.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The Application definition is found structurally, so every shape an app
|
|
7
|
+
* author may have refactored their app file into gets wired the same way
|
|
8
|
+
* the freshly generated shape does.
|
|
9
|
+
*/
|
|
10
|
+
describe("wireInspectorSupport", () => {
|
|
11
|
+
it("wires the generated shape (export default class extends Application)", () => {
|
|
12
|
+
const code = [
|
|
13
|
+
`import Application from "ember-strict-application-resolver";`,
|
|
14
|
+
``,
|
|
15
|
+
`export default class App extends Application {`,
|
|
16
|
+
` modules = {`,
|
|
17
|
+
` ...import.meta.glob("./router.*", { eager: true }),`,
|
|
18
|
+
` };`,
|
|
19
|
+
`}`,
|
|
20
|
+
].join("\n");
|
|
21
|
+
|
|
22
|
+
expect(wireInspectorSupport(code, "app/app.ts")).toMatchInlineSnapshot(`
|
|
23
|
+
"import Application from "ember-strict-application-resolver";
|
|
24
|
+
import setupInspector from "@embroider/legacy-inspector-support/ember-source-4.12";
|
|
25
|
+
export default class App extends Application {
|
|
26
|
+
modules = { ...import.meta.glob("./router.*", { eager: true }) };
|
|
27
|
+
inspector = setupInspector(this);
|
|
28
|
+
}"
|
|
29
|
+
`);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("wires a named class exported in a separate statement", () => {
|
|
33
|
+
const code = [
|
|
34
|
+
`import Application from "ember-strict-application-resolver";`,
|
|
35
|
+
``,
|
|
36
|
+
`class App extends Application {`,
|
|
37
|
+
` modules = {};`,
|
|
38
|
+
`}`,
|
|
39
|
+
``,
|
|
40
|
+
`export default App;`,
|
|
41
|
+
].join("\n");
|
|
42
|
+
|
|
43
|
+
expect(wireInspectorSupport(code, "app/app.js")).toMatchInlineSnapshot(`
|
|
44
|
+
"import Application from "ember-strict-application-resolver";
|
|
45
|
+
import setupInspector from "@embroider/legacy-inspector-support/ember-source-4.12";
|
|
46
|
+
class App extends Application {
|
|
47
|
+
modules = { };
|
|
48
|
+
inspector = setupInspector(this);
|
|
49
|
+
}
|
|
50
|
+
export default App"
|
|
51
|
+
`);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("wires an anonymous default-exported class", () => {
|
|
55
|
+
const code = [
|
|
56
|
+
`import Application from "ember-strict-application-resolver";`,
|
|
57
|
+
``,
|
|
58
|
+
`export default class extends Application {}`,
|
|
59
|
+
].join("\n");
|
|
60
|
+
|
|
61
|
+
expect(wireInspectorSupport(code, "app/app.js")).toMatchInlineSnapshot(`
|
|
62
|
+
"import Application from "ember-strict-application-resolver";
|
|
63
|
+
import setupInspector from "@embroider/legacy-inspector-support/ember-source-4.12";
|
|
64
|
+
export default class extends Application {
|
|
65
|
+
inspector = setupInspector(this);
|
|
66
|
+
}"
|
|
67
|
+
`);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("wires a class expression assigned to a variable", () => {
|
|
71
|
+
const code = [
|
|
72
|
+
`import Application from "ember-strict-application-resolver";`,
|
|
73
|
+
``,
|
|
74
|
+
`const App = class extends Application {`,
|
|
75
|
+
` modules = {};`,
|
|
76
|
+
`};`,
|
|
77
|
+
``,
|
|
78
|
+
`export default App;`,
|
|
79
|
+
].join("\n");
|
|
80
|
+
|
|
81
|
+
expect(wireInspectorSupport(code, "app/app.js")).toMatchInlineSnapshot(`
|
|
82
|
+
"import Application from "ember-strict-application-resolver";
|
|
83
|
+
import setupInspector from "@embroider/legacy-inspector-support/ember-source-4.12";
|
|
84
|
+
const App = class extends Application {
|
|
85
|
+
modules = { };
|
|
86
|
+
inspector = setupInspector(this);
|
|
87
|
+
};
|
|
88
|
+
export default App"
|
|
89
|
+
`);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("wires a class extending @ember/application under a renamed import", () => {
|
|
93
|
+
const code = [
|
|
94
|
+
`import Base from "@ember/application";`,
|
|
95
|
+
``,
|
|
96
|
+
`export default class App extends Base {}`,
|
|
97
|
+
].join("\n");
|
|
98
|
+
|
|
99
|
+
expect(wireInspectorSupport(code, "app/app.ts")).toMatchInlineSnapshot(`
|
|
100
|
+
"import Base from "@ember/application";
|
|
101
|
+
import setupInspector from "@embroider/legacy-inspector-support/ember-source-4.12";
|
|
102
|
+
export default class App extends Base {
|
|
103
|
+
inspector = setupInspector(this);
|
|
104
|
+
}"
|
|
105
|
+
`);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("adds only the missing member when the import is already present", () => {
|
|
109
|
+
const code = [
|
|
110
|
+
`import wireUp from "@embroider/legacy-inspector-support/ember-source-4.12";`,
|
|
111
|
+
`import Application from "ember-strict-application-resolver";`,
|
|
112
|
+
``,
|
|
113
|
+
`export default class App extends Application {}`,
|
|
114
|
+
].join("\n");
|
|
115
|
+
|
|
116
|
+
expect(wireInspectorSupport(code, "app/app.ts")).toMatchInlineSnapshot(`
|
|
117
|
+
"import wireUp from "@embroider/legacy-inspector-support/ember-source-4.12";
|
|
118
|
+
import Application from "ember-strict-application-resolver";
|
|
119
|
+
export default class App extends Application {
|
|
120
|
+
inspector = wireUp(this);
|
|
121
|
+
}"
|
|
122
|
+
`);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("is idempotent", () => {
|
|
126
|
+
const code = [
|
|
127
|
+
`import Application from "ember-strict-application-resolver";`,
|
|
128
|
+
``,
|
|
129
|
+
`export default class App extends Application {`,
|
|
130
|
+
` modules = {};`,
|
|
131
|
+
`}`,
|
|
132
|
+
].join("\n");
|
|
133
|
+
|
|
134
|
+
const once = wireInspectorSupport(code, "app/app.ts");
|
|
135
|
+
const twice = wireInspectorSupport(once, "app/app.ts");
|
|
136
|
+
|
|
137
|
+
expect(twice).toBe(once);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("leaves a module without an Application definition alone", () => {
|
|
141
|
+
const code = [`export function noApplicationHere() {}`, ``].join("\n");
|
|
142
|
+
|
|
143
|
+
expect(wireInspectorSupport(code, "app/app.js")).toBe(code);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("leaves a class that does not extend an Application import alone", () => {
|
|
147
|
+
const code = [
|
|
148
|
+
`import Component from "@glimmer/component";`,
|
|
149
|
+
``,
|
|
150
|
+
`export default class App extends Component {}`,
|
|
151
|
+
].join("\n");
|
|
152
|
+
|
|
153
|
+
expect(wireInspectorSupport(code, "app/app.ts")).toBe(code);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("hasInspectorSupport", () => {
|
|
158
|
+
it("is false before wiring and true after", () => {
|
|
159
|
+
const code = [
|
|
160
|
+
`import Application from "ember-strict-application-resolver";`,
|
|
161
|
+
``,
|
|
162
|
+
`export default class App extends Application {}`,
|
|
163
|
+
].join("\n");
|
|
164
|
+
|
|
165
|
+
expect(hasInspectorSupport(code, "app/app.ts")).toBe(false);
|
|
166
|
+
expect(hasInspectorSupport(wireInspectorSupport(code, "app/app.ts"), "app/app.ts")).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("is false when only the import is present", () => {
|
|
170
|
+
const code = [
|
|
171
|
+
`import setupInspector from "@embroider/legacy-inspector-support/ember-source-4.12";`,
|
|
172
|
+
`import Application from "ember-strict-application-resolver";`,
|
|
173
|
+
``,
|
|
174
|
+
`export default class App extends Application {}`,
|
|
175
|
+
].join("\n");
|
|
176
|
+
|
|
177
|
+
expect(hasInspectorSupport(code, "app/app.ts")).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
});
|