ts-node-pack 0.1.3 → 0.2.1
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/README.md +25 -9
- package/package.json +1 -1
- package/src/cli.js +12 -8
- package/src/index.d.ts +15 -3
- package/src/index.js +77 -114
- package/src/validation.d.ts +21 -0
- package/src/validation.js +146 -0
package/README.md
CHANGED
|
@@ -25,18 +25,19 @@ Requires Node ≥ 20 and TypeScript ≥ 5.7 available in the package being packe
|
|
|
25
25
|
## Usage
|
|
26
26
|
|
|
27
27
|
```sh
|
|
28
|
-
ts-node-pack <packageDir> [--tsconfig <path>] [--
|
|
28
|
+
ts-node-pack <packageDir> [--tsconfig <path>] [--stage-to <dir>] [--skip-pack] [--force] [--verbose]
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
| Flag | Description |
|
|
32
32
|
| ------------------- | -------------------------------------------------------------------------------------------- |
|
|
33
33
|
| `--tsconfig <path>` | tsconfig to extend. Defaults to `tsconfig.build.json` if present, otherwise `tsconfig.json`. |
|
|
34
|
-
| `--
|
|
35
|
-
| `--
|
|
34
|
+
| `--stage-to <dir>` | Stage into `<dir>` instead of an auto-created temp dir. Caller owns its lifecycle. |
|
|
35
|
+
| `--skip-pack` | Skip the final `npm pack` step. Requires `--stage-to`. |
|
|
36
|
+
| `--force` | With `--stage-to`, clear `<dir>` if it already has contents. |
|
|
36
37
|
| `-v`, `--verbose` | Log each pipeline phase to stderr. |
|
|
37
38
|
| `-h`, `--help` | Show help. |
|
|
38
39
|
|
|
39
|
-
The resulting `<name>-<version>.tgz` is written to the current working directory.
|
|
40
|
+
The resulting `<name>-<version>.tgz` is written to the current working directory (unless `--skip-pack` is set).
|
|
40
41
|
|
|
41
42
|
### Example
|
|
42
43
|
|
|
@@ -46,20 +47,35 @@ ts-node-pack ./packages/core --verbose
|
|
|
46
47
|
npm install ./my-core-1.2.3.tgz
|
|
47
48
|
```
|
|
48
49
|
|
|
50
|
+
### `--stage-to` and `--skip-pack`
|
|
51
|
+
|
|
52
|
+
By default `ts-node-pack` stages into an auto-created `mkdtemp()` directory, runs `npm pack` against it, copies the resulting `.tgz` to the current working directory, and removes the temp dir.
|
|
53
|
+
|
|
54
|
+
Pass `--stage-to <dir>` when you want to keep the staged contents — for example, to let another tool pack from that directory instead (`lerna publish --contents <dir>`, an alternate tarball builder, etc.). `<dir>` must either not exist, be empty, or be opted-in for clearing via `--force`.
|
|
55
|
+
|
|
56
|
+
Combine with `--skip-pack` to stop after staging and never run `npm pack` at all. `--skip-pack` is only valid with `--stage-to` (otherwise the staged contents would have no accessible location).
|
|
57
|
+
|
|
58
|
+
| Invocation | Behavior |
|
|
59
|
+
| ------------------------------------------------- | ------------------------------------------------------------------- |
|
|
60
|
+
| `ts-node-pack <pkg>` | Stage to temp dir, pack, copy `.tgz` to CWD, delete temp dir. |
|
|
61
|
+
| `ts-node-pack <pkg> --stage-to <dir>` | Stage to `<dir>`, pack, copy `.tgz` to CWD, leave `<dir>` in place. |
|
|
62
|
+
| `ts-node-pack <pkg> --stage-to <dir> --skip-pack` | Stage to `<dir>`, skip pack, leave `<dir>` in place. |
|
|
63
|
+
| `ts-node-pack <pkg> --skip-pack` | Error: `skipPack requires stageTo`. |
|
|
64
|
+
|
|
49
65
|
## Pipeline
|
|
50
66
|
|
|
51
67
|
1. **Resolve package** — read `package.json`, pick tsconfig.
|
|
52
|
-
2. **Stage** — create `mkdtemp()/package/`.
|
|
53
|
-
3. **Derived tsconfig** — write `tsconfig.emit.json` _inside the
|
|
68
|
+
2. **Stage** — use `--stage-to <dir>` if given, otherwise create `mkdtemp()/package/`. A small separate `mkdtemp()` work dir always holds auxiliary files (e.g. the derived tsconfig) so they never land in the packed contents.
|
|
69
|
+
3. **Derived tsconfig** — write `tsconfig.emit.json` _inside the work dir_ that `extends` the chosen tsconfig (by absolute path) and forces `outDir`, `declaration`, `rewriteRelativeImportExtensions: true`, `noEmit: false`. If the base tsconfig enables `sourceMap`, `inlineSourceMap`, or `declarationMap`, `inlineSources: true` is also set so debuggers get full source-level fidelity without any `.ts` files in the tarball.
|
|
54
70
|
4. **Emit** — run `tsc -p` against the derived config.
|
|
55
71
|
5. **Rewrite `.d.ts`** — for each emitted `.d.ts`, rewrite `./foo.ts` → `./foo.js` in `import` / `export from` / dynamic `import()` specifiers. Non-relative specifiers are left alone.
|
|
56
72
|
6. **Rewrite `package.json`** — rewrite `.ts` → `.js` (and → `.d.ts` under `types` conditions) in `main`, `module`, `types`, `typings`, `bin`, `exports`, and the `files` array. Strip `devDependencies` and `scripts`.
|
|
57
73
|
7. **Copy assets** — `README*`, `LICENSE*`, `CHANGELOG*`, `NOTICE*`. Source `.ts` files are never copied.
|
|
58
74
|
8. **Validate** — fail if any `.ts` specifier remains in emitted `.js` / `.d.ts` / `package.json`, or if a referenced entry point does not exist.
|
|
59
|
-
9. **Pack** — `npm pack` in the staging directory
|
|
60
|
-
10. **Cleanup** — remove
|
|
75
|
+
9. **Pack** — unless `--skip-pack`: `npm pack` in the staging directory and move the tarball to the original CWD.
|
|
76
|
+
10. **Cleanup** — always remove the work dir. In default mode this also removes the staging dir (which is nested inside). In `--stage-to` mode the staging dir is the caller's, and survives.
|
|
61
77
|
|
|
62
|
-
The source tree is never mutated.
|
|
78
|
+
The source tree is never mutated.
|
|
63
79
|
|
|
64
80
|
### Sourcemaps
|
|
65
81
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -8,8 +8,9 @@ const { values, positionals } = parseArgs({
|
|
|
8
8
|
allowPositionals: true,
|
|
9
9
|
options: {
|
|
10
10
|
tsconfig: { type: "string" },
|
|
11
|
-
"
|
|
12
|
-
"
|
|
11
|
+
"skip-pack": { type: "boolean", default: false },
|
|
12
|
+
"stage-to": { type: "string" },
|
|
13
|
+
force: { type: "boolean", default: false },
|
|
13
14
|
verbose: { type: "boolean", short: "v", default: false },
|
|
14
15
|
help: { type: "boolean", short: "h", default: false },
|
|
15
16
|
},
|
|
@@ -20,10 +21,12 @@ if (values.help || positionals.length === 0) {
|
|
|
20
21
|
|
|
21
22
|
Options:
|
|
22
23
|
--tsconfig <path> Path to tsconfig (default: tsconfig.build.json or tsconfig.json)
|
|
23
|
-
--
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
24
|
+
--stage-to <dir> Stage into <dir> instead of an auto-created temp dir.
|
|
25
|
+
Caller owns cleanup. Errors if <dir> is non-empty unless --force.
|
|
26
|
+
--skip-pack Skip the final \`npm pack\` step. Requires --stage-to.
|
|
27
|
+
--force With --stage-to, clear <dir> if it already has contents.
|
|
28
|
+
-v, --verbose Log each pipeline phase to stderr.
|
|
29
|
+
-h, --help Show this help message.
|
|
27
30
|
`);
|
|
28
31
|
process.exit(values.help ? 0 : 1);
|
|
29
32
|
}
|
|
@@ -33,8 +36,9 @@ const packageDir = resolve(positionals[0]);
|
|
|
33
36
|
try {
|
|
34
37
|
const result = await tsNodePack(packageDir, {
|
|
35
38
|
tsconfig: values.tsconfig,
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
skipPack: values["skip-pack"],
|
|
40
|
+
stageTo: values["stage-to"],
|
|
41
|
+
force: values.force,
|
|
38
42
|
verbose: values.verbose,
|
|
39
43
|
});
|
|
40
44
|
console.log(result);
|
package/src/index.d.ts
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
export interface TsNodePackOptions {
|
|
2
2
|
tsconfig?: string;
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Skip the final `npm pack` step. Requires `stageTo` (otherwise there
|
|
5
|
+
* is no way for the caller to access the staged contents).
|
|
6
|
+
*/
|
|
7
|
+
skipPack?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Stage directly into this directory instead of an auto-created temp
|
|
10
|
+
* dir. Caller owns cleanup. Errors if the directory already has
|
|
11
|
+
* contents, unless `force` is set.
|
|
12
|
+
*/
|
|
13
|
+
stageTo?: string;
|
|
14
|
+
/** With `stageTo`, clear the target directory if it already has contents. */
|
|
15
|
+
force?: boolean;
|
|
5
16
|
verbose?: boolean;
|
|
6
17
|
}
|
|
7
18
|
/**
|
|
8
19
|
* Main pipeline: pack a TypeScript package into a Node-compatible tarball.
|
|
9
|
-
* Returns the path to the .tgz file
|
|
20
|
+
* Returns the path to the .tgz file, or the staging directory when
|
|
21
|
+
* `skipPack` is set.
|
|
10
22
|
*/
|
|
11
23
|
export declare function tsNodePack(packageDir: string, options?: TsNodePackOptions): Promise<string>;
|
|
12
24
|
/**
|
package/src/index.js
CHANGED
|
@@ -15,28 +15,47 @@ import { basename, dirname, join, relative, resolve } from "node:path";
|
|
|
15
15
|
import { promisify } from "node:util";
|
|
16
16
|
import packlist from "npm-packlist";
|
|
17
17
|
import tsBlankSpace from "ts-blank-space";
|
|
18
|
-
import {
|
|
18
|
+
import { rewriteTsSpecifiers } from "./rewrite-specifiers.js";
|
|
19
|
+
import { validate } from "./validation.js";
|
|
19
20
|
|
|
20
21
|
const execFileAsync = promisify(execFile);
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
26
38
|
|
|
27
39
|
|
|
28
40
|
|
|
29
41
|
/**
|
|
30
42
|
* Main pipeline: pack a TypeScript package into a Node-compatible tarball.
|
|
31
|
-
* Returns the path to the .tgz file
|
|
43
|
+
* Returns the path to the .tgz file, or the staging directory when
|
|
44
|
+
* `skipPack` is set.
|
|
32
45
|
*/
|
|
33
46
|
export async function tsNodePack(
|
|
34
47
|
packageDir ,
|
|
35
48
|
options = {},
|
|
36
49
|
) {
|
|
37
|
-
const { tsconfig,
|
|
50
|
+
const { tsconfig, skipPack, stageTo, force, verbose } = options;
|
|
38
51
|
const log = verbose ? (...args ) => console.error("[ts-node-pack]", ...args) : () => {};
|
|
39
52
|
|
|
53
|
+
if (skipPack && !stageTo) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
"skipPack requires stageTo: caller must specify where to put the staged contents",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
40
59
|
packageDir = resolve(packageDir);
|
|
41
60
|
|
|
42
61
|
// ── Phase 1: Resolve package ──────────────────────────────────────────
|
|
@@ -48,11 +67,34 @@ export async function tsNodePack(
|
|
|
48
67
|
const tsconfigPath = resolveTsconfig(packageDir, tsconfig);
|
|
49
68
|
if (tsconfigPath) log(`Found tsconfig: ${tsconfigPath}`);
|
|
50
69
|
|
|
51
|
-
// ── Phase 2: Create staging
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
70
|
+
// ── Phase 2: Create work and staging directories ─────────────────────
|
|
71
|
+
// We always need a private work dir for auxiliary files (e.g.
|
|
72
|
+
// tsconfig.emit.json) that MUST NOT land inside the packed contents.
|
|
73
|
+
// In default mode the staging dir is nested inside the work dir, so
|
|
74
|
+
// cleaning up the work dir cleans up everything. In stageTo mode the
|
|
75
|
+
// staging dir is the caller's directory — we still create a small
|
|
76
|
+
// separate work dir for auxiliary files and rm only that in Phase 10.
|
|
77
|
+
log("Phase 2: Creating work and staging directories...");
|
|
78
|
+
const workDir = await mkdtemp(join(tmpdir(), "ts-node-pack-"));
|
|
79
|
+
let stagingDir ;
|
|
80
|
+
if (stageTo) {
|
|
81
|
+
stagingDir = resolve(stageTo);
|
|
82
|
+
if (existsSync(stagingDir)) {
|
|
83
|
+
const entries = await readdir(stagingDir);
|
|
84
|
+
if (entries.length > 0) {
|
|
85
|
+
if (!force) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`stageTo directory is not empty: ${stagingDir}. Pass force: true to clear it.`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
await mkdir(stagingDir, { recursive: true });
|
|
94
|
+
} else {
|
|
95
|
+
stagingDir = join(workDir, "package");
|
|
96
|
+
await mkdir(stagingDir, { recursive: true });
|
|
97
|
+
}
|
|
56
98
|
log(`Staging: ${stagingDir}`);
|
|
57
99
|
|
|
58
100
|
try {
|
|
@@ -100,7 +142,7 @@ export async function tsNodePack(
|
|
|
100
142
|
|
|
101
143
|
if (shouldRunTsc) {
|
|
102
144
|
log("Phase 4: Generating derived tsconfig...");
|
|
103
|
-
const emitConfigPath = join(
|
|
145
|
+
const emitConfigPath = join(workDir, "tsconfig.emit.json");
|
|
104
146
|
// tsc's typeRoots walk-up starts at the config's directory, so it
|
|
105
147
|
// can't reach packageDir/node_modules/@types from our temp-dir
|
|
106
148
|
// location. Pin typeRoots when that directory exists. (TS 6.0
|
|
@@ -187,26 +229,31 @@ export async function tsNodePack(
|
|
|
187
229
|
await validate(stagingDir, rewrittenPkg, log);
|
|
188
230
|
|
|
189
231
|
// ── Phase 9: Pack ─────────────────────────────────────────────────────
|
|
190
|
-
if (!
|
|
232
|
+
if (!skipPack) {
|
|
191
233
|
log("Phase 9: Packing...");
|
|
192
234
|
const tgzPath = await pack(stagingDir);
|
|
193
235
|
const tgzName = basename(tgzPath);
|
|
194
236
|
const dest = join(process.cwd(), tgzName);
|
|
195
237
|
await copyFile(tgzPath, dest);
|
|
238
|
+
// In stageTo mode the whole staging dir survives into the caller's
|
|
239
|
+
// filesystem, so the intermediate .tgz that `npm pack` wrote in
|
|
240
|
+
// there would stick around as visible clutter. Remove it. (In
|
|
241
|
+
// default mode the entire workDir is rm'd in Phase 10, so this
|
|
242
|
+
// unlink is redundant but harmless.)
|
|
243
|
+
if (stageTo) await unlink(tgzPath);
|
|
196
244
|
log(`Created: ${dest}`);
|
|
197
245
|
return dest;
|
|
198
246
|
}
|
|
199
247
|
|
|
200
|
-
log(`
|
|
248
|
+
log(`Skip-pack mode. Staging directory: ${stagingDir}`);
|
|
201
249
|
return stagingDir;
|
|
202
250
|
} finally {
|
|
203
251
|
// ── Phase 10: Cleanup ───────────────────────────────────────────────
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
252
|
+
// Always rm the work dir. In default mode this also removes the
|
|
253
|
+
// staging dir (nested inside). In stageTo mode the staging dir is
|
|
254
|
+
// the caller's directory and survives.
|
|
255
|
+
log("Phase 10: Cleaning up work directory...");
|
|
256
|
+
await rm(workDir, { recursive: true, force: true });
|
|
210
257
|
}
|
|
211
258
|
}
|
|
212
259
|
|
|
@@ -549,7 +596,7 @@ export function rewritePackageJson(pkg) {
|
|
|
549
596
|
} else if (result.typings) {
|
|
550
597
|
result.typings = rewriteTsToDts(result.typings);
|
|
551
598
|
} else if (mainWasTs) {
|
|
552
|
-
result.types =
|
|
599
|
+
result.types = rewriteTsToDts(originalMain);
|
|
553
600
|
}
|
|
554
601
|
|
|
555
602
|
// Rewrite bin
|
|
@@ -571,7 +618,7 @@ export function rewritePackageJson(pkg) {
|
|
|
571
618
|
// Rewrite files array
|
|
572
619
|
if (Array.isArray(result.files)) {
|
|
573
620
|
result.files = result.files.flatMap((f) => {
|
|
574
|
-
if (typeof f === "string" &&
|
|
621
|
+
if (typeof f === "string" && TS_SOURCE_EXT_RE.test(f)) {
|
|
575
622
|
return [rewriteTsToJs(f), rewriteTsToDts(f)];
|
|
576
623
|
}
|
|
577
624
|
return [f];
|
|
@@ -581,14 +628,22 @@ export function rewritePackageJson(pkg) {
|
|
|
581
628
|
return result;
|
|
582
629
|
}
|
|
583
630
|
|
|
631
|
+
// `.ts`/`.tsx` → `.js`; `.mts` → `.mjs`. Matches what `ts-blank-space`
|
|
632
|
+
// writes in Phase 6 and the specifier-rewrite pass in rewrite-specifiers.ts.
|
|
584
633
|
function rewriteTsToJs(p) {
|
|
585
|
-
|
|
634
|
+
if (typeof p !== "string") return p;
|
|
635
|
+
return p.replace(/\.mts$/, ".mjs").replace(/\.tsx?$/, ".js");
|
|
586
636
|
}
|
|
587
637
|
|
|
638
|
+
// `.ts`/`.tsx` → `.d.ts`; `.mts` → `.d.mts`. Matches the declaration files
|
|
639
|
+
// tsc emits from an .mts source.
|
|
588
640
|
function rewriteTsToDts(p) {
|
|
589
|
-
|
|
641
|
+
if (typeof p !== "string") return p;
|
|
642
|
+
return p.replace(/\.mts$/, ".d.mts").replace(/\.tsx?$/, ".d.ts");
|
|
590
643
|
}
|
|
591
644
|
|
|
645
|
+
const TS_SOURCE_EXT_RE = /\.(tsx?|mts)$/;
|
|
646
|
+
|
|
592
647
|
function rewriteExportsValue(value, isTypesKey) {
|
|
593
648
|
if (typeof value === "string") {
|
|
594
649
|
return isTypesKey ? rewriteTsToDts(value) : rewriteTsToJs(value);
|
|
@@ -606,98 +661,6 @@ function rewriteExportsValue(value, isTypesKey) {
|
|
|
606
661
|
return value;
|
|
607
662
|
}
|
|
608
663
|
|
|
609
|
-
async function validate(stagingDir, pkg, log) {
|
|
610
|
-
const errors = [];
|
|
611
|
-
|
|
612
|
-
// Check .js and .d.ts files for remaining .ts specifiers
|
|
613
|
-
const allFiles = await findFiles(
|
|
614
|
-
stagingDir,
|
|
615
|
-
(name) => name.endsWith(".js") || name.endsWith(".d.ts"),
|
|
616
|
-
);
|
|
617
|
-
|
|
618
|
-
for (const filePath of allFiles) {
|
|
619
|
-
const raw = await readFile(filePath, "utf8");
|
|
620
|
-
// Strip block and line comments before scanning so JSDoc examples that
|
|
621
|
-
// literally contain strings like `import './foo.js'` don't register as
|
|
622
|
-
// false positives. Comment stripping here is intentionally coarse
|
|
623
|
-
// (doesn't understand strings-containing-comment-markers) — good enough
|
|
624
|
-
// for tsc-emitted output, where comments are well-behaved.
|
|
625
|
-
const content = raw.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
|
|
626
|
-
const relPath = relative(stagingDir, filePath);
|
|
627
|
-
|
|
628
|
-
for (const pattern of TS_SPECIFIER_PATTERNS) {
|
|
629
|
-
for (const m of content.matchAll(pattern)) {
|
|
630
|
-
errors.push(`${relPath}: remaining .ts specifier: ${m[0]}`);
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// Check package.json for .ts references in entry points
|
|
636
|
-
const entryFields = ["main", "module", "types", "typings"];
|
|
637
|
-
for (const field of entryFields) {
|
|
638
|
-
if (typeof pkg[field] === "string" && /(?<!\.d)\.tsx?$/.test(pkg[field])) {
|
|
639
|
-
errors.push(`package.json "${field}" still references .ts: ${pkg[field]}`);
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
// Non-fatal: yarn/npm tolerate `bin`/`main` pointing at missing
|
|
644
|
-
// files. Real transformation bugs are caught by the specifier
|
|
645
|
-
// checks above.
|
|
646
|
-
const referencedFiles = collectReferencedFiles(pkg);
|
|
647
|
-
for (const ref of referencedFiles) {
|
|
648
|
-
if (!existsSync(join(stagingDir, ref))) {
|
|
649
|
-
log(` Warning: referenced file missing from tarball: ${ref}`);
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
if (errors.length > 0) {
|
|
654
|
-
const msg = "Validation failed:\n " + errors.join("\n ");
|
|
655
|
-
throw new Error(msg);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
log(`Validated ${allFiles.length} file(s), no issues found`);
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
function collectReferencedFiles(pkg) {
|
|
662
|
-
const refs = new Set ();
|
|
663
|
-
|
|
664
|
-
for (const field of ["main", "module", "types", "typings"]) {
|
|
665
|
-
if (typeof pkg[field] === "string") {
|
|
666
|
-
refs.add(pkg[field].replace(/^\.\//, ""));
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
if (typeof pkg.bin === "string") {
|
|
671
|
-
refs.add(pkg.bin.replace(/^\.\//, ""));
|
|
672
|
-
} else if (typeof pkg.bin === "object" && pkg.bin !== null) {
|
|
673
|
-
for (const v of Object.values(pkg.bin)) {
|
|
674
|
-
if (typeof v === "string") refs.add(v.replace(/^\.\//, ""));
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
if (pkg.exports) {
|
|
679
|
-
collectExportsRefs(pkg.exports, refs);
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
return refs;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
function collectExportsRefs(value, refs) {
|
|
686
|
-
if (typeof value === "string") {
|
|
687
|
-
refs.add(value.replace(/^\.\//, ""));
|
|
688
|
-
return;
|
|
689
|
-
}
|
|
690
|
-
if (Array.isArray(value)) {
|
|
691
|
-
for (const v of value) collectExportsRefs(v, refs);
|
|
692
|
-
return;
|
|
693
|
-
}
|
|
694
|
-
if (typeof value === "object" && value !== null) {
|
|
695
|
-
for (const v of Object.values(value)) {
|
|
696
|
-
collectExportsRefs(v, refs);
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
|
|
701
664
|
async function pack(stagingDir) {
|
|
702
665
|
const { stdout } = await execFileAsync("npm", ["pack"], {
|
|
703
666
|
cwd: stagingDir,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare function validate(stagingDir: any, pkg: any, log?: (...args: unknown[]) => void): Promise<void>;
|
|
2
|
+
/**
|
|
3
|
+
* Collect the relative file paths referenced by path-bearing fields of a
|
|
4
|
+
* package.json manifest, partitioned by how strictly a missing target
|
|
5
|
+
* should be treated.
|
|
6
|
+
*
|
|
7
|
+
* - `strict`: `main`, `module`, `types`, `typings`, and every leaf of
|
|
8
|
+
* `exports`. A missing target here is a bug in the tarball we produced —
|
|
9
|
+
* Node's module resolution will fail and `npm publish` warns on
|
|
10
|
+
* dangling `types`.
|
|
11
|
+
* - `lenient`: `bin` entries. yarn and npm both tolerate a `bin` pointing
|
|
12
|
+
* at a missing file (see the `bin-missing` fixture, which mirrors
|
|
13
|
+
* agoric/portfolio-api's intentional shape).
|
|
14
|
+
*
|
|
15
|
+
* Exported for direct unit-testing of the policy; the only runtime caller
|
|
16
|
+
* is `validate()` above.
|
|
17
|
+
*/
|
|
18
|
+
export declare function collectReferencedFiles(pkg: any): {
|
|
19
|
+
strict: Set<string>;
|
|
20
|
+
lenient: Set<string>;
|
|
21
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-rewrite validation of a staged package. `validate()` is the one
|
|
3
|
+
* caller of `collectReferencedFiles`; both live here because the
|
|
4
|
+
* strict-vs-lenient partition is a policy decision owned by validation.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
8
|
+
import { join, relative } from "node:path";
|
|
9
|
+
import { TS_SPECIFIER_PATTERNS } from "./rewrite-specifiers.js";
|
|
10
|
+
|
|
11
|
+
export async function validate(
|
|
12
|
+
stagingDir,
|
|
13
|
+
pkg,
|
|
14
|
+
log = () => {},
|
|
15
|
+
) {
|
|
16
|
+
const errors = [];
|
|
17
|
+
|
|
18
|
+
// Check .js/.mjs and .d.ts/.d.mts files for remaining .ts specifiers
|
|
19
|
+
const allFiles = await findFiles(
|
|
20
|
+
stagingDir,
|
|
21
|
+
(name) =>
|
|
22
|
+
name.endsWith(".js") ||
|
|
23
|
+
name.endsWith(".mjs") ||
|
|
24
|
+
name.endsWith(".d.ts") ||
|
|
25
|
+
name.endsWith(".d.mts"),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
for (const filePath of allFiles) {
|
|
29
|
+
const raw = await readFile(filePath, "utf8");
|
|
30
|
+
// Strip block and line comments before scanning so JSDoc examples that
|
|
31
|
+
// literally contain strings like `import './foo.js'` don't register as
|
|
32
|
+
// false positives. Comment stripping here is intentionally coarse
|
|
33
|
+
// (doesn't understand strings-containing-comment-markers) — good enough
|
|
34
|
+
// for tsc-emitted output, where comments are well-behaved.
|
|
35
|
+
const content = raw.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
|
|
36
|
+
const relPath = relative(stagingDir, filePath);
|
|
37
|
+
|
|
38
|
+
for (const pattern of TS_SPECIFIER_PATTERNS) {
|
|
39
|
+
for (const m of content.matchAll(pattern)) {
|
|
40
|
+
errors.push(`${relPath}: remaining .ts specifier: ${m[0]}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check package.json for .ts references in entry points
|
|
46
|
+
const entryFields = ["main", "module", "types", "typings"];
|
|
47
|
+
for (const field of entryFields) {
|
|
48
|
+
if (typeof pkg[field] === "string" && /(?<!\.d)\.(tsx?|mts)$/.test(pkg[field])) {
|
|
49
|
+
errors.push(`package.json "${field}" still references .ts: ${pkg[field]}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// main/module/types/typings/exports must resolve to a file that is
|
|
54
|
+
// actually in the staging dir. This catches rewrite bugs that specifier
|
|
55
|
+
// scanning can't — e.g. the @endo/ses-ava regression where a synthesized
|
|
56
|
+
// `types: ./index.d.ts` pointed at a .d.ts ts-node-pack never emitted.
|
|
57
|
+
// `bin` is deliberately excluded: yarn/npm both tolerate a missing bin
|
|
58
|
+
// target (see the bin-missing fixture — agoric/portfolio-api ships that
|
|
59
|
+
// way intentionally), so we only warn.
|
|
60
|
+
const { strict: strictRefs, lenient: lenientRefs } = collectReferencedFiles(pkg);
|
|
61
|
+
for (const ref of strictRefs) {
|
|
62
|
+
if (!existsSync(join(stagingDir, ref))) {
|
|
63
|
+
errors.push(`referenced file missing from tarball: ${ref}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const ref of lenientRefs) {
|
|
67
|
+
if (!existsSync(join(stagingDir, ref))) {
|
|
68
|
+
log(` Warning: referenced file missing from tarball: ${ref}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (errors.length > 0) {
|
|
73
|
+
const msg = "Validation failed:\n " + errors.join("\n ");
|
|
74
|
+
throw new Error(msg);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
log(`Validated ${allFiles.length} file(s), no issues found`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Collect the relative file paths referenced by path-bearing fields of a
|
|
82
|
+
* package.json manifest, partitioned by how strictly a missing target
|
|
83
|
+
* should be treated.
|
|
84
|
+
*
|
|
85
|
+
* - `strict`: `main`, `module`, `types`, `typings`, and every leaf of
|
|
86
|
+
* `exports`. A missing target here is a bug in the tarball we produced —
|
|
87
|
+
* Node's module resolution will fail and `npm publish` warns on
|
|
88
|
+
* dangling `types`.
|
|
89
|
+
* - `lenient`: `bin` entries. yarn and npm both tolerate a `bin` pointing
|
|
90
|
+
* at a missing file (see the `bin-missing` fixture, which mirrors
|
|
91
|
+
* agoric/portfolio-api's intentional shape).
|
|
92
|
+
*
|
|
93
|
+
* Exported for direct unit-testing of the policy; the only runtime caller
|
|
94
|
+
* is `validate()` above.
|
|
95
|
+
*/
|
|
96
|
+
export function collectReferencedFiles(pkg) {
|
|
97
|
+
const strict = new Set ();
|
|
98
|
+
const lenient = new Set ();
|
|
99
|
+
const addStrict = (p) => strict.add(stripDotSlash(p));
|
|
100
|
+
const addLenient = (p) => lenient.add(stripDotSlash(p));
|
|
101
|
+
|
|
102
|
+
for (const field of ["main", "module", "types", "typings"]) {
|
|
103
|
+
if (typeof pkg[field] === "string") addStrict(pkg[field]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (typeof pkg.bin === "string") {
|
|
107
|
+
addLenient(pkg.bin);
|
|
108
|
+
} else if (typeof pkg.bin === "object" && pkg.bin !== null) {
|
|
109
|
+
for (const v of Object.values(pkg.bin)) {
|
|
110
|
+
if (typeof v === "string") addLenient(v);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (pkg.exports) collectExportsRefs(pkg.exports, strict);
|
|
115
|
+
|
|
116
|
+
return { strict, lenient };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function collectExportsRefs(value, refs) {
|
|
120
|
+
if (typeof value === "string") {
|
|
121
|
+
refs.add(stripDotSlash(value));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (Array.isArray(value)) {
|
|
125
|
+
for (const v of value) collectExportsRefs(v, refs);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (typeof value === "object" && value !== null) {
|
|
129
|
+
for (const v of Object.values(value)) collectExportsRefs(v, refs);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function stripDotSlash(p) {
|
|
134
|
+
return p.replace(/^\.\//, "");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function findFiles(dir, test) {
|
|
138
|
+
const results = [];
|
|
139
|
+
const entries = await readdir(dir, { withFileTypes: true, recursive: true });
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
if (entry.isFile() && test(entry.name)) {
|
|
142
|
+
results.push(join(entry.parentPath, entry.name));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return results;
|
|
146
|
+
}
|