ember.nvp 1.8.0 → 1.10.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 +15 -35
- package/CHANGELOG.md +33 -0
- package/README.md +73 -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 +170 -48
- package/src/cli/generator.js +2 -1
- package/src/cli/help.js +82 -0
- 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/files/type-tests/index.test.ts +4 -0
- package/src/layers/expect-type/files/type-tests/tsconfig.json +8 -0
- package/src/layers/expect-type/index.js +114 -0
- 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 +142 -1
- package/test/ember.nvp/layer-expect-type.test.ts +113 -0
- package/test/ember.nvp/layer-options.test.ts +248 -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 +2 -2
- package/test/ember.nvp/permutations/shared.ts +6 -6
- package/test/ember.nvp/validate-option.test.ts +152 -0
- package/test/test-helpers/permutate.test.ts +2 -7
- package/test/test-helpers.ts +12 -2
- package/vitest.config.js +7 -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
|
@@ -1,12 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { originalPositionFor, TraceMap } from "@jridgewell/trace-mapping";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdtemp, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
4
|
import { tmpdir } from "node:os";
|
|
3
5
|
import path from "node:path";
|
|
4
6
|
import { rolldown } from "rolldown";
|
|
5
|
-
import type { Plugin } from "rolldown";
|
|
7
|
+
import type { OutputChunk, Plugin } from "rolldown";
|
|
6
8
|
import { describe, expect, it } from "vitest";
|
|
7
9
|
|
|
8
10
|
import { emberTransform } from "./transform.ts";
|
|
9
11
|
|
|
12
|
+
interface BuildOptions {
|
|
13
|
+
input?: string[];
|
|
14
|
+
plugins?: (dir: string) => Plugin[];
|
|
15
|
+
sourcemap?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Build {
|
|
19
|
+
/**
|
|
20
|
+
* The output directory.
|
|
21
|
+
* Nothing is written to it, but sourcemaps name their sources relative to it.
|
|
22
|
+
*/
|
|
23
|
+
dir: string;
|
|
24
|
+
chunks: OutputChunk[];
|
|
25
|
+
}
|
|
26
|
+
|
|
10
27
|
/**
|
|
11
28
|
* Drives the plugin through a real rolldown build.
|
|
12
29
|
*
|
|
@@ -16,16 +33,11 @@ import { emberTransform } from "./transform.ts";
|
|
|
16
33
|
* Every bare (package) specifier is marked external,
|
|
17
34
|
* so the build only exercises the local `.gts` / `.gjs` / `.ts` handling
|
|
18
35
|
* and doesn't need real dependencies installed.
|
|
19
|
-
*
|
|
20
|
-
* Returns the concatenated code of every emitted chunk.
|
|
21
36
|
*/
|
|
22
|
-
async function
|
|
37
|
+
async function generate(
|
|
23
38
|
files: Record<string, string>,
|
|
24
|
-
{
|
|
25
|
-
|
|
26
|
-
plugins = () => [],
|
|
27
|
-
}: { input?: string[]; plugins?: (dir: string) => Plugin[] } = {},
|
|
28
|
-
): Promise<string> {
|
|
39
|
+
{ input = ["index.ts"], plugins = () => [], sourcemap = false }: BuildOptions = {},
|
|
40
|
+
): Promise<Build> {
|
|
29
41
|
const dir = await mkdtemp(path.join(tmpdir(), "ember-rolldown-build-"));
|
|
30
42
|
|
|
31
43
|
for (const [relative, source] of Object.entries(files)) {
|
|
@@ -43,18 +55,73 @@ async function bundle(
|
|
|
43
55
|
onwarn() {},
|
|
44
56
|
});
|
|
45
57
|
|
|
46
|
-
const
|
|
58
|
+
const outDir = path.join(dir, "dist");
|
|
59
|
+
const { output } = await build.generate({ format: "es", dir: outDir, sourcemap });
|
|
47
60
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
.
|
|
51
|
-
|
|
61
|
+
return {
|
|
62
|
+
dir: outDir,
|
|
63
|
+
chunks: output.filter((chunk): chunk is OutputChunk => chunk.type === "chunk"),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Returns the concatenated code of every chunk that `generate` emits.
|
|
69
|
+
*/
|
|
70
|
+
async function bundle(files: Record<string, string>, options?: BuildOptions): Promise<string> {
|
|
71
|
+
const { chunks } = await generate(files, options);
|
|
72
|
+
const code = chunks.map((chunk) => chunk.code).join("\n");
|
|
52
73
|
|
|
53
74
|
// rolldown's `//#region <path>` comments reference the random temp dir.
|
|
54
75
|
// Collapse them to the bare filename so snapshots stay deterministic.
|
|
55
76
|
return code.replace(/(\/\/#region ).*\/([^/\n]+)$/gm, "$1$2");
|
|
56
77
|
}
|
|
57
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Follows each needle in the emitted code through the chunk's sourcemap,
|
|
81
|
+
* the way a debugger does.
|
|
82
|
+
*
|
|
83
|
+
* One line per needle:
|
|
84
|
+
* - the source the map names
|
|
85
|
+
* - that source's line (1-based) and column (0-based)
|
|
86
|
+
* - the source text from that column on
|
|
87
|
+
*/
|
|
88
|
+
async function traced(build: Build, needles: string[]): Promise<string> {
|
|
89
|
+
const [chunk] = build.chunks;
|
|
90
|
+
if (!chunk?.map) throw new Error("The build emitted no sourcemap");
|
|
91
|
+
|
|
92
|
+
const map = new TraceMap(chunk.map.toString());
|
|
93
|
+
const lines = chunk.code.split("\n");
|
|
94
|
+
const result: string[] = [];
|
|
95
|
+
|
|
96
|
+
for (const needle of needles) {
|
|
97
|
+
const index = lines.findIndex((line) => line.includes(needle));
|
|
98
|
+
if (index === -1) throw new Error(`\`${needle}\` is not in the output:\n${chunk.code}`);
|
|
99
|
+
|
|
100
|
+
const {
|
|
101
|
+
source,
|
|
102
|
+
line: sourceLine,
|
|
103
|
+
column,
|
|
104
|
+
} = originalPositionFor(map, {
|
|
105
|
+
line: index + 1,
|
|
106
|
+
column: lines[index]!.indexOf(needle),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (!source || sourceLine === null || column === null) {
|
|
110
|
+
result.push(`${needle} -> (unmapped)`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const sourcePath = path.resolve(build.dir, source);
|
|
115
|
+
const text = existsSync(sourcePath)
|
|
116
|
+
? (await readFile(sourcePath, "utf8")).split("\n")[sourceLine - 1]?.slice(column)
|
|
117
|
+
: "(no such file)";
|
|
118
|
+
|
|
119
|
+
result.push(`${needle} -> ${source}:${sourceLine}:${column} ${text}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return result.join("\n");
|
|
123
|
+
}
|
|
124
|
+
|
|
58
125
|
describe("emberTransform (full plugin via rolldown)", () => {
|
|
59
126
|
it("compiles a .gts module's <template> and keeps bare imports external", async () => {
|
|
60
127
|
const code = await bundle({
|
|
@@ -313,4 +380,134 @@ describe("emberTransform (full plugin via rolldown)", () => {
|
|
|
313
380
|
const sources = (chunk as { map?: { sources: string[] } }).map?.sources ?? [];
|
|
314
381
|
expect(sources.some((source) => source.endsWith("foo.gts"))).toBe(true);
|
|
315
382
|
});
|
|
383
|
+
|
|
384
|
+
it("names the .gts file in the map of a .gts with no <template>", async () => {
|
|
385
|
+
// content-tag does not run on these modules, but they still load under a virtual .ts id.
|
|
386
|
+
// helpers.gts goes through the specifier rewrite, and plain.gts does not.
|
|
387
|
+
const { chunks } = await generate(
|
|
388
|
+
{
|
|
389
|
+
"index.ts": [
|
|
390
|
+
`export { double } from './helpers.gts';`,
|
|
391
|
+
`export { half } from './plain.gts';`,
|
|
392
|
+
].join("\n"),
|
|
393
|
+
"helpers.gts": [
|
|
394
|
+
`import { half } from './plain.gts';`,
|
|
395
|
+
`export function double(value: number): number {`,
|
|
396
|
+
` return half(value) * 4;`,
|
|
397
|
+
`}`,
|
|
398
|
+
].join("\n"),
|
|
399
|
+
"plain.gts": [
|
|
400
|
+
`export function half(value: number): number {`,
|
|
401
|
+
` return value / 2;`,
|
|
402
|
+
`}`,
|
|
403
|
+
].join("\n"),
|
|
404
|
+
},
|
|
405
|
+
{ sourcemap: true },
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
// index.ts only re-exports, so it contributes no code and no source.
|
|
409
|
+
expect(chunks[0]?.map?.sources.toSorted()).toEqual(["../helpers.gts", "../plain.gts"]);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("maps a rewritten .gts module to its exact source lines and columns", async () => {
|
|
413
|
+
// content-tag puts a doc comment's closing `*/` and the next declaration on one line.
|
|
414
|
+
// widget.gts imports another .gts, so the specifier rewrite runs on that output.
|
|
415
|
+
const build = await generate(
|
|
416
|
+
{
|
|
417
|
+
"index.ts": `export { Widget } from './widget.ts';`,
|
|
418
|
+
"widget.gts": [
|
|
419
|
+
`import { helper } from './other.gts';`,
|
|
420
|
+
``,
|
|
421
|
+
`/**`,
|
|
422
|
+
` * A widget.`,
|
|
423
|
+
` */`,
|
|
424
|
+
`export class Widget {`,
|
|
425
|
+
` /**`,
|
|
426
|
+
` * The name.`,
|
|
427
|
+
` */`,
|
|
428
|
+
` get name(): string {`,
|
|
429
|
+
` return helper();`,
|
|
430
|
+
` }`,
|
|
431
|
+
``,
|
|
432
|
+
` <template>Hello {{this.name}}</template>`,
|
|
433
|
+
`}`,
|
|
434
|
+
].join("\n"),
|
|
435
|
+
"other.gts": [`export function helper(): string {`, ` return 'x';`, `}`].join("\n"),
|
|
436
|
+
},
|
|
437
|
+
{ sourcemap: true },
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
expect(await traced(build, ["Widget = class", "name()", "return helper"]))
|
|
441
|
+
.toMatchInlineSnapshot(`
|
|
442
|
+
"Widget = class -> ../widget.gts:6:13 Widget {
|
|
443
|
+
name() -> ../widget.gts:10:6 name(): string {
|
|
444
|
+
return helper -> ../widget.gts:11:4 return helper();"
|
|
445
|
+
`);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("keeps columns in a plain .ts module that imports a .gts", async () => {
|
|
449
|
+
const build = await generate(
|
|
450
|
+
{
|
|
451
|
+
"index.ts": [
|
|
452
|
+
`import { helper } from './other.gts';`,
|
|
453
|
+
``,
|
|
454
|
+
`export function greet(name: string): string {`,
|
|
455
|
+
` return helper() + name;`,
|
|
456
|
+
`}`,
|
|
457
|
+
].join("\n"),
|
|
458
|
+
"other.gts": [`export function helper(): string {`, ` return 'x';`, `}`].join("\n"),
|
|
459
|
+
},
|
|
460
|
+
{ sourcemap: true },
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
expect(await traced(build, ["greet", "return helper"])).toMatchInlineSnapshot(`
|
|
464
|
+
"greet -> ../index.ts:3:16 greet(name: string): string {
|
|
465
|
+
return helper -> ../index.ts:4:2 return helper() + name;"
|
|
466
|
+
`);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("maps .gts modules that the specifier rewrite does not touch", async () => {
|
|
470
|
+
// other.gts has no <template>, and widget.gts imports no .gts.
|
|
471
|
+
// Neither goes through the rewrite.
|
|
472
|
+
const build = await generate(
|
|
473
|
+
{
|
|
474
|
+
"index.ts": [
|
|
475
|
+
`export { helper } from './other.ts';`,
|
|
476
|
+
`export { Widget } from './widget.ts';`,
|
|
477
|
+
].join("\n"),
|
|
478
|
+
"widget.gts": [
|
|
479
|
+
`import { helper } from './other.ts';`,
|
|
480
|
+
``,
|
|
481
|
+
`/**`,
|
|
482
|
+
` * A widget.`,
|
|
483
|
+
` */`,
|
|
484
|
+
`export class Widget {`,
|
|
485
|
+
` get name(): string {`,
|
|
486
|
+
` return helper();`,
|
|
487
|
+
` }`,
|
|
488
|
+
``,
|
|
489
|
+
` <template>Hello {{this.name}}</template>`,
|
|
490
|
+
`}`,
|
|
491
|
+
].join("\n"),
|
|
492
|
+
"other.gts": [`export function helper(): string {`, ` return 'x';`, `}`].join("\n"),
|
|
493
|
+
},
|
|
494
|
+
{ sourcemap: true },
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
expect(
|
|
498
|
+
await traced(build, [
|
|
499
|
+
"helper() {",
|
|
500
|
+
`return "x"`,
|
|
501
|
+
"Widget = class",
|
|
502
|
+
"name()",
|
|
503
|
+
"return helper",
|
|
504
|
+
]),
|
|
505
|
+
).toMatchInlineSnapshot(`
|
|
506
|
+
"helper() { -> ../other.gts:1:16 helper(): string {
|
|
507
|
+
return "x" -> ../other.gts:2:2 return 'x';
|
|
508
|
+
Widget = class -> ../widget.gts:6:13 Widget {
|
|
509
|
+
name() -> ../widget.gts:7:6 name(): string {
|
|
510
|
+
return helper -> ../widget.gts:8:4 return helper();"
|
|
511
|
+
`);
|
|
512
|
+
});
|
|
316
513
|
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import remapping, { type EncodedSourceMap } from "@jridgewell/remapping";
|
|
1
2
|
import { Preprocessor } from "content-tag";
|
|
3
|
+
import MagicString from "magic-string";
|
|
2
4
|
import { existsSync, realpathSync } from "node:fs";
|
|
3
5
|
import { readFile, writeFile, readdir } from "node:fs/promises";
|
|
4
6
|
import path from "node:path";
|
|
@@ -40,26 +42,78 @@ async function fixDtsExtensionsInDir(dir: string): Promise<void> {
|
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
/**
|
|
43
|
-
* A sourcemap
|
|
45
|
+
* A sourcemap from `code` back to `source`, with a segment for every column.
|
|
44
46
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* (each following line advances the source line by one)
|
|
47
|
+
* rolldown traces each output position back through the map of every hook.
|
|
48
|
+
* A position between two segments resolves to the segment before it,
|
|
49
|
+
* so fewer segments move positions to an earlier column.
|
|
49
50
|
*
|
|
50
|
-
*
|
|
51
|
+
* content-tag's output often puts a doc comment's end and a declaration on one line.
|
|
52
|
+
* With one segment per line, the declaration resolves to the comment.
|
|
51
53
|
*/
|
|
52
|
-
function
|
|
53
|
-
return {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
function columnMap(code: MagicString, source: string) {
|
|
55
|
+
return code.generateMap({ source, includeContent: true, hires: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The `.gts` / `.gjs` file behind a virtual `.ts` / `.js` id,
|
|
60
|
+
* and content-tag's map back to it (absent when there was no `<template>` to compile).
|
|
61
|
+
*/
|
|
62
|
+
interface Backing {
|
|
63
|
+
fileName: string;
|
|
64
|
+
map?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Points a declaration map at the `.gts` / `.gjs` files behind its virtual sources.
|
|
69
|
+
*
|
|
70
|
+
* rolldown-plugin-dts generates declarations from the loaded module (content-tag's output).
|
|
71
|
+
* So its maps name:
|
|
72
|
+
* - the virtual id, a file that does not exist
|
|
73
|
+
* - content-tag's line numbers, which do not match the source
|
|
74
|
+
*
|
|
75
|
+
* Tracing through content-tag's map gives the file and line
|
|
76
|
+
* that a consumer's "go to definition" can open.
|
|
77
|
+
*
|
|
78
|
+
* Returns `undefined` when no source is virtual.
|
|
79
|
+
*/
|
|
80
|
+
function remapDeclarationMap(
|
|
81
|
+
json: string,
|
|
82
|
+
mapDir: string,
|
|
83
|
+
backings: Map<string, Backing>,
|
|
84
|
+
): string | undefined {
|
|
85
|
+
const map = JSON.parse(json) as EncodedSourceMap;
|
|
86
|
+
|
|
87
|
+
if (!map.sources.some((source) => source && backings.has(path.resolve(mapDir, source)))) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const remapped = remapping(
|
|
92
|
+
map,
|
|
93
|
+
(source, context) => {
|
|
94
|
+
// Only the declaration map's own sources can be virtual.
|
|
95
|
+
if (context.depth > 1) return null;
|
|
96
|
+
|
|
97
|
+
const backing = backings.get(path.resolve(mapDir, source));
|
|
98
|
+
if (!backing) return null;
|
|
99
|
+
|
|
100
|
+
context.source = backing.fileName;
|
|
101
|
+
return backing.map ?? null;
|
|
102
|
+
},
|
|
103
|
+
// Like the rest of the declaration map, point at the published sources.
|
|
104
|
+
// Do not embed them.
|
|
105
|
+
{ excludeContent: true },
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Backing files come back absolute.
|
|
109
|
+
// The other sources are relative to the map's directory.
|
|
110
|
+
remapped.sources = remapped.sources.map((source) =>
|
|
111
|
+
source && path.isAbsolute(source)
|
|
112
|
+
? path.relative(mapDir, source).split(path.sep).join("/")
|
|
113
|
+
: source,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
return remapped.toString();
|
|
63
117
|
}
|
|
64
118
|
|
|
65
119
|
/**
|
|
@@ -69,6 +123,9 @@ function lineIdentityMap(id: string, source: string) {
|
|
|
69
123
|
* so rolldown can identify them as ts / js.
|
|
70
124
|
*/
|
|
71
125
|
export function emberTransform(): Plugin {
|
|
126
|
+
// virtual id -> what it was loaded from
|
|
127
|
+
const backings = new Map<string, Backing>();
|
|
128
|
+
|
|
72
129
|
return {
|
|
73
130
|
name: "ember:transform",
|
|
74
131
|
|
|
@@ -190,10 +247,16 @@ export function emberTransform(): Plugin {
|
|
|
190
247
|
const { code, map } = processor.process(source, {
|
|
191
248
|
filename: fileName,
|
|
192
249
|
});
|
|
250
|
+
backings.set(id, { fileName, map });
|
|
193
251
|
return { code, map };
|
|
194
252
|
}
|
|
195
253
|
|
|
196
|
-
|
|
254
|
+
// The map names the `.gts` / `.gjs` file.
|
|
255
|
+
// Without it, the `.js.map` names the virtual id, which is not on disk.
|
|
256
|
+
//
|
|
257
|
+
// The backing needs no map, because its declaration lines match the source.
|
|
258
|
+
backings.set(id, { fileName });
|
|
259
|
+
return { code: source, map: columnMap(new MagicString(source), fileName) };
|
|
197
260
|
}
|
|
198
261
|
|
|
199
262
|
return null;
|
|
@@ -215,22 +278,39 @@ export function emberTransform(): Plugin {
|
|
|
215
278
|
// "load it (registering its declaration), then map to the declaration id".
|
|
216
279
|
// A `.d.ts` id is returned as-is: unloadable when the module isn't registered yet
|
|
217
280
|
// (e.g. the `.gts` is only ever type-imported).
|
|
218
|
-
const output = input
|
|
281
|
+
const output = new MagicString(input);
|
|
282
|
+
|
|
283
|
+
for (const match of input.matchAll(
|
|
219
284
|
/(['"`])((?:\.\.?\/|\/|@|[A-Za-z0-9_\-])[^'"]*?\.gts)\1/g,
|
|
220
|
-
|
|
221
|
-
|
|
285
|
+
)) {
|
|
286
|
+
// `.gts` ends the specifier, just before the closing quote.
|
|
287
|
+
const end = match.index + match[0].length - 1;
|
|
288
|
+
output.overwrite(end - ".gts".length, end, ".ts");
|
|
289
|
+
}
|
|
222
290
|
|
|
223
|
-
if (output
|
|
291
|
+
if (!output.hasChanged()) {
|
|
224
292
|
return null;
|
|
225
293
|
}
|
|
226
294
|
|
|
227
|
-
|
|
228
|
-
// so a line-identity map is accurate to the line
|
|
229
|
-
// (and to the column, for everything before the first rewritten specifier on a line).
|
|
230
|
-
return { code: output, map: lineIdentityMap(id, input) };
|
|
295
|
+
return { code: output.toString(), map: columnMap(output, id) };
|
|
231
296
|
},
|
|
232
297
|
},
|
|
233
298
|
|
|
299
|
+
generateBundle(options, bundle) {
|
|
300
|
+
const outDir = options.dir ?? (options.file && path.dirname(options.file));
|
|
301
|
+
if (!outDir || backings.size === 0) return;
|
|
302
|
+
|
|
303
|
+
for (const output of Object.values(bundle)) {
|
|
304
|
+
if (output.type !== "asset" || !output.fileName.endsWith(".d.ts.map")) continue;
|
|
305
|
+
if (typeof output.source !== "string") continue;
|
|
306
|
+
|
|
307
|
+
const mapDir = path.dirname(path.resolve(outDir, output.fileName));
|
|
308
|
+
const remapped = remapDeclarationMap(output.source, mapDir, backings);
|
|
309
|
+
|
|
310
|
+
if (remapped) output.source = remapped;
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
|
|
234
314
|
writeBundle: {
|
|
235
315
|
async handler(options) {
|
|
236
316
|
const outDir = (options as { dir?: string }).dir;
|
package/src/cli/args.js
CHANGED
|
@@ -1,61 +1,183 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import { styleText, parseArgs } from "node:util";
|
|
3
|
+
import { layers as discoveredLayers } from "#layers";
|
|
4
|
+
import { validateOption } from "./questions/validate-option.js";
|
|
5
|
+
import { printHelp } from "./help.js";
|
|
3
6
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
7
|
+
export const coreOptions = /** @type {const} */ ({
|
|
8
|
+
help: {
|
|
9
|
+
type: "boolean",
|
|
10
|
+
short: "h",
|
|
11
|
+
description: "Show CLI help and option details",
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
name: {
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "Name of the project",
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
path: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "Target directory path for the project",
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
type: {
|
|
25
|
+
type: "string",
|
|
26
|
+
choices: ["app", "addon", "library", "extension", "custom-element"],
|
|
27
|
+
description: "Type of project to generate",
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
confirm: {
|
|
31
|
+
type: "string",
|
|
32
|
+
choices: ["yes", "no"],
|
|
33
|
+
description: "Bypass target confirmation step",
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
layers: {
|
|
37
|
+
type: "string",
|
|
38
|
+
multiple: true,
|
|
39
|
+
description: "Layers to apply to the project (repeat for multiple layers)",
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
packageManager: {
|
|
43
|
+
type: "string",
|
|
44
|
+
choices: ["npm", "pnpm"],
|
|
45
|
+
description: "Package manager to configure for the project",
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
replaceOrUpdate: {
|
|
49
|
+
type: "string",
|
|
50
|
+
choices: ["replace", "update"],
|
|
51
|
+
description: "Strategy to use if target directory exists",
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
write: {
|
|
55
|
+
type: "string",
|
|
56
|
+
choices: ["yes", "no"],
|
|
57
|
+
description: "Confirm writing changes to disk",
|
|
43
58
|
},
|
|
44
59
|
});
|
|
45
60
|
|
|
46
|
-
|
|
61
|
+
/**
|
|
62
|
+
* This lightweight check is here because Node's `parseArgs` is in strict mode.
|
|
63
|
+
* If a user runs `npx ember.nvp --help` alongside invalid or unknown flags,
|
|
64
|
+
* standard strict parsing would throw a `TypeError (ERR_PARSE_ARGS_UNKNOWN_OPTION)`
|
|
65
|
+
* before reaching any help handler. Pre-checking raw arguments ensures `--help`
|
|
66
|
+
* always prints successfully regardless of invalid flags.
|
|
67
|
+
*/
|
|
68
|
+
const isHelpRequested = process.argv.slice(2).some((arg) => arg === "--help" || arg === "-h");
|
|
69
|
+
if (isHelpRequested) {
|
|
70
|
+
printHelp(coreOptions, discoveredLayers);
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parse the core flags, plus one `--<layer>.<option>` flag per layer option.
|
|
76
|
+
*
|
|
77
|
+
* Every layer adds its flags, not only the selected ones,
|
|
78
|
+
* because layers are selected after parsing.
|
|
79
|
+
*
|
|
80
|
+
* Throws on an unknown flag, or on a value given to a boolean flag.
|
|
81
|
+
*
|
|
82
|
+
* @param {string[]} args
|
|
83
|
+
* @param {import('#types').DiscoveredLayer[]} layers
|
|
84
|
+
*/
|
|
85
|
+
export function parseCliArgs(args, layers) {
|
|
86
|
+
/** @type {Record<string, import("node:util").ParseArgsOptionDescriptor>} */
|
|
87
|
+
const options = { ...coreOptions };
|
|
88
|
+
|
|
89
|
+
for (const layer of layers) {
|
|
90
|
+
for (const [key, schema] of Object.entries(layer.options ?? {})) {
|
|
91
|
+
options[`${layer.name}.${key}`] = {
|
|
92
|
+
type: schema.type === "confirm" ? "boolean" : "string",
|
|
93
|
+
multiple: schema.type === "multiselect",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// allowNegative makes `--no-<layer>.<option>` turn a "confirm" option off
|
|
99
|
+
return parseArgs({ args, options, allowNegative: true }).values;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** @type {ReturnType<typeof parseCliArgs>} */
|
|
103
|
+
let values;
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
values = parseCliArgs(process.argv.slice(2), discoveredLayers);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
// parseArgs errors name the flag, so a stack trace adds nothing for the user
|
|
109
|
+
p.cancel(error instanceof Error ? error.message : String(error));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const typedValues =
|
|
114
|
+
/** @type {ReturnType<typeof parseArgs<{ options: typeof coreOptions }>>['values']} */ (values);
|
|
47
115
|
|
|
48
116
|
export const answers = {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
layers,
|
|
52
|
-
packageManager,
|
|
53
|
-
path,
|
|
54
|
-
confirm,
|
|
55
|
-
replaceOrUpdate,
|
|
56
|
-
write,
|
|
117
|
+
...typedValues,
|
|
118
|
+
layers: typedValues.layers ?? [],
|
|
57
119
|
};
|
|
58
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Extract layer options from parsed CLI values.
|
|
123
|
+
*
|
|
124
|
+
* Exits when a value does not pass the option's validation.
|
|
125
|
+
*
|
|
126
|
+
* @param {import('#types').DiscoveredLayer[]} layers
|
|
127
|
+
* @param {Record<string, any>} [parsedValues] Defaults to the parsed `process.argv`
|
|
128
|
+
* @returns {Record<string, Record<string, any>>}
|
|
129
|
+
*/
|
|
130
|
+
export function parseLayerOptionsFromParsedArgs(layers = [], parsedValues = values) {
|
|
131
|
+
/** @type {Record<string, Record<string, any>>} */
|
|
132
|
+
const result = {};
|
|
133
|
+
|
|
134
|
+
for (const layer of layers) {
|
|
135
|
+
if (!layer.options) continue;
|
|
136
|
+
|
|
137
|
+
for (const [optionKey, schema] of Object.entries(layer.options)) {
|
|
138
|
+
const flagKey = `${layer.name}.${optionKey}`;
|
|
139
|
+
const rawVal = parsedValues[flagKey];
|
|
140
|
+
|
|
141
|
+
if (rawVal === undefined) continue;
|
|
142
|
+
|
|
143
|
+
const validation = validateOption(
|
|
144
|
+
schema,
|
|
145
|
+
schema.type === "multiselect" ? splitCommas(rawVal) : rawVal,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
if (!validation.ok) {
|
|
149
|
+
p.cancel(`Invalid CLI argument '--${flagKey}': ${validation.error}`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
(result[layer.name] ??= {})[optionKey] = validation.value;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* `--x.y a,b` means the same as `--x.y a --x.y b`
|
|
162
|
+
*
|
|
163
|
+
* @param {string[]} values
|
|
164
|
+
* @returns {string[]}
|
|
165
|
+
*/
|
|
166
|
+
function splitCommas(values) {
|
|
167
|
+
/** @type {string[]} */
|
|
168
|
+
const result = [];
|
|
169
|
+
|
|
170
|
+
for (const value of values) {
|
|
171
|
+
for (const part of value.split(",")) {
|
|
172
|
+
const trimmed = part.trim();
|
|
173
|
+
|
|
174
|
+
if (trimmed) result.push(trimmed);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
|
|
59
181
|
/**
|
|
60
182
|
*
|
|
61
183
|
* @param {string} label
|
package/src/cli/generator.js
CHANGED