ember.nvp 1.9.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.release-plan.json +4 -4
- package/CHANGELOG.md +31 -0
- package/README.md +65 -4
- package/package.json +2 -1
- package/packages/rolldown/package.json +6 -2
- package/packages/rolldown/src/dts.test.ts +105 -1
- package/packages/rolldown/src/isolated-declarations.ts +67 -61
- package/packages/rolldown/src/transform.test.ts +212 -15
- package/packages/rolldown/src/transform.ts +106 -26
- package/src/cli/args.js +105 -15
- package/src/cli/generator.js +2 -1
- package/src/cli/help.js +76 -16
- package/src/cli/index.js +15 -0
- package/src/cli/questions/layer-options.js +130 -0
- package/src/cli/questions/validate-option.js +106 -0
- package/src/layers/eslint-bundled/app.test.ts +182 -0
- package/src/layers/eslint-bundled/index.js +172 -0
- package/src/layers/expect-type/index.js +11 -1
- package/src/layers/github-actions/app.test.ts +1 -1
- package/src/layers/typescript/app.test.ts +6 -0
- package/src/layers/typescript/index.js +48 -148
- package/src/layers/typescript/typescript-6.js +102 -0
- package/src/layers/typescript/typescript-7.js +177 -0
- package/src/utils/manifest.js +13 -0
- package/src/utils/npm.js +27 -0
- package/src/utils/project.js +23 -0
- package/src/utils/types.ts +52 -1
- package/src/utils/typescript.js +147 -0
- package/test/ember.nvp/base-library.test.ts +11 -0
- package/test/ember.nvp/cli.test.ts +90 -3
- package/test/ember.nvp/layer-options.test.ts +249 -0
- package/test/ember.nvp/layer-readme.test.ts +4 -2
- package/test/ember.nvp/layer-typescript.test.ts +302 -0
- package/test/ember.nvp/layers.test.ts +1 -2
- package/test/ember.nvp/permutations/shared.ts +3 -4
- package/test/ember.nvp/validate-option.test.ts +152 -0
- package/test/test-helpers/permutate.test.ts +2 -7
- package/test/test-helpers.ts +8 -1
- package/src/layers/eslint-bundled-ember/index.js +0 -91
- package/src/layers/eslint-bundled-nvp/app.test.ts +0 -91
- package/src/layers/eslint-bundled-nvp/index.js +0 -91
- /package/src/layers/{eslint-bundled-ember/files → eslint-bundled/files/ember}/eslint.config.js +0 -0
- /package/src/layers/{eslint-bundled-nvp/files → eslint-bundled/files/nvp}/eslint.config.js +0 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { packageJson, tsconfig } from "ember-apply";
|
|
2
|
+
import { hasConfiguredTSBabel } from "#utils/babel.js";
|
|
3
|
+
import { getLatest } from "#utils/npm.js";
|
|
4
|
+
import {
|
|
5
|
+
TYPESCRIPT_7,
|
|
6
|
+
addTSConfig,
|
|
7
|
+
hasTypeScript6,
|
|
8
|
+
syncTypeScript6,
|
|
9
|
+
updateBabelConfig,
|
|
10
|
+
} from "#utils/typescript.js";
|
|
11
|
+
|
|
12
|
+
const MAPPER = "ember-content-mapper";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* TypeScript 7 does not load tsserver plugins.
|
|
16
|
+
* The content mapper replaces this one.
|
|
17
|
+
*/
|
|
18
|
+
const TSSERVER_PLUGIN = "@glint/tsserver-plugin";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `@typescript/native` owns the `tsc` bin.
|
|
22
|
+
* `typescript` is left to eslint, which needs TypeScript 6, see `syncTypeScript6`.
|
|
23
|
+
*
|
|
24
|
+
* The mapper transforms with `@glint/ember-tsc` and references its types.
|
|
25
|
+
* `@glint/template` types the signatures written by hand.
|
|
26
|
+
*/
|
|
27
|
+
const sharedDeps = {
|
|
28
|
+
"@glint/ember-tsc": "^1.11.0",
|
|
29
|
+
"@glint/template": "^1.7.3",
|
|
30
|
+
"@typescript/native": TYPESCRIPT_7,
|
|
31
|
+
[MAPPER]: "^0.3.1",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const appDeps = {
|
|
35
|
+
// Apps strip types via their own babel.config.js.
|
|
36
|
+
// Libraries have no babel config: ember() handles type stripping.
|
|
37
|
+
"@babel/plugin-transform-typescript": "^7.28.5",
|
|
38
|
+
"@ember/app-tsconfig": "^2.0.0",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const libraryDeps = {
|
|
42
|
+
"@ember/library-tsconfig": "^2.0.0",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const scripts = {
|
|
46
|
+
"lint:types": "tsc --noEmit --runExternalCode",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {import('#utils/project.js').Project} project
|
|
51
|
+
*/
|
|
52
|
+
function depsFor(project) {
|
|
53
|
+
return {
|
|
54
|
+
...sharedDeps,
|
|
55
|
+
...(project.isLibrary ? libraryDeps : appDeps),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {Record<string, any>} config
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
function hasContentMapper(config) {
|
|
64
|
+
return (
|
|
65
|
+
Array.isArray(config.contentMappers) &&
|
|
66
|
+
config.contentMappers.some((/** @type {any} */ mapper) => mapper?.package === MAPPER)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {import('#utils/project.js').Project} project
|
|
72
|
+
*/
|
|
73
|
+
export async function run(project) {
|
|
74
|
+
await addTSConfig(project);
|
|
75
|
+
await updateTSConfig(project);
|
|
76
|
+
await updatePackageJson(project);
|
|
77
|
+
await syncTypeScript6(project);
|
|
78
|
+
await updateBabelConfig(project);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {import('#utils/project.js').Project} project
|
|
83
|
+
* @param {Record<string, any>} manifest
|
|
84
|
+
* @returns {Promise<string[]>} why TypeScript 7 is not set up
|
|
85
|
+
*/
|
|
86
|
+
export async function reasons(project, manifest) {
|
|
87
|
+
const reasons = [];
|
|
88
|
+
|
|
89
|
+
if (!project.hasFile("tsconfig.json")) {
|
|
90
|
+
reasons.push("tsconfig.json is missing");
|
|
91
|
+
} else if (!hasContentMapper(await tsconfig.read(project.directory))) {
|
|
92
|
+
reasons.push(`tsconfig.json is missing the ${MAPPER} entry in contentMappers`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Only projects with their own babel config need the TS plugin in it.
|
|
96
|
+
// Without one (libraries), ember() strips types.
|
|
97
|
+
if (project.hasFile("babel.config.js") && !(await hasConfiguredTSBabel(project))) {
|
|
98
|
+
reasons.push(`babel.config is missing @babel/plugin-transform-typescript`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!manifest.scripts?.["lint:types"]?.includes("--runExternalCode")) {
|
|
102
|
+
reasons.push(`package.json's "lint:types" script is missing --runExternalCode`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (let dep of Object.keys(depsFor(project))) {
|
|
106
|
+
if (!manifest.devDependencies?.[dep]) {
|
|
107
|
+
reasons.push(`package.json is missing ${dep} in devDependencies`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (manifest.devDependencies?.typescript && !hasTypeScript6(manifest)) {
|
|
112
|
+
reasons.push(`package.json's typescript must be @typescript/typescript6, or tsc is ambiguous`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return reasons;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {import('#utils/project.js').Project} project
|
|
120
|
+
*/
|
|
121
|
+
export function readme(project) {
|
|
122
|
+
return `### TypeScript
|
|
123
|
+
|
|
124
|
+
This project uses TypeScript 7 and [ember-content-mapper](https://github.com/NullVoxPopuli/ember-content-mapper) for static type checking.
|
|
125
|
+
|
|
126
|
+
- \`${project.runPrefix} lint:types\` - Typecheck code with TypeScript
|
|
127
|
+
- Imports of \`.gts\` and \`.gjs\` modules must include the extension
|
|
128
|
+
- Editor setup: see [ember-content-mapper's editors section](https://github.com/NullVoxPopuli/ember-content-mapper#editors)`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {import('#utils/project.js').Project} project
|
|
133
|
+
*/
|
|
134
|
+
async function updateTSConfig(project) {
|
|
135
|
+
let config = await tsconfig.read(project.directory);
|
|
136
|
+
|
|
137
|
+
if (hasContentMapper(config) && !hasTSServerPlugin(config)) return;
|
|
138
|
+
|
|
139
|
+
await tsconfig.modify((json) => {
|
|
140
|
+
if (!hasContentMapper(json)) {
|
|
141
|
+
json.contentMappers ||= [];
|
|
142
|
+
json.contentMappers.push({ package: MAPPER, extensions: [".gts", ".gjs"] });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (hasTSServerPlugin(json)) {
|
|
146
|
+
json.compilerOptions.plugins = json.compilerOptions.plugins.filter(
|
|
147
|
+
(/** @type {any} */ plugin) => plugin?.name !== TSSERVER_PLUGIN,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
if (json.compilerOptions.plugins.length === 0) {
|
|
151
|
+
delete json.compilerOptions.plugins;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}, project.directory);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {Record<string, any>} config
|
|
159
|
+
* @returns {boolean}
|
|
160
|
+
*/
|
|
161
|
+
function hasTSServerPlugin(config) {
|
|
162
|
+
let plugins = config.compilerOptions?.plugins;
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
Array.isArray(plugins) &&
|
|
166
|
+
plugins.some((/** @type {any} */ plugin) => plugin?.name === TSSERVER_PLUGIN)
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @param {import('#utils/project.js').Project} project
|
|
172
|
+
*/
|
|
173
|
+
async function updatePackageJson(project) {
|
|
174
|
+
await packageJson.addScripts(scripts, project.directory);
|
|
175
|
+
await packageJson.addDevDependencies(await getLatest(depsFor(project)), project.directory);
|
|
176
|
+
await packageJson.removeDevDependencies([TSSERVER_PLUGIN], project.directory);
|
|
177
|
+
}
|
package/src/utils/manifest.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { packageJson } from "ember-apply";
|
|
1
2
|
import { hasAllKeys } from "./object.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -7,3 +8,15 @@ import { hasAllKeys } from "./object.js";
|
|
|
7
8
|
export function hasDevDeps(manifest, listOrObject) {
|
|
8
9
|
return hasAllKeys(manifest.devDependencies ?? {}, listOrObject);
|
|
9
10
|
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Like `packageJson.read`, but an empty object when there is no package.json yet
|
|
14
|
+
*
|
|
15
|
+
* @param {import('./project.js').Project} project
|
|
16
|
+
* @returns {Promise<Record<string, any>>}
|
|
17
|
+
*/
|
|
18
|
+
export async function readManifest(project) {
|
|
19
|
+
if (!project.hasFile("package.json")) return {};
|
|
20
|
+
|
|
21
|
+
return packageJson.read(project.directory);
|
|
22
|
+
}
|
package/src/utils/npm.js
CHANGED
|
@@ -35,6 +35,7 @@ export async function getLatest(deps) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
let version;
|
|
38
|
+
let alias = parseAlias(range);
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* HACK FOR CI.
|
|
@@ -54,6 +55,8 @@ export async function getLatest(deps) {
|
|
|
54
55
|
if (needsLocalLink && LOCAL_PACKAGES[dep]) {
|
|
55
56
|
version =
|
|
56
57
|
"link:" + resolve(join(import.meta.dirname, "../../packages", LOCAL_PACKAGES[dep]));
|
|
58
|
+
} else if (alias) {
|
|
59
|
+
version = `npm:${alias.name}@${await latestVersion(alias.name, { version: alias.range })}`;
|
|
57
60
|
} else {
|
|
58
61
|
if (range == "workspace:*") {
|
|
59
62
|
range = "latest";
|
|
@@ -71,6 +74,30 @@ export async function getLatest(deps) {
|
|
|
71
74
|
return Object.fromEntries(results);
|
|
72
75
|
}
|
|
73
76
|
|
|
77
|
+
/**
|
|
78
|
+
* An npm alias installs a package under another name:
|
|
79
|
+
*
|
|
80
|
+
* "@typescript/native": "npm:typescript@^7.1.0-0"
|
|
81
|
+
*
|
|
82
|
+
* The range belongs to the aliased package.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} range
|
|
85
|
+
* @returns {{ name: string; range: string } | undefined}
|
|
86
|
+
*/
|
|
87
|
+
function parseAlias(range) {
|
|
88
|
+
if (!range.startsWith("npm:")) return;
|
|
89
|
+
|
|
90
|
+
let spec = range.slice("npm:".length);
|
|
91
|
+
// The last `@` starts the range. A scope's `@` is at index 0.
|
|
92
|
+
let at = spec.lastIndexOf("@");
|
|
93
|
+
|
|
94
|
+
if (at <= 0) {
|
|
95
|
+
return { name: spec, range: "latest" };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { name: spec.slice(0, at), range: spec.slice(at + 1) };
|
|
99
|
+
}
|
|
100
|
+
|
|
74
101
|
async function needsWorkspace() {
|
|
75
102
|
if (process.env.GITHUB_REPOSITORY === "NullVoxPopuli/ember.nvp") return true;
|
|
76
103
|
|
package/src/utils/project.js
CHANGED
|
@@ -80,6 +80,29 @@ export class Project {
|
|
|
80
80
|
return this.#desires;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Get resolved options for a specific layer, merging defaults with user-supplied options.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} layerName
|
|
87
|
+
* @returns {Record<string, any>}
|
|
88
|
+
*/
|
|
89
|
+
getLayerOptions(layerName) {
|
|
90
|
+
let layer = this.desires.layers?.find((l) => l.name === layerName);
|
|
91
|
+
/** @type {Record<string, any>} */
|
|
92
|
+
let defaults = {};
|
|
93
|
+
|
|
94
|
+
if (layer?.options) {
|
|
95
|
+
for (const [key, schema] of Object.entries(layer.options)) {
|
|
96
|
+
if (schema.default !== undefined) {
|
|
97
|
+
defaults[key] = schema.default;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let userOptions = this.desires.options?.[layerName] ?? {};
|
|
103
|
+
return { ...defaults, ...userOptions };
|
|
104
|
+
}
|
|
105
|
+
|
|
83
106
|
/**
|
|
84
107
|
* @param {string} name
|
|
85
108
|
* @returns {Promise<boolean>}
|
package/src/utils/types.ts
CHANGED
|
@@ -1,13 +1,61 @@
|
|
|
1
|
+
import type { Validate } from "@clack/core";
|
|
1
2
|
import { Project } from "./project.js";
|
|
2
3
|
export type PackageManager = "pnpm" | "npm";
|
|
3
4
|
export type ProjectType = "app" | "library" | "extension" | "custom-element";
|
|
4
5
|
|
|
6
|
+
export type LayerOptionType = "text" | "number" | "select" | "confirm" | "multiselect";
|
|
7
|
+
|
|
8
|
+
export interface LayerSelectChoice {
|
|
9
|
+
/**
|
|
10
|
+
* A string, because the same value is also typed as a CLI flag
|
|
11
|
+
*/
|
|
12
|
+
value: string;
|
|
13
|
+
label: string;
|
|
14
|
+
hint?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LayerOptionSchema {
|
|
18
|
+
type: LayerOptionType;
|
|
19
|
+
/**
|
|
20
|
+
* The question to ask, also shown in `--help`
|
|
21
|
+
*/
|
|
22
|
+
prompt: string;
|
|
23
|
+
default?: any;
|
|
24
|
+
/**
|
|
25
|
+
* The choices for "select" and "multiselect"
|
|
26
|
+
*/
|
|
27
|
+
options?: LayerSelectChoice[];
|
|
28
|
+
/**
|
|
29
|
+
* The same as clack's `validate`:
|
|
30
|
+
* a function that returns a message (or an Error) to reject the value,
|
|
31
|
+
* or a Standard Schema.
|
|
32
|
+
*
|
|
33
|
+
* Receives the value after conversion to the option's type.
|
|
34
|
+
*/
|
|
35
|
+
validate?: Validate<any>;
|
|
36
|
+
/**
|
|
37
|
+
* Reads the value that an existing project uses now.
|
|
38
|
+
*
|
|
39
|
+
* When updating a project, the prompt starts at this value instead of `default`,
|
|
40
|
+
* so that pressing enter keeps the project as it is.
|
|
41
|
+
*/
|
|
42
|
+
detect?: (project: Project) => unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface LayerOptionsSchema {
|
|
46
|
+
[optionKey: string]: LayerOptionSchema;
|
|
47
|
+
}
|
|
48
|
+
|
|
5
49
|
export interface Layer {
|
|
6
50
|
/**
|
|
7
51
|
* The text to show during selection
|
|
8
52
|
*/
|
|
9
53
|
label: string;
|
|
10
54
|
hint?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Optional schema of configurable options for this layer
|
|
57
|
+
*/
|
|
58
|
+
options?: LayerOptionsSchema;
|
|
11
59
|
/**
|
|
12
60
|
* Whether the layer is pre-selected in the CLI.
|
|
13
61
|
* The user can still deselect it.
|
|
@@ -24,8 +72,10 @@ export interface Layer {
|
|
|
24
72
|
*
|
|
25
73
|
* run _may_ be invoked multiple times,
|
|
26
74
|
* so it's important to not require interaction here
|
|
75
|
+
*
|
|
76
|
+
* `options` are this layer's options, with defaults filled in.
|
|
27
77
|
*/
|
|
28
|
-
run: (project: Project) => Promise<void>;
|
|
78
|
+
run: (project: Project, options: Record<string, any>) => Promise<void>;
|
|
29
79
|
isSetup: <Explain extends boolean = false>(
|
|
30
80
|
project: Project,
|
|
31
81
|
explain?: Explain,
|
|
@@ -55,4 +105,5 @@ export interface Answers {
|
|
|
55
105
|
name: string;
|
|
56
106
|
layers: DiscoveredLayer[];
|
|
57
107
|
packageManager: PackageManager;
|
|
108
|
+
options?: Record<string, Record<string, any>>;
|
|
58
109
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { packageJson } from "ember-apply";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { cp } from "node:fs/promises";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { hasConfiguredTSBabel, prependPlugin } from "#utils/babel.js";
|
|
6
|
+
import { getLatest } from "#utils/npm.js";
|
|
7
|
+
|
|
8
|
+
const bases = join(import.meta.dirname, "../bases");
|
|
9
|
+
const appBase = join(bases, "minimal-app/files");
|
|
10
|
+
const extensionBase = join(bases, "minimal-extension/files");
|
|
11
|
+
const libraryBase = join(bases, "minimal-library/files");
|
|
12
|
+
const customElementBase = join(bases, "minimal-custom-element/files");
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* TypeScript 7 has no JavaScript API.
|
|
16
|
+
*
|
|
17
|
+
* typescript-eslint imports `typescript`, so it keeps using TypeScript 6.
|
|
18
|
+
*
|
|
19
|
+
* `@typescript/typescript6` is TypeScript 6 with a `tsc6` bin,
|
|
20
|
+
* so `tsc` stays TypeScript 7.
|
|
21
|
+
*/
|
|
22
|
+
export const TYPESCRIPT_6 = "npm:@typescript/typescript6@^6.0.2";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Content mappers (for .gts and .gjs) need 7.1 or newer.
|
|
26
|
+
*/
|
|
27
|
+
export const TYPESCRIPT_7 = "npm:typescript@^7.1.0-0";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {Record<string, any>} manifest
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
export function hasTypeScript6(manifest) {
|
|
34
|
+
let spec = manifest.devDependencies?.typescript;
|
|
35
|
+
|
|
36
|
+
return typeof spec === "string" && spec.startsWith("npm:@typescript/typescript6@");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {import('#utils/project.js').Project} project
|
|
41
|
+
* @returns {boolean}
|
|
42
|
+
*/
|
|
43
|
+
export function wantsTypeScript7(project) {
|
|
44
|
+
return project.wantsLayer("typescript") && project.getLayerOptions("typescript").version === "7";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Reads the manifest instead of asking the typescript layer.
|
|
49
|
+
* That layer's isSetup asks about eslint, and the eslint layers ask about TypeScript 7.
|
|
50
|
+
*
|
|
51
|
+
* @param {import('#utils/project.js').Project} project
|
|
52
|
+
* @param {Record<string, any>} manifest
|
|
53
|
+
* @returns {boolean}
|
|
54
|
+
*/
|
|
55
|
+
export function usesTypeScript7(project, manifest) {
|
|
56
|
+
return wantsTypeScript7(project) || Boolean(manifest.devDependencies?.["@typescript/native"]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {import('#utils/project.js').Project} project
|
|
61
|
+
* @param {Record<string, any>} manifest
|
|
62
|
+
* @returns {boolean}
|
|
63
|
+
*/
|
|
64
|
+
export function needsTypeScript6(project, manifest) {
|
|
65
|
+
return project.wantsLayer("eslint") || Boolean(manifest.devDependencies?.eslint);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* In a TypeScript 7 project:
|
|
70
|
+
* - keeps `@typescript/typescript6`
|
|
71
|
+
* - adds it when eslint needs TypeScript 6
|
|
72
|
+
* - removes any other `typescript`, because it would be a second `tsc`
|
|
73
|
+
*
|
|
74
|
+
* @param {import('#utils/project.js').Project} project
|
|
75
|
+
*/
|
|
76
|
+
export async function syncTypeScript6(project) {
|
|
77
|
+
let manifest = await packageJson.read(project.directory);
|
|
78
|
+
|
|
79
|
+
if (!usesTypeScript7(project, manifest)) return;
|
|
80
|
+
if (hasTypeScript6(manifest)) return;
|
|
81
|
+
|
|
82
|
+
if (needsTypeScript6(project, manifest)) {
|
|
83
|
+
await packageJson.addDevDependencies(
|
|
84
|
+
await getLatest({ typescript: TYPESCRIPT_6 }),
|
|
85
|
+
project.directory,
|
|
86
|
+
);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await packageJson.removeDevDependencies(["typescript"], project.directory);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Copies the base's tsconfig, unless the project already has one.
|
|
95
|
+
*
|
|
96
|
+
* @param {import('#utils/project.js').Project} project
|
|
97
|
+
*/
|
|
98
|
+
export async function addTSConfig(project) {
|
|
99
|
+
if (existsSync(project.path("tsconfig.json"))) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (project.type === "app") {
|
|
104
|
+
await cp(join(appBase, "tsconfig.json"), project.path("tsconfig.json"));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (project.type === "extension") {
|
|
109
|
+
await cp(join(extensionBase, "tsconfig.json"), project.path("tsconfig.json"));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (project.type === "library") {
|
|
114
|
+
await cp(join(libraryBase, "tsconfig.json"), project.path("tsconfig.json"));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (project.type === "custom-element") {
|
|
119
|
+
await cp(join(customElementBase, "tsconfig.json"), project.path("tsconfig.json"));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {import('#utils/project.js').Project} project
|
|
125
|
+
*/
|
|
126
|
+
export async function updateBabelConfig(project) {
|
|
127
|
+
if (!project.hasFile("babel.config.js")) {
|
|
128
|
+
// Nothing to patch (libraries): ember() strips types.
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (await hasConfiguredTSBabel(project)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await prependPlugin(
|
|
137
|
+
project,
|
|
138
|
+
`[
|
|
139
|
+
"@babel/plugin-transform-typescript",
|
|
140
|
+
{
|
|
141
|
+
allExtensions: true,
|
|
142
|
+
onlyRemoveTypeImports: true,
|
|
143
|
+
allowDeclareFields: true,
|
|
144
|
+
},
|
|
145
|
+
]`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -204,6 +204,17 @@ describe("base: minimal-library", () => {
|
|
|
204
204
|
//# sourceMappingURL=index.d.ts.map"
|
|
205
205
|
`);
|
|
206
206
|
|
|
207
|
+
// A consumer's "go to definition" opens these, from the published src/
|
|
208
|
+
let declarationMap = JSON.parse((await project.read("dist/index.d.ts.map"))!);
|
|
209
|
+
|
|
210
|
+
expect(declarationMap.sources).toMatchInlineSnapshot(`
|
|
211
|
+
[
|
|
212
|
+
"../src/components/badge.gts",
|
|
213
|
+
"../src/components/greeting.gts",
|
|
214
|
+
"../src/utils/math.ts",
|
|
215
|
+
]
|
|
216
|
+
`);
|
|
217
|
+
|
|
207
218
|
expect(await listFiles(join(project.directory, "dist"))).toMatchInlineSnapshot(`
|
|
208
219
|
[
|
|
209
220
|
"index.d.ts",
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { test, expect as hardExpect } from "vitest";
|
|
2
|
-
import { cli } from "#test-helpers";
|
|
2
|
+
import { cli, mktemp } from "#test-helpers";
|
|
3
|
+
import { packageJson } from "ember-apply";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { rm } from "node:fs/promises";
|
|
6
|
+
import { join } from "node:path";
|
|
3
7
|
import { stripVTControlCharacters } from "node:util";
|
|
4
8
|
|
|
5
9
|
const expect = hardExpect.soft;
|
|
@@ -33,8 +37,8 @@ const BASIC_HELP_OUTPUT = `
|
|
|
33
37
|
--confirm <string>
|
|
34
38
|
Bypass target confirmation step [choices: "yes", "no"]
|
|
35
39
|
|
|
36
|
-
--layers <string>
|
|
37
|
-
Layers to apply to the project
|
|
40
|
+
--layers <string> [--layers <string> ...]
|
|
41
|
+
Layers to apply to the project [choices: "are-the-types-wrong", "eslint-bundled", "expect-type", "git", "github-actions", "inspector-support", "prettier", "publint", "qunit", "readme", "renovate", "typescript", "vitest"]
|
|
38
42
|
|
|
39
43
|
--packageManager <string>
|
|
40
44
|
Package manager to configure for the project [choices: "npm", "pnpm"]
|
|
@@ -44,6 +48,15 @@ const BASIC_HELP_OUTPUT = `
|
|
|
44
48
|
|
|
45
49
|
--write <string>
|
|
46
50
|
Confirm writing changes to disk [choices: "yes", "no"]
|
|
51
|
+
|
|
52
|
+
Layer Options:
|
|
53
|
+
Layer: eslint-bundled
|
|
54
|
+
--eslint-bundled.preset <string>
|
|
55
|
+
Which ESLint config? [choices: "ember", "nvp"] [default: "ember"]
|
|
56
|
+
|
|
57
|
+
Layer: typescript
|
|
58
|
+
--typescript.version <string>
|
|
59
|
+
Which TypeScript version? [choices: "7", "6"] [default: "7"]
|
|
47
60
|
"
|
|
48
61
|
`;
|
|
49
62
|
|
|
@@ -66,3 +79,77 @@ test("cli -h alias works", async () => {
|
|
|
66
79
|
const outStr = stripVTControlCharacters(res.stdout);
|
|
67
80
|
expect(outStr).toMatchInlineSnapshot(BASIC_HELP_OUTPUT);
|
|
68
81
|
});
|
|
82
|
+
|
|
83
|
+
test("cli prints a message, not a stack trace, for an unknown flag", async () => {
|
|
84
|
+
let { execaPromise } = cli(["--bogus"]);
|
|
85
|
+
|
|
86
|
+
const res = await execaPromise.catch((error) => error);
|
|
87
|
+
|
|
88
|
+
expect(res.exitCode).toBe(1);
|
|
89
|
+
const outStr = stripVTControlCharacters(`${res.stdout}\n${res.stderr}`);
|
|
90
|
+
expect(outStr).toContain("Unknown option '--bogus'");
|
|
91
|
+
expect(outStr).not.toContain("TypeError");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Every question has a flag, so nothing prompts.
|
|
96
|
+
* The path does not exist yet, so there is no replace-or-update question either.
|
|
97
|
+
*/
|
|
98
|
+
async function eslintProjectArgs(preset: string) {
|
|
99
|
+
let parent = await mktemp("cli-layer-options");
|
|
100
|
+
let path = join(parent, "my-app");
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
parent,
|
|
104
|
+
path,
|
|
105
|
+
args: [
|
|
106
|
+
"--name",
|
|
107
|
+
"my-app",
|
|
108
|
+
"--path",
|
|
109
|
+
path,
|
|
110
|
+
"--type",
|
|
111
|
+
"app",
|
|
112
|
+
"--layers",
|
|
113
|
+
"eslint-bundled",
|
|
114
|
+
"--packageManager",
|
|
115
|
+
"pnpm",
|
|
116
|
+
"--confirm",
|
|
117
|
+
"yes",
|
|
118
|
+
"--eslint-bundled.preset",
|
|
119
|
+
preset,
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
test("cli applies a layer option flag", async () => {
|
|
125
|
+
let { parent, path, args } = await eslintProjectArgs("nvp");
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
let res = await cli(args).execaPromise;
|
|
129
|
+
|
|
130
|
+
expect(res.exitCode).toBe(0);
|
|
131
|
+
|
|
132
|
+
let manifest = await packageJson.read(path);
|
|
133
|
+
|
|
134
|
+
expect(manifest.devDependencies).toHaveProperty("@nullvoxpopuli/eslint-configs");
|
|
135
|
+
expect(manifest.devDependencies).not.toHaveProperty("ember-eslint");
|
|
136
|
+
} finally {
|
|
137
|
+
await rm(parent, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("cli exits on an invalid layer option flag, before writing anything", async () => {
|
|
142
|
+
let { parent, path, args } = await eslintProjectArgs("bogus");
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const res = await cli(args).execaPromise.catch((error) => error);
|
|
146
|
+
|
|
147
|
+
expect(res.exitCode).toBe(1);
|
|
148
|
+
expect(stripVTControlCharacters(res.stdout)).toContain(
|
|
149
|
+
"Invalid CLI argument '--eslint-bundled.preset': Invalid option 'bogus'. Must be one of: ember, nvp",
|
|
150
|
+
);
|
|
151
|
+
expect(existsSync(path)).toBe(false);
|
|
152
|
+
} finally {
|
|
153
|
+
await rm(parent, { recursive: true, force: true });
|
|
154
|
+
}
|
|
155
|
+
});
|