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.
- package/.github/workflows/ci.yml +17 -2
- package/.release-plan.json +84 -4
- package/CHANGELOG.md +68 -0
- package/README.md +29 -0
- package/package.json +4 -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 +115 -0
- package/packages/rolldown/index.ts +56 -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/define-config.ts +32 -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 +6 -0
- package/src/bases/minimal-library/index.js +158 -2
- package/src/cli/args.js +7 -1
- package/src/cli/index.js +90 -16
- package/src/cli/questions/layers.js +5 -2
- package/src/cli/questions/project-type.js +0 -5
- package/src/cli/questions/write.js +169 -0
- package/src/index.js +1 -0
- 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/stage.js +317 -0
- package/src/utils/tsdown-config.js +85 -0
- package/src/utils/types.ts +5 -1
- package/test/ember.nvp/base-library.test.ts +232 -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 +103 -0
- package/test/ember.nvp/layer-qunit.test.ts +376 -15
- 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/ember.nvp/stage.test.ts +188 -0
- package/test/test-helpers/permutate.test.ts +17 -0
- package/test/test-helpers.ts +20 -7
- 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
|
@@ -1,7 +1,44 @@
|
|
|
1
|
+
import { applyFolderTo } from "#utils/fs.js";
|
|
1
2
|
import { getLatest } from "#utils/npm.js";
|
|
2
|
-
import { packageJson
|
|
3
|
+
import { packageJson } from "ember-apply";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
|
|
6
|
+
const deps = {
|
|
7
|
+
"@ember/test-helpers": "^5.4.3",
|
|
8
|
+
"@vitest/browser": "^4.1.10",
|
|
9
|
+
"@vitest/browser-webdriverio": "^4.1.10",
|
|
10
|
+
"@vitest/ui": "^4.1.10",
|
|
11
|
+
"ember-vitest": "^0.4.0",
|
|
12
|
+
vitest: "^4.1.10",
|
|
13
|
+
webdriverio: "^9.29.1",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Apps already get these from their base. The babel plugins are resolved
|
|
17
|
+
// by name from the project by @nullvoxpopuli/ember-vite's no-config babel
|
|
18
|
+
// fallback (decorator-transforms is already a base dependency).
|
|
19
|
+
const libraryDeps = {
|
|
20
|
+
"@babel/plugin-transform-typescript": "^7.28.10",
|
|
21
|
+
"@nullvoxpopuli/ember-vite": "workspace:*",
|
|
22
|
+
"babel-plugin-ember-template-compilation": "^4.0.0",
|
|
23
|
+
vite: "^8.0.14",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const scripts = {
|
|
27
|
+
test: "vitest run",
|
|
28
|
+
"test:watch": "vitest",
|
|
29
|
+
"test:ui": "vitest --ui",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {import('#utils/project.js').Project} project
|
|
34
|
+
*/
|
|
35
|
+
function depsFor(project) {
|
|
36
|
+
return {
|
|
37
|
+
...deps,
|
|
38
|
+
...(project.type === "library" ? libraryDeps : {}),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
5
42
|
/**
|
|
6
43
|
* @type {import('#types').Layer}
|
|
7
44
|
*/
|
|
@@ -9,34 +46,11 @@ export default {
|
|
|
9
46
|
label: "Vitest",
|
|
10
47
|
|
|
11
48
|
async run(project) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
await packageJson.
|
|
17
|
-
await getLatest({
|
|
18
|
-
"@ember/test-helpers": "^5.4.1",
|
|
19
|
-
"@ember/test-waiters": "^4.1.1",
|
|
20
|
-
"@testing-library/dom": "^10.4.1",
|
|
21
|
-
"ember-vitest": "^0.3.3",
|
|
22
|
-
vitest: "^4.0.0",
|
|
23
|
-
"@vitest/ui": "^4.0.0",
|
|
24
|
-
"@vitest/browser": "^4.0.0",
|
|
25
|
-
"@vitest/browser-webdriverio": "^4.0.0",
|
|
26
|
-
webdriverio: "^9.23.2",
|
|
27
|
-
}),
|
|
28
|
-
project.directory,
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
// Add scripts
|
|
32
|
-
await packageJson.addScripts(
|
|
33
|
-
{
|
|
34
|
-
test: "vitest run",
|
|
35
|
-
"test:watch": "vitest",
|
|
36
|
-
"test:ui": "vitest --ui",
|
|
37
|
-
},
|
|
38
|
-
project.directory,
|
|
39
|
-
);
|
|
49
|
+
await applyFolderTo(join(import.meta.dirname, "files"), project);
|
|
50
|
+
|
|
51
|
+
await packageJson.addDevDependencies(await getLatest(depsFor(project)), project.directory);
|
|
52
|
+
|
|
53
|
+
await packageJson.addScripts(scripts, project.directory);
|
|
40
54
|
},
|
|
41
55
|
|
|
42
56
|
/**
|
|
@@ -52,15 +66,39 @@ export default {
|
|
|
52
66
|
* @returns {Promise<boolean>}
|
|
53
67
|
*/
|
|
54
68
|
async isSetup(project, explain) {
|
|
55
|
-
const reasons = [
|
|
69
|
+
const reasons = [];
|
|
70
|
+
|
|
71
|
+
if (!project.hasFile("vitest.config.mjs")) {
|
|
72
|
+
if (!explain) return false;
|
|
73
|
+
|
|
74
|
+
reasons.push("vitest.config.mjs is missing");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let manifest = await packageJson.read(project.directory);
|
|
78
|
+
|
|
79
|
+
for (let script of Object.keys(scripts)) {
|
|
80
|
+
if (!manifest.scripts?.[script]) {
|
|
81
|
+
if (!explain) return false;
|
|
82
|
+
|
|
83
|
+
reasons.push(`package.json is missing the "${script}" script`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (let dep of Object.keys(depsFor(project))) {
|
|
88
|
+
if (!manifest.devDependencies?.[dep]) {
|
|
89
|
+
if (!explain) return false;
|
|
90
|
+
|
|
91
|
+
reasons.push(`package.json is missing ${dep} in devDependencies`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
56
94
|
|
|
57
95
|
if (explain) {
|
|
58
96
|
return {
|
|
59
|
-
isSetup:
|
|
97
|
+
isSetup: reasons.length === 0,
|
|
60
98
|
reasons,
|
|
61
99
|
};
|
|
62
100
|
}
|
|
63
101
|
|
|
64
|
-
return
|
|
102
|
+
return reasons.length === 0;
|
|
65
103
|
},
|
|
66
104
|
};
|
package/src/utils/babel.js
CHANGED
|
@@ -11,11 +11,12 @@ export async function hasConfiguredTSBabel(project) {
|
|
|
11
11
|
/**
|
|
12
12
|
* @param {import('./project.js').Project} project
|
|
13
13
|
* @param {string} pluginName
|
|
14
|
+
* @param {string} [configPath] which babel config to inspect
|
|
14
15
|
*/
|
|
15
|
-
export async function hasConfiguredPlugin(project, pluginName) {
|
|
16
|
+
export async function hasConfiguredPlugin(project, pluginName, configPath = "babel.config.js") {
|
|
16
17
|
let hasPlugin = false;
|
|
17
18
|
|
|
18
|
-
await js.analyze(project.path(
|
|
19
|
+
await js.analyze(project.path(configPath), async ({ root, j }) => {
|
|
19
20
|
root
|
|
20
21
|
.find(j.ArrayExpression, {
|
|
21
22
|
elements: {
|
|
@@ -33,9 +34,10 @@ export async function hasConfiguredPlugin(project, pluginName) {
|
|
|
33
34
|
/**
|
|
34
35
|
* @param {import('./project.js').Project} project
|
|
35
36
|
* @param {string} pluginName
|
|
37
|
+
* @param {string} [configPath] which babel config to modify
|
|
36
38
|
*/
|
|
37
|
-
export async function removeConfiguredPlugin(project, pluginName) {
|
|
38
|
-
await js.transform(project.path(
|
|
39
|
+
export async function removeConfiguredPlugin(project, pluginName, configPath = "babel.config.js") {
|
|
40
|
+
await js.transform(project.path(configPath), async ({ root, j }) => {
|
|
39
41
|
root
|
|
40
42
|
.find(j.ArrayExpression, {
|
|
41
43
|
elements: {
|
|
@@ -56,9 +58,10 @@ export async function removeConfiguredPlugin(project, pluginName) {
|
|
|
56
58
|
/**
|
|
57
59
|
* @param {import('./project.js').Project} project
|
|
58
60
|
* @param {string} plugin
|
|
61
|
+
* @param {string} [configPath] which babel config to modify
|
|
59
62
|
*/
|
|
60
|
-
export async function prependPlugin(project, plugin) {
|
|
61
|
-
await js.transform(project.path(
|
|
63
|
+
export async function prependPlugin(project, plugin, configPath = "babel.config.js") {
|
|
64
|
+
await js.transform(project.path(configPath), async ({ root, j }) => {
|
|
62
65
|
root
|
|
63
66
|
.find(j.Property, {
|
|
64
67
|
key: { name: "plugins" },
|
package/src/utils/fs.js
CHANGED
|
@@ -3,6 +3,14 @@ import { readFile, glob, mkdir, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { join, dirname } from "node:path";
|
|
4
4
|
import { parse as parsePath } from "node:path";
|
|
5
5
|
import { removeTypes } from "./remove-types.js";
|
|
6
|
+
import { rewriteImportsToMatchFiles } from "./rewrite-imports.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `**\/*` alone skips dotfiles, which bases legitimately provide
|
|
10
|
+
* (`.gitignore`, `.env.development`, ...): the extra patterns match
|
|
11
|
+
* dotfiles at any depth and files inside dot-directories (`.github/...`).
|
|
12
|
+
*/
|
|
13
|
+
const EVERY_FILE = ["**/*", "**/.*", "**/.*/**/*"];
|
|
6
14
|
|
|
7
15
|
/**
|
|
8
16
|
*
|
|
@@ -15,7 +23,10 @@ import { removeTypes } from "./remove-types.js";
|
|
|
15
23
|
* @param {{to: import('#utils/project.js').Project, process: (data: { entry: string, contents: string }) => string | Promise<void>}} options sub folder within the target project to copy the contents to
|
|
16
24
|
*/
|
|
17
25
|
export async function applyFolder(from, options) {
|
|
18
|
-
for await (const entry of glob(
|
|
26
|
+
for await (const entry of glob(EVERY_FILE, {
|
|
27
|
+
exclude: ["**/node_modules", "**/dist"],
|
|
28
|
+
cwd: from,
|
|
29
|
+
})) {
|
|
19
30
|
let sourcePath = join(from, entry);
|
|
20
31
|
let targetPath = join(options.to.directory, entry);
|
|
21
32
|
let directory = dirname(targetPath);
|
|
@@ -42,6 +53,19 @@ export async function applyFolder(from, options) {
|
|
|
42
53
|
* @param {import('#utils/project.js').Project} project
|
|
43
54
|
*/
|
|
44
55
|
export async function applyFolderTo(from, project) {
|
|
56
|
+
/**
|
|
57
|
+
* Files we wrote, to be import-rewritten in a second pass.
|
|
58
|
+
*
|
|
59
|
+
* Rewriting has to wait until every file is on disk: it checks which
|
|
60
|
+
* file a specifier actually resolves to, so rewriting while the folder
|
|
61
|
+
* is still half-applied depends on iteration order (e.g. `index.js`
|
|
62
|
+
* pointing at `./utils/math.ts` before `math.js` exists would be left
|
|
63
|
+
* alone, emitting a broken import).
|
|
64
|
+
*
|
|
65
|
+
* @type {string[]}
|
|
66
|
+
*/
|
|
67
|
+
let written = [];
|
|
68
|
+
|
|
45
69
|
await applyFolder(from, {
|
|
46
70
|
to: project,
|
|
47
71
|
async process({ entry, contents }) {
|
|
@@ -65,9 +89,9 @@ export async function applyFolderTo(from, project) {
|
|
|
65
89
|
*/
|
|
66
90
|
return;
|
|
67
91
|
}
|
|
68
|
-
let newContents = await removeTypes(ext, contents);
|
|
69
92
|
|
|
70
|
-
await writeFile(filePath,
|
|
93
|
+
await writeFile(filePath, await removeTypes(ext, contents));
|
|
94
|
+
written.push(filePath);
|
|
71
95
|
return;
|
|
72
96
|
}
|
|
73
97
|
}
|
|
@@ -79,7 +103,24 @@ export async function applyFolderTo(from, project) {
|
|
|
79
103
|
*/
|
|
80
104
|
return;
|
|
81
105
|
}
|
|
106
|
+
|
|
82
107
|
await writeFile(entry, contents);
|
|
108
|
+
written.push(entry);
|
|
83
109
|
},
|
|
84
110
|
});
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Imports must always match the emitted files (and carry their
|
|
114
|
+
* extension). The util decides which files it applies to and is a
|
|
115
|
+
* no-op for everything else. Files that already existed are the
|
|
116
|
+
* user's; we leave them alone.
|
|
117
|
+
*/
|
|
118
|
+
for (let filePath of written) {
|
|
119
|
+
let contents = await readFile(filePath, "utf-8");
|
|
120
|
+
let rewritten = rewriteImportsToMatchFiles(contents, filePath);
|
|
121
|
+
|
|
122
|
+
if (rewritten !== contents) {
|
|
123
|
+
await writeFile(filePath, rewritten);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
85
126
|
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { toTree, print, statement } from "ember-estree";
|
|
2
|
+
|
|
3
|
+
export const INSPECTOR_PACKAGE = "@embroider/legacy-inspector-support";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The `ember-source-4.12` flavor covers ember-source 4.12 and newer,
|
|
7
|
+
* which includes the ember-source the app base generates with.
|
|
8
|
+
*/
|
|
9
|
+
export const INSPECTOR_ENTRY = `${INSPECTOR_PACKAGE}/ember-source-4.12`;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Modules whose default export is the Application (sub)class an app
|
|
13
|
+
* extends. The class extending one of these is where inspector support
|
|
14
|
+
* gets attached.
|
|
15
|
+
*/
|
|
16
|
+
const APPLICATION_MODULES = new Set(["ember-strict-application-resolver", "@ember/application"]);
|
|
17
|
+
|
|
18
|
+
/** @typedef {import('ember-estree').ASTNode} ASTNode */
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {unknown} value
|
|
22
|
+
* @returns {ASTNode | null}
|
|
23
|
+
*/
|
|
24
|
+
function asNode(value) {
|
|
25
|
+
return value && typeof value === "object" ? /** @type {ASTNode} */ (value) : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {unknown} value
|
|
30
|
+
* @returns {string} an Identifier node's name ("" when it isn't one)
|
|
31
|
+
*/
|
|
32
|
+
function nameOf(value) {
|
|
33
|
+
let node = asNode(value);
|
|
34
|
+
|
|
35
|
+
return node?.type === "Identifier" && typeof node.name === "string" ? node.name : "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {ASTNode} importNode
|
|
40
|
+
* @returns {ASTNode[]}
|
|
41
|
+
*/
|
|
42
|
+
function specifiersOf(importNode) {
|
|
43
|
+
return Array.isArray(importNode.specifiers)
|
|
44
|
+
? /** @type {ASTNode[]} */ (importNode.specifiers)
|
|
45
|
+
: [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {ASTNode} classNode
|
|
50
|
+
* @returns {ASTNode[]} the class's members
|
|
51
|
+
*/
|
|
52
|
+
function classMembers(classNode) {
|
|
53
|
+
let members = asNode(classNode.body)?.body;
|
|
54
|
+
|
|
55
|
+
return Array.isArray(members) ? /** @type {ASTNode[]} */ (members) : [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Everything wiring and detection need to know about the module: its
|
|
60
|
+
* import declarations, the import from {@link INSPECTOR_PACKAGE} (any
|
|
61
|
+
* subpath), the class extending an Application import, and that class's
|
|
62
|
+
* `inspector` member.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} code
|
|
65
|
+
* @param {string} filePath
|
|
66
|
+
*/
|
|
67
|
+
function analyze(code, filePath) {
|
|
68
|
+
/** @type {ASTNode[]} */
|
|
69
|
+
let imports = [];
|
|
70
|
+
/** @type {ASTNode[]} */
|
|
71
|
+
let classes = [];
|
|
72
|
+
|
|
73
|
+
// (not templateOnly, so this is always a FileNode)
|
|
74
|
+
let tree = /** @type {import('ember-estree').FileNode} */ (
|
|
75
|
+
toTree(code, {
|
|
76
|
+
filePath,
|
|
77
|
+
visitors: {
|
|
78
|
+
ImportDeclaration: (node) => imports.push(node),
|
|
79
|
+
ClassDeclaration: (node) => classes.push(node),
|
|
80
|
+
ClassExpression: (node) => classes.push(node),
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
/** @type {Set<string>} */
|
|
86
|
+
let applicationLocals = new Set();
|
|
87
|
+
/** @type {ASTNode | null} */
|
|
88
|
+
let inspectorImport = null;
|
|
89
|
+
|
|
90
|
+
for (let node of imports) {
|
|
91
|
+
let source = asNode(node.source);
|
|
92
|
+
let from = source?.type === "Literal" && typeof source.value === "string" ? source.value : "";
|
|
93
|
+
|
|
94
|
+
if (APPLICATION_MODULES.has(from)) {
|
|
95
|
+
for (let specifier of specifiersOf(node)) {
|
|
96
|
+
let local = nameOf(specifier.local);
|
|
97
|
+
|
|
98
|
+
if (local) applicationLocals.add(local);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (from === INSPECTOR_PACKAGE || from.startsWith(`${INSPECTOR_PACKAGE}/`)) {
|
|
103
|
+
inspectorImport ??= node;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let applicationClass =
|
|
108
|
+
classes.find((node) => applicationLocals.has(nameOf(node.superClass))) ?? null;
|
|
109
|
+
|
|
110
|
+
let inspectorMember = applicationClass
|
|
111
|
+
? (classMembers(applicationClass).find(
|
|
112
|
+
(member) => !member.static && !member.computed && nameOf(member.key) === "inspector",
|
|
113
|
+
) ?? null)
|
|
114
|
+
: null;
|
|
115
|
+
|
|
116
|
+
return { tree, imports, inspectorImport, applicationClass, inspectorMember };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Whether the module already wires inspector support: it imports
|
|
121
|
+
* {@link INSPECTOR_PACKAGE} and its Application class carries the
|
|
122
|
+
* `inspector` member.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} code
|
|
125
|
+
* @param {string} filePath
|
|
126
|
+
* @returns {boolean}
|
|
127
|
+
*/
|
|
128
|
+
export function hasInspectorSupport(code, filePath) {
|
|
129
|
+
let { inspectorImport, inspectorMember } = analyze(code, filePath);
|
|
130
|
+
|
|
131
|
+
return Boolean(inspectorImport && inspectorMember);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Wires ember-inspector support into the module containing the app's
|
|
136
|
+
* Application definition, the way the official app blueprint does:
|
|
137
|
+
*
|
|
138
|
+
* ```ts
|
|
139
|
+
* import setupInspector from "@embroider/legacy-inspector-support/ember-source-4.12";
|
|
140
|
+
*
|
|
141
|
+
* export default class App extends Application {
|
|
142
|
+
* inspector = setupInspector(this);
|
|
143
|
+
* }
|
|
144
|
+
* ```
|
|
145
|
+
*
|
|
146
|
+
* The Application definition is found structurally -- any class
|
|
147
|
+
* (declaration or expression, named, anonymous, or assigned to a
|
|
148
|
+
* variable) extending a binding imported from an Application-providing
|
|
149
|
+
* module -- so user-restructured app files are wired the same as
|
|
150
|
+
* freshly generated ones.
|
|
151
|
+
*
|
|
152
|
+
* Idempotent: already-wired (or partially wired) modules keep their
|
|
153
|
+
* existing import name and member. Returns the code unchanged when
|
|
154
|
+
* there is nothing to do -- including when no Application definition
|
|
155
|
+
* can be found.
|
|
156
|
+
*
|
|
157
|
+
* @param {string} code
|
|
158
|
+
* @param {string} filePath
|
|
159
|
+
* @returns {string}
|
|
160
|
+
*/
|
|
161
|
+
export function wireInspectorSupport(code, filePath) {
|
|
162
|
+
let { tree, imports, inspectorImport, applicationClass, inspectorMember } = analyze(
|
|
163
|
+
code,
|
|
164
|
+
filePath,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (!applicationClass) return code;
|
|
168
|
+
if (inspectorImport && inspectorMember) return code;
|
|
169
|
+
|
|
170
|
+
let localName = "setupInspector";
|
|
171
|
+
|
|
172
|
+
if (inspectorImport) {
|
|
173
|
+
let defaultSpecifier = specifiersOf(inspectorImport).find(
|
|
174
|
+
(specifier) => specifier.type === "ImportDefaultSpecifier",
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
localName = nameOf(defaultSpecifier?.local);
|
|
178
|
+
|
|
179
|
+
// an import that binds no default gives the member nothing to call
|
|
180
|
+
if (!localName) return code;
|
|
181
|
+
} else {
|
|
182
|
+
let body = /** @type {ASTNode[]} */ (tree.program.body);
|
|
183
|
+
let lastImport = imports.at(-1);
|
|
184
|
+
let insertAt = lastImport ? body.indexOf(lastImport) + 1 : 0;
|
|
185
|
+
|
|
186
|
+
body.splice(insertAt, 0, statement`import ${localName} from "${INSPECTOR_ENTRY}";`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!inspectorMember) {
|
|
190
|
+
let [member] = classMembers(statement`class _ { inspector = ${localName}(this); }`);
|
|
191
|
+
|
|
192
|
+
if (member) classMembers(applicationClass).push(member);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return print(tree);
|
|
196
|
+
}
|
package/src/utils/npm.js
CHANGED
|
@@ -7,6 +7,18 @@ import { join, resolve } from "node:path";
|
|
|
7
7
|
*/
|
|
8
8
|
const CACHE = {};
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Local, not-yet-published workspace packages that must be `link:`ed rather
|
|
12
|
+
* than resolved from the registry. Maps the published package name to its
|
|
13
|
+
* folder under `packages/`.
|
|
14
|
+
*
|
|
15
|
+
* @type {{ [name: string]: string }}
|
|
16
|
+
*/
|
|
17
|
+
const LOCAL_PACKAGES = {
|
|
18
|
+
"@nullvoxpopuli/ember-vite": "vite",
|
|
19
|
+
"@nullvoxpopuli/ember-rolldown": "rolldown",
|
|
20
|
+
};
|
|
21
|
+
|
|
10
22
|
/**
|
|
11
23
|
* @param {{ [name: string]: string }} deps map of dep name to semver range
|
|
12
24
|
*/
|
|
@@ -25,18 +37,20 @@ export async function getLatest(deps) {
|
|
|
25
37
|
|
|
26
38
|
/**
|
|
27
39
|
* HACK FOR CI.
|
|
28
|
-
* In practice,
|
|
29
|
-
* Only the local, not-yet-published
|
|
30
|
-
*
|
|
40
|
+
* In practice, these packages will be published separately, and those
|
|
41
|
+
* versions will be used. Only the local, not-yet-published
|
|
42
|
+
* @nullvoxpopuli/ember-* packages need the link; every other dependency
|
|
43
|
+
* must still resolve its real version.
|
|
31
44
|
*
|
|
32
45
|
* We use `link:` (a symlink) rather than `file:` (a copy into the store)
|
|
33
|
-
* on purpose: the
|
|
46
|
+
* on purpose: the packages ship TypeScript source, and Node 24 refuses to
|
|
34
47
|
* strip types for files physically located under node_modules. A symlink
|
|
35
|
-
* makes Node resolve the realpath to packages
|
|
48
|
+
* makes Node resolve the realpath to packages/* (outside node_modules),
|
|
36
49
|
* so its `.ts` runs directly.
|
|
37
50
|
*/
|
|
38
|
-
if (needsLocalLink && dep
|
|
39
|
-
version =
|
|
51
|
+
if (needsLocalLink && LOCAL_PACKAGES[dep]) {
|
|
52
|
+
version =
|
|
53
|
+
"link:" + resolve(join(import.meta.dirname, "../../packages", LOCAL_PACKAGES[dep]));
|
|
40
54
|
} else {
|
|
41
55
|
if (range == "workspace:*") {
|
|
42
56
|
range = "latest";
|
|
@@ -9,6 +9,11 @@ import { substringBytes } from "./buffer.js";
|
|
|
9
9
|
* @returns
|
|
10
10
|
*/
|
|
11
11
|
export async function removeTypes(extension, code) {
|
|
12
|
+
// Nothing to transform (babel-remove-types throws on empty input)
|
|
13
|
+
if (code.trim() === "") {
|
|
14
|
+
return code;
|
|
15
|
+
}
|
|
16
|
+
|
|
12
17
|
if (extension === ".gts") {
|
|
13
18
|
return await wrappedRemoveTypes(code, babelRemoveTypes);
|
|
14
19
|
}
|
|
@@ -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
|
+
}
|