ts-node-pack 0.1.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/LICENSE +21 -0
- package/README.md +77 -0
- package/package.json +38 -0
- package/src/cli.d.ts +2 -0
- package/src/cli.js +44 -0
- package/src/index.d.ts +82 -0
- package/src/index.js +771 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Turadg Aleahmad
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# ts-node-pack
|
|
2
|
+
|
|
3
|
+
Pack a TypeScript package into a Node-compatible npm tarball — without modifying the source tree, without bundling, and without changing module resolution semantics.
|
|
4
|
+
|
|
5
|
+
Given a TypeScript package whose sources use `.ts` files and `.ts` in relative import specifiers, `ts-node-pack` produces a `.tgz` whose contents are plain `.js` + `.d.ts` with correct `.js` specifiers, ready to `npm install` into any Node ESM project.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
TypeScript 5.7 introduced [`rewriteRelativeImportExtensions`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-7.html#path-rewriting-for-relative-paths), which lets you author `import './foo.ts'` and have `tsc` emit `import './foo.js'` in the compiled `.js`. But:
|
|
10
|
+
|
|
11
|
+
- `tsc` does **not** rewrite `.ts` specifiers inside emitted `.d.ts` files.
|
|
12
|
+
- Your `package.json` (`main`, `module`, `exports`, `types`) still points at `.ts`.
|
|
13
|
+
- You probably don't want the `.ts` sources in the published tarball at all.
|
|
14
|
+
|
|
15
|
+
`ts-node-pack` wraps `tsc` + `npm pack` and fills in exactly those gaps.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm install --save-dev ts-node-pack
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Requires Node ≥ 20 and TypeScript ≥ 5.7 available in the package being packed (resolved via `npx tsc`).
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
ts-node-pack <packageDir> [--tsconfig <path>] [--emit-only] [--keep-temp] [--verbose]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
| Flag | Description |
|
|
32
|
+
| ------------------- | -------------------------------------------------------------------------------------------- |
|
|
33
|
+
| `--tsconfig <path>` | tsconfig to extend. Defaults to `tsconfig.build.json` if present, otherwise `tsconfig.json`. |
|
|
34
|
+
| `--emit-only` | Run emit + rewrites + validation, but skip `npm pack`. Prints the staging directory. |
|
|
35
|
+
| `--keep-temp` | Do not delete the temporary staging directory on exit. |
|
|
36
|
+
| `-v`, `--verbose` | Log each pipeline phase to stderr. |
|
|
37
|
+
| `-h`, `--help` | Show help. |
|
|
38
|
+
|
|
39
|
+
The resulting `<name>-<version>.tgz` is written to the current working directory.
|
|
40
|
+
|
|
41
|
+
### Example
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
cd my-project
|
|
45
|
+
ts-node-pack ./packages/core --verbose
|
|
46
|
+
npm install ./my-core-1.2.3.tgz
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Pipeline
|
|
50
|
+
|
|
51
|
+
1. **Resolve package** — read `package.json`, pick tsconfig.
|
|
52
|
+
2. **Stage** — create `mkdtemp()/package/`.
|
|
53
|
+
3. **Derived tsconfig** — write `tsconfig.emit.json` _inside the temp 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
|
+
4. **Emit** — run `tsc -p` against the derived config.
|
|
55
|
+
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
|
+
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
|
+
7. **Copy assets** — `README*`, `LICENSE*`, `CHANGELOG*`, `NOTICE*`. Source `.ts` files are never copied.
|
|
58
|
+
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; move the tarball to the original CWD.
|
|
60
|
+
10. **Cleanup** — remove `.ts-node-pack/` and the temp directory (unless `--keep-temp`).
|
|
61
|
+
|
|
62
|
+
The source tree is never mutated. All intermediate artifacts (derived tsconfig, staging dir, tarball) live under a single `mkdtemp()` directory that is removed on exit.
|
|
63
|
+
|
|
64
|
+
### Sourcemaps
|
|
65
|
+
|
|
66
|
+
If your tsconfig has `sourceMap` (or `inlineSourceMap` / `declarationMap`) enabled, `ts-node-pack` automatically forces `inlineSources: true` so each emitted `.map` embeds its source text via `sourcesContent`. This gives full source-level debugging and "Go to Definition" without shipping `.ts` files — sidestepping dual-resolution hazards where a downstream bundler or TS project might pick `./foo.ts` over `./foo.js`. Detection is a shallow read of the chosen tsconfig; if you inherit `sourceMap` from a base config via `extends`, set it explicitly at the leaf.
|
|
67
|
+
|
|
68
|
+
## Non-goals
|
|
69
|
+
|
|
70
|
+
- No bundling.
|
|
71
|
+
- No AST transforms.
|
|
72
|
+
- No custom module resolution.
|
|
73
|
+
- No `npm publish` logic.
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ts-node-pack",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pack a TypeScript package into a Node-compatible npm tarball without modifying the source tree",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"esm",
|
|
8
|
+
"npm",
|
|
9
|
+
"pack",
|
|
10
|
+
"typescript"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "Turadg Aleahmad <turadg@aleahmad.net>",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/turadg/ts-node-pack.git"
|
|
17
|
+
},
|
|
18
|
+
"bin": {
|
|
19
|
+
"ts-node-pack": "./src/cli.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "./src/index.js",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": "./src/index.js"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20"
|
|
31
|
+
},
|
|
32
|
+
"packageManager": "pnpm@10.33.0",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"npm-packlist": "^10.0.4",
|
|
35
|
+
"ts-blank-space": "^0.8.0"
|
|
36
|
+
},
|
|
37
|
+
"types": "./src/index.d.ts"
|
|
38
|
+
}
|
package/src/cli.d.ts
ADDED
package/src/cli.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { tsNodePack } from "./index.js";
|
|
6
|
+
|
|
7
|
+
const { values, positionals } = parseArgs({
|
|
8
|
+
allowPositionals: true,
|
|
9
|
+
options: {
|
|
10
|
+
tsconfig: { type: "string" },
|
|
11
|
+
"emit-only": { type: "boolean", default: false },
|
|
12
|
+
"keep-temp": { type: "boolean", default: false },
|
|
13
|
+
verbose: { type: "boolean", short: "v", default: false },
|
|
14
|
+
help: { type: "boolean", short: "h", default: false },
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if (values.help || positionals.length === 0) {
|
|
19
|
+
console.log(`Usage: ts-node-pack <packageDir> [options]
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--tsconfig <path> Path to tsconfig (default: tsconfig.build.json or tsconfig.json)
|
|
23
|
+
--emit-only Emit compiled files without packing
|
|
24
|
+
--keep-temp Keep temporary staging directory
|
|
25
|
+
-v, --verbose Verbose output
|
|
26
|
+
-h, --help Show this help message
|
|
27
|
+
`);
|
|
28
|
+
process.exit(values.help ? 0 : 1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const packageDir = resolve(positionals[0]);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const result = await tsNodePack(packageDir, {
|
|
35
|
+
tsconfig: values.tsconfig,
|
|
36
|
+
emitOnly: values["emit-only"],
|
|
37
|
+
keepTemp: values["keep-temp"],
|
|
38
|
+
verbose: values.verbose,
|
|
39
|
+
});
|
|
40
|
+
console.log(result);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error(err.message);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export interface TsNodePackOptions {
|
|
2
|
+
tsconfig?: string;
|
|
3
|
+
emitOnly?: boolean;
|
|
4
|
+
keepTemp?: boolean;
|
|
5
|
+
verbose?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Main pipeline: pack a TypeScript package into a Node-compatible tarball.
|
|
9
|
+
* Returns the path to the .tgz file (or the staging dir if --emit-only).
|
|
10
|
+
*/
|
|
11
|
+
export declare function tsNodePack(packageDir: string, options?: TsNodePackOptions): Promise<string>;
|
|
12
|
+
/**
|
|
13
|
+
* Rewrite .ts/.tsx extensions to .js in relative import/export specifiers.
|
|
14
|
+
*
|
|
15
|
+
* Why regex is sound here:
|
|
16
|
+
*
|
|
17
|
+
* The input is exclusively tsc-generated .d.ts files, not arbitrary source.
|
|
18
|
+
* tsc emits a highly constrained subset of syntax in declarations:
|
|
19
|
+
*
|
|
20
|
+
* - Import/export specifiers are always single-line string literals.
|
|
21
|
+
* - Specifiers are always wrapped in matching quotes (' or ").
|
|
22
|
+
* - Relative paths always start with ./ or ../ (tsc never normalizes these).
|
|
23
|
+
* - No template literals, concatenation, or computed specifiers appear.
|
|
24
|
+
* - The only keywords introducing module specifiers are `from`, `import`,
|
|
25
|
+
* and `import()`.
|
|
26
|
+
*
|
|
27
|
+
* Because the grammar of specifiers in .d.ts output is regular (a finite
|
|
28
|
+
* set of keyword prefixes + a quoted string literal), regex matches it
|
|
29
|
+
* exactly — no ambiguity, no context-sensitivity, no false positives.
|
|
30
|
+
*
|
|
31
|
+
* If tsc ever adds native .d.ts rewriting (TypeScript#56556), this
|
|
32
|
+
* function becomes a no-op: the `if (rewritten !== original)` guard
|
|
33
|
+
* in rewriteDtsFiles means no file is touched.
|
|
34
|
+
*
|
|
35
|
+
* Handles:
|
|
36
|
+
* - Named/default/type imports: import { x } from './foo.js'
|
|
37
|
+
* - Re-exports: export { x } from './foo.js'
|
|
38
|
+
* - Star re-exports: export * from './foo.js'
|
|
39
|
+
* - Side-effect imports: import './foo.js'
|
|
40
|
+
* - Dynamic imports: import('./foo.js')
|
|
41
|
+
* - .tsx extensions: import './component.js'
|
|
42
|
+
*
|
|
43
|
+
* Does NOT rewrite (by design):
|
|
44
|
+
* - Non-relative specifiers: import 'lodash'
|
|
45
|
+
* - Already-.js specifiers: import './foo.js'
|
|
46
|
+
* - import.meta.url: not a module specifier
|
|
47
|
+
* - require(): not valid in ESM .d.ts
|
|
48
|
+
* - Bare string literals: const x = './foo.ts'
|
|
49
|
+
*/
|
|
50
|
+
export declare const TS_SPECIFIER_PATTERNS: RegExp[];
|
|
51
|
+
export declare function rewriteTsSpecifiers(content: any): any;
|
|
52
|
+
/**
|
|
53
|
+
* Resolve `workspace:` protocol specifiers in the package's dependency
|
|
54
|
+
* fields to concrete versions, matching yarn-berry and pnpm behavior.
|
|
55
|
+
*
|
|
56
|
+
* Translation rules:
|
|
57
|
+
* workspace:* → <version>
|
|
58
|
+
* workspace:^ → ^<version>
|
|
59
|
+
* workspace:~ → ~<version>
|
|
60
|
+
* workspace:<range> → <range>
|
|
61
|
+
*
|
|
62
|
+
* Walks upward from packageDir to find the workspace root (the first
|
|
63
|
+
* ancestor whose package.json has a `workspaces` field), expands its
|
|
64
|
+
* workspace globs to locate sibling packages, and builds a
|
|
65
|
+
* `name → version` map.
|
|
66
|
+
*
|
|
67
|
+
* Throws if the package has `workspace:` specifiers but no workspace
|
|
68
|
+
* root can be found, or if a referenced sibling package is unknown.
|
|
69
|
+
* These failure modes indicate a broken workspace, not a ts-node-pack
|
|
70
|
+
* bug — surface them loudly rather than shipping an unpublishable
|
|
71
|
+
* tarball.
|
|
72
|
+
*/
|
|
73
|
+
export declare function resolveWorkspaceDependencies(pkg: any, packageDir: any, log: any): Promise<any>;
|
|
74
|
+
/**
|
|
75
|
+
* Translate a single `workspace:` specifier into the published form.
|
|
76
|
+
* workspace:* → <version>
|
|
77
|
+
* workspace:^ → ^<version>
|
|
78
|
+
* workspace:~ → ~<version>
|
|
79
|
+
* workspace:<range> → <range>
|
|
80
|
+
*/
|
|
81
|
+
export declare function resolveWorkspaceSpec(spec: any, version: any): any;
|
|
82
|
+
export declare function rewritePackageJson(pkg: any): any;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import {
|
|
4
|
+
copyFile,
|
|
5
|
+
mkdir,
|
|
6
|
+
mkdtemp,
|
|
7
|
+
readdir,
|
|
8
|
+
readFile,
|
|
9
|
+
rm,
|
|
10
|
+
unlink,
|
|
11
|
+
writeFile,
|
|
12
|
+
} from "node:fs/promises";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
15
|
+
import { promisify } from "node:util";
|
|
16
|
+
import packlist from "npm-packlist";
|
|
17
|
+
import tsBlankSpace from "ts-blank-space";
|
|
18
|
+
|
|
19
|
+
const execFileAsync = promisify(execFile);
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Main pipeline: pack a TypeScript package into a Node-compatible tarball.
|
|
30
|
+
* Returns the path to the .tgz file (or the staging dir if --emit-only).
|
|
31
|
+
*/
|
|
32
|
+
export async function tsNodePack(
|
|
33
|
+
packageDir ,
|
|
34
|
+
options = {},
|
|
35
|
+
) {
|
|
36
|
+
const { tsconfig, emitOnly, keepTemp, verbose } = options;
|
|
37
|
+
const log = verbose ? (...args ) => console.error("[ts-node-pack]", ...args) : () => {};
|
|
38
|
+
|
|
39
|
+
packageDir = resolve(packageDir);
|
|
40
|
+
|
|
41
|
+
// ── Phase 1: Resolve package ──────────────────────────────────────────
|
|
42
|
+
log("Phase 1: Resolving package...");
|
|
43
|
+
const pkgJsonPath = join(packageDir, "package.json");
|
|
44
|
+
const pkgJson = JSON.parse(await readFile(pkgJsonPath, "utf8"));
|
|
45
|
+
log(`Package: ${pkgJson.name}@${pkgJson.version}`);
|
|
46
|
+
|
|
47
|
+
const tsconfigPath = resolveTsconfig(packageDir, tsconfig);
|
|
48
|
+
log(`Using tsconfig: ${tsconfigPath}`);
|
|
49
|
+
|
|
50
|
+
// ── Phase 2: Create staging directory ─────────────────────────────────
|
|
51
|
+
log("Phase 2: Creating staging directory...");
|
|
52
|
+
const tmpDir = await mkdtemp(join(tmpdir(), "ts-node-pack-"));
|
|
53
|
+
const stagingDir = join(tmpDir, "package");
|
|
54
|
+
await mkdir(stagingDir, { recursive: true });
|
|
55
|
+
log(`Staging: ${stagingDir}`);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
// ── Phase 3: Generate derived tsconfig ────────────────────────────────
|
|
59
|
+
// Written inside the temp dir — never touches the source tree.
|
|
60
|
+
// `extends` takes an absolute path; paths defined by the base tsconfig
|
|
61
|
+
// (include/files/exclude/rootDir/paths) resolve relative to the base
|
|
62
|
+
// config's own location, which is still inside packageDir, so globs
|
|
63
|
+
// continue to work.
|
|
64
|
+
log("Phase 3: Generating derived tsconfig...");
|
|
65
|
+
const emitConfigPath = join(tmpDir, "tsconfig.emit.json");
|
|
66
|
+
|
|
67
|
+
// Because the emit config lives outside packageDir, tsc's typeRoots
|
|
68
|
+
// walk-up (which starts at the config's directory) can't reach
|
|
69
|
+
// packageDir/node_modules/@types. Pin typeRoots explicitly when that
|
|
70
|
+
// directory exists so entries named in the base config's `types`
|
|
71
|
+
// field — e.g. `types: ["node"]` — can be resolved. TS 6.0 made this
|
|
72
|
+
// explicit opt-in (no more auto-inclusion of every @types package),
|
|
73
|
+
// which surfaces the missing resolution as a clear TS2688 error
|
|
74
|
+
// rather than silent "Cannot find module 'node:util'".
|
|
75
|
+
const atTypesDir = join(packageDir, "node_modules", "@types");
|
|
76
|
+
const hasAtTypes = existsSync(atTypesDir);
|
|
77
|
+
|
|
78
|
+
const emitConfig = {
|
|
79
|
+
extends: tsconfigPath,
|
|
80
|
+
compilerOptions: {
|
|
81
|
+
// Force rootDir so emit preserves the source layout — otherwise
|
|
82
|
+
// tsc infers the common ancestor and strips the `src/` prefix,
|
|
83
|
+
// breaking `main`/`exports` that reference `./src/...`.
|
|
84
|
+
rootDir: packageDir,
|
|
85
|
+
outDir: stagingDir,
|
|
86
|
+
declaration: true,
|
|
87
|
+
// ts-blank-space handles .js emit (Phase 6); tsc only emits .d.ts.
|
|
88
|
+
emitDeclarationOnly: true,
|
|
89
|
+
// Extract types from JS+JSDoc sources in mixed packages.
|
|
90
|
+
allowJs: true,
|
|
91
|
+
noEmit: false,
|
|
92
|
+
// Incremental/composite would try to write a .tsbuildinfo whose
|
|
93
|
+
// path tsc computes relative to the base config, producing
|
|
94
|
+
// garbled paths when our outDir crosses directory trees.
|
|
95
|
+
incremental: false,
|
|
96
|
+
composite: false,
|
|
97
|
+
tsBuildInfoFile: null,
|
|
98
|
+
...(hasAtTypes ? { typeRoots: [atTypesDir] } : {}),
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
await writeFile(emitConfigPath, JSON.stringify(emitConfig, null, 2) + "\n");
|
|
102
|
+
|
|
103
|
+
// ── Phase 4: Copy the npm-packlist into staging ──────────────────────
|
|
104
|
+
// `npm-packlist` is the same file-enumeration engine `npm pack` uses
|
|
105
|
+
// internally. It applies `files`, `.npmignore`, the npm default
|
|
106
|
+
// includes (package.json, README*, LICENSE*) and default excludes
|
|
107
|
+
// (.git, node_modules, ...) — so the staging directory ends up as a
|
|
108
|
+
// faithful mirror of whatever `npm pack` would have produced from
|
|
109
|
+
// the source tree, minus our downstream transforms (ts-blank-space,
|
|
110
|
+
// declaration emit, specifier rewrite).
|
|
111
|
+
log("Phase 4: Enumerating package files via npm-packlist...");
|
|
112
|
+
// npm-packlist's tree-based API (v7+) expects a node with `path` +
|
|
113
|
+
// parsed `package.json`. We fabricate a minimal tree rather than
|
|
114
|
+
// pulling in @npmcli/arborist — we don't need bundled-dependency
|
|
115
|
+
// resolution, workspace walking, or the rest of arborist's
|
|
116
|
+
// machinery, just the pack-file list.
|
|
117
|
+
//
|
|
118
|
+
// `isProjectRoot: true` sends npm-packlist down the
|
|
119
|
+
// "root package" branch of `gatherBundles`, which looks only at
|
|
120
|
+
// bundleDependencies (missing here, so no-op). Without this, it
|
|
121
|
+
// takes the "bundled dep of some other project" branch and tries
|
|
122
|
+
// to walk `edgesOut` — which doesn't exist on our hand-built tree.
|
|
123
|
+
const packFiles = await packlist({
|
|
124
|
+
path: packageDir,
|
|
125
|
+
package: pkgJson,
|
|
126
|
+
isProjectRoot: true,
|
|
127
|
+
});
|
|
128
|
+
// Pre-create every parent directory serially (mkdir on the same
|
|
129
|
+
// path from multiple concurrent calls can race), then copy files
|
|
130
|
+
// in parallel.
|
|
131
|
+
const dirs = new Set (
|
|
132
|
+
packFiles.map((rel) => dirname(join(stagingDir, rel))),
|
|
133
|
+
);
|
|
134
|
+
for (const d of dirs) await mkdir(d, { recursive: true });
|
|
135
|
+
await Promise.all(
|
|
136
|
+
packFiles.map((rel) =>
|
|
137
|
+
copyFile(join(packageDir, rel), join(stagingDir, rel)),
|
|
138
|
+
),
|
|
139
|
+
);
|
|
140
|
+
log(`Copied ${packFiles.length} file(s) into staging`);
|
|
141
|
+
|
|
142
|
+
// ── Phase 5: Emit declarations via tsc ──────────────────────────────
|
|
143
|
+
// tsc reads sources from packageDir (the base config's include
|
|
144
|
+
// resolves there) and writes .d.ts files into stagingDir via outDir.
|
|
145
|
+
// The copies from Phase 4 are left alone — tsc's outputs land
|
|
146
|
+
// alongside them. tsc may emit declarations for files outside the
|
|
147
|
+
// `files` array (e.g. test/); those won't make it into the final
|
|
148
|
+
// tarball because npm pack only ships what `files` references.
|
|
149
|
+
log("Phase 5: Emitting .d.ts files via tsc...");
|
|
150
|
+
await runTsc(emitConfigPath, packageDir, log);
|
|
151
|
+
|
|
152
|
+
// ── Phase 6: Strip types and rewrite specifiers ─────────────────────
|
|
153
|
+
// Single-pass transform of every staging file that could contain a
|
|
154
|
+
// relative module specifier: .ts/.mts get ts-blank-space'd then
|
|
155
|
+
// specifier-rewritten (one read, one write, original unlinked);
|
|
156
|
+
// .js/.mjs/.d.ts/.d.mts just get specifier-rewritten in place.
|
|
157
|
+
// ts-blank-space preserves line and column positions, so no
|
|
158
|
+
// sourcemaps are needed for debugging.
|
|
159
|
+
log("Phase 6: Stripping types and rewriting specifiers...");
|
|
160
|
+
const { strippedCount, rewrittenCount } = await processStagingFiles(
|
|
161
|
+
stagingDir,
|
|
162
|
+
log,
|
|
163
|
+
);
|
|
164
|
+
log(
|
|
165
|
+
`Stripped ${strippedCount} type-annotated file(s); rewrote ${rewrittenCount} other file(s)`,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// ── Phase 7: Rewrite package.json ───────────────────────────────────
|
|
169
|
+
// README, LICENSE, and any other npm-always-included files already
|
|
170
|
+
// landed in staging via Phase 4. We overwrite the copy of
|
|
171
|
+
// package.json with a rewritten one: first resolve any
|
|
172
|
+
// `workspace:*` protocol specifiers (required for monorepo
|
|
173
|
+
// packages to publish), then flip entry paths and strip dev-only
|
|
174
|
+
// fields.
|
|
175
|
+
log("Phase 7: Rewriting package.json...");
|
|
176
|
+
const resolvedPkg = await resolveWorkspaceDependencies(
|
|
177
|
+
pkgJson,
|
|
178
|
+
packageDir,
|
|
179
|
+
log,
|
|
180
|
+
);
|
|
181
|
+
const rewrittenPkg = rewritePackageJson(resolvedPkg);
|
|
182
|
+
await writeFile(
|
|
183
|
+
join(stagingDir, "package.json"),
|
|
184
|
+
JSON.stringify(rewrittenPkg, null, 2) + "\n",
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
// ── Phase 8: Validate ─────────────────────────────────────────────────
|
|
188
|
+
log("Phase 8: Validating...");
|
|
189
|
+
await validate(stagingDir, rewrittenPkg, log);
|
|
190
|
+
|
|
191
|
+
// ── Phase 9: Pack ─────────────────────────────────────────────────────
|
|
192
|
+
if (!emitOnly) {
|
|
193
|
+
log("Phase 9: Packing...");
|
|
194
|
+
const tgzPath = await pack(stagingDir, log);
|
|
195
|
+
const tgzName = basename(tgzPath);
|
|
196
|
+
const dest = join(process.cwd(), tgzName);
|
|
197
|
+
await copyFile(tgzPath, dest);
|
|
198
|
+
log(`Created: ${dest}`);
|
|
199
|
+
return dest;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
log(`Emit-only mode. Staging directory: ${stagingDir}`);
|
|
203
|
+
return stagingDir;
|
|
204
|
+
} finally {
|
|
205
|
+
// ── Phase 10: Cleanup ───────────────────────────────────────────────
|
|
206
|
+
if (!emitOnly && !keepTemp) {
|
|
207
|
+
log("Phase 10: Cleaning up temp directory...");
|
|
208
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
209
|
+
} else if (keepTemp) {
|
|
210
|
+
log(`Keeping temp directory: ${tmpDir}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Phase helpers ──────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
function resolveTsconfig(packageDir, tsconfigOption) {
|
|
218
|
+
if (tsconfigOption) {
|
|
219
|
+
const p = resolve(packageDir, tsconfigOption);
|
|
220
|
+
if (!existsSync(p)) {
|
|
221
|
+
throw new Error(`tsconfig not found: ${p}`);
|
|
222
|
+
}
|
|
223
|
+
return p;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const buildConfig = join(packageDir, "tsconfig.build.json");
|
|
227
|
+
if (existsSync(buildConfig)) return buildConfig;
|
|
228
|
+
|
|
229
|
+
const defaultConfig = join(packageDir, "tsconfig.json");
|
|
230
|
+
if (existsSync(defaultConfig)) return defaultConfig;
|
|
231
|
+
|
|
232
|
+
throw new Error(`No tsconfig found in ${packageDir}. Provide one with --tsconfig.`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Walk upward from `startDir`, returning the first `node_modules/.bin/<name>`
|
|
237
|
+
* that exists. Mirrors how npm composes `$PATH` for scripts in a workspace.
|
|
238
|
+
* Returns null when nothing is found before the filesystem root.
|
|
239
|
+
*/
|
|
240
|
+
function findLocalBin(startDir, name) {
|
|
241
|
+
let dir = startDir;
|
|
242
|
+
while (true) {
|
|
243
|
+
const candidate = join(dir, "node_modules", ".bin", name);
|
|
244
|
+
if (existsSync(candidate)) return candidate;
|
|
245
|
+
const parent = dirname(dir);
|
|
246
|
+
if (parent === dir) return null;
|
|
247
|
+
dir = parent;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function runTsc(emitConfigPath, cwd, log) {
|
|
252
|
+
// Prefer a local tsc so users control the compiler version (and to avoid
|
|
253
|
+
// npx resolving to macOS's /usr/bin/tsc — the TeX/Smalltalk compiler —
|
|
254
|
+
// when no local install exists). In a monorepo, the package's own
|
|
255
|
+
// node_modules/.bin may be empty while the workspace root has the
|
|
256
|
+
// binary, so walk upward the same way npm's `$PATH` composition does.
|
|
257
|
+
const binName = process.platform === "win32" ? "tsc.cmd" : "tsc";
|
|
258
|
+
const localTsc = findLocalBin(cwd, binName);
|
|
259
|
+
const useLocal = localTsc !== null;
|
|
260
|
+
const [cmd, argv] = useLocal
|
|
261
|
+
? [localTsc, ["-p", emitConfigPath]]
|
|
262
|
+
: ["npx", ["--yes", "tsc", "-p", emitConfigPath]];
|
|
263
|
+
log(useLocal ? `Using local tsc: ${localTsc}` : "Using npx tsc (no local install found)");
|
|
264
|
+
try {
|
|
265
|
+
const { stdout, stderr } = await execFileAsync(cmd, argv, {
|
|
266
|
+
cwd,
|
|
267
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
268
|
+
});
|
|
269
|
+
if (stdout.trim()) log(stdout.trim());
|
|
270
|
+
if (stderr.trim()) log(stderr.trim());
|
|
271
|
+
} catch (err) {
|
|
272
|
+
const output = [err.stdout, err.stderr].filter(Boolean).join("\n").trim();
|
|
273
|
+
if (output) console.error(output);
|
|
274
|
+
throw new Error("TypeScript compilation failed");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Walk stagingDir for every file that might contain a relative module
|
|
280
|
+
* specifier — .ts/.mts (strip types then rewrite), and .js/.mjs/.d.ts/
|
|
281
|
+
* .d.mts (rewrite specifiers only). Each file is read once and, if
|
|
282
|
+
* changed, written once. Sourcemap files (.d.ts.map, .d.mts.map) are
|
|
283
|
+
* skipped because they're binary-encoded JSON with no specifiers.
|
|
284
|
+
*
|
|
285
|
+
* Throws if ts-blank-space rejects a .ts/.mts file (non-erasable
|
|
286
|
+
* syntax like `enum`, `namespace`, or parameter properties).
|
|
287
|
+
*/
|
|
288
|
+
async function processStagingFiles(stagingDir, log) {
|
|
289
|
+
const files = await findFiles(
|
|
290
|
+
stagingDir,
|
|
291
|
+
(name) =>
|
|
292
|
+
(name.endsWith(".ts") && !name.endsWith(".d.ts")) ||
|
|
293
|
+
(name.endsWith(".mts") && !name.endsWith(".d.mts")) ||
|
|
294
|
+
name.endsWith(".js") ||
|
|
295
|
+
name.endsWith(".mjs") ||
|
|
296
|
+
(name.endsWith(".d.ts") && !name.endsWith(".d.ts.map")) ||
|
|
297
|
+
(name.endsWith(".d.mts") && !name.endsWith(".d.mts.map")),
|
|
298
|
+
);
|
|
299
|
+
let strippedCount = 0;
|
|
300
|
+
let rewrittenCount = 0;
|
|
301
|
+
for (const srcPath of files) {
|
|
302
|
+
const rel = relative(stagingDir, srcPath);
|
|
303
|
+
const isTs = srcPath.endsWith(".ts") && !srcPath.endsWith(".d.ts");
|
|
304
|
+
const isMts = srcPath.endsWith(".mts") && !srcPath.endsWith(".d.mts");
|
|
305
|
+
const source = await readFile(srcPath, "utf8");
|
|
306
|
+
|
|
307
|
+
let content = source;
|
|
308
|
+
if (isTs || isMts) {
|
|
309
|
+
const errors = [];
|
|
310
|
+
content = tsBlankSpace(source, (node) => {
|
|
311
|
+
errors.push(
|
|
312
|
+
`${rel}: unsupported non-erasable syntax: ${String(node.getText?.() ?? node).slice(0, 80)}`,
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
if (errors.length > 0) {
|
|
316
|
+
throw new Error(
|
|
317
|
+
"ts-blank-space rejected non-erasable TypeScript:\n " +
|
|
318
|
+
errors.join("\n "),
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
content = rewriteTsSpecifiers(content);
|
|
323
|
+
|
|
324
|
+
if (isTs || isMts) {
|
|
325
|
+
const destPath = srcPath.replace(
|
|
326
|
+
isMts ? /\.mts$/ : /\.ts$/,
|
|
327
|
+
isMts ? ".mjs" : ".js",
|
|
328
|
+
);
|
|
329
|
+
await writeFile(destPath, content);
|
|
330
|
+
await unlink(srcPath);
|
|
331
|
+
strippedCount++;
|
|
332
|
+
log(` Stripped: ${rel} → ${basename(destPath)}`);
|
|
333
|
+
} else if (content !== source) {
|
|
334
|
+
await writeFile(srcPath, content);
|
|
335
|
+
rewrittenCount++;
|
|
336
|
+
log(` Rewrote: ${rel}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return { strippedCount, rewrittenCount };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Rewrite .ts/.tsx extensions to .js in relative import/export specifiers.
|
|
344
|
+
*
|
|
345
|
+
* Why regex is sound here:
|
|
346
|
+
*
|
|
347
|
+
* The input is exclusively tsc-generated .d.ts files, not arbitrary source.
|
|
348
|
+
* tsc emits a highly constrained subset of syntax in declarations:
|
|
349
|
+
*
|
|
350
|
+
* - Import/export specifiers are always single-line string literals.
|
|
351
|
+
* - Specifiers are always wrapped in matching quotes (' or ").
|
|
352
|
+
* - Relative paths always start with ./ or ../ (tsc never normalizes these).
|
|
353
|
+
* - No template literals, concatenation, or computed specifiers appear.
|
|
354
|
+
* - The only keywords introducing module specifiers are `from`, `import`,
|
|
355
|
+
* and `import()`.
|
|
356
|
+
*
|
|
357
|
+
* Because the grammar of specifiers in .d.ts output is regular (a finite
|
|
358
|
+
* set of keyword prefixes + a quoted string literal), regex matches it
|
|
359
|
+
* exactly — no ambiguity, no context-sensitivity, no false positives.
|
|
360
|
+
*
|
|
361
|
+
* If tsc ever adds native .d.ts rewriting (TypeScript#56556), this
|
|
362
|
+
* function becomes a no-op: the `if (rewritten !== original)` guard
|
|
363
|
+
* in rewriteDtsFiles means no file is touched.
|
|
364
|
+
*
|
|
365
|
+
* Handles:
|
|
366
|
+
* - Named/default/type imports: import { x } from './foo.js'
|
|
367
|
+
* - Re-exports: export { x } from './foo.js'
|
|
368
|
+
* - Star re-exports: export * from './foo.js'
|
|
369
|
+
* - Side-effect imports: import './foo.js'
|
|
370
|
+
* - Dynamic imports: import('./foo.js')
|
|
371
|
+
* - .tsx extensions: import './component.js'
|
|
372
|
+
*
|
|
373
|
+
* Does NOT rewrite (by design):
|
|
374
|
+
* - Non-relative specifiers: import 'lodash'
|
|
375
|
+
* - Already-.js specifiers: import './foo.js'
|
|
376
|
+
* - import.meta.url: not a module specifier
|
|
377
|
+
* - require(): not valid in ESM .d.ts
|
|
378
|
+
* - Bare string literals: const x = './foo.ts'
|
|
379
|
+
*/
|
|
380
|
+
// Three shapes for a relative TypeScript specifier in a string literal:
|
|
381
|
+
// 1. `from './foo.js'` — named/default/type imports, re-exports
|
|
382
|
+
// 2. `import './foo.js'` — bare side-effect imports
|
|
383
|
+
// 3. `import('./foo.js')` — dynamic imports
|
|
384
|
+
//
|
|
385
|
+
// The extension capture group matches `ts`, `tsx`, or `mts`. Rewriting
|
|
386
|
+
// turns `.ts`/`.tsx` into `.js` and `.mts` into `.mjs`, matching the
|
|
387
|
+
// output extensions ts-blank-space produces in Phase 6.
|
|
388
|
+
//
|
|
389
|
+
// Used both to rewrite specifiers and (in the validator) to detect any
|
|
390
|
+
// specifier that slipped through. Must stay in sync with each other.
|
|
391
|
+
export const TS_SPECIFIER_PATTERNS = [
|
|
392
|
+
/(from\s*['"])(\.\.?\/[^'"]*?)\.(tsx?|mts)(['"])/g,
|
|
393
|
+
/(import\s+['"])(\.\.?\/[^'"]*?)\.(tsx?|mts)(['"])/g,
|
|
394
|
+
/(import\s*\(\s*['"])(\.\.?\/[^'"]*?)\.(tsx?|mts)(['"]\s*\))/g,
|
|
395
|
+
];
|
|
396
|
+
|
|
397
|
+
export function rewriteTsSpecifiers(content) {
|
|
398
|
+
for (const pattern of TS_SPECIFIER_PATTERNS) {
|
|
399
|
+
content = content.replace(pattern, (_m, pre, path, ext, post) =>
|
|
400
|
+
pre + path + (ext === "mts" ? ".mjs" : ".js") + post,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
return content;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const DEP_FIELDS_WITH_WORKSPACE = [
|
|
407
|
+
"dependencies",
|
|
408
|
+
"peerDependencies",
|
|
409
|
+
"optionalDependencies",
|
|
410
|
+
"devDependencies",
|
|
411
|
+
];
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Resolve `workspace:` protocol specifiers in the package's dependency
|
|
415
|
+
* fields to concrete versions, matching yarn-berry and pnpm behavior.
|
|
416
|
+
*
|
|
417
|
+
* Translation rules:
|
|
418
|
+
* workspace:* → <version>
|
|
419
|
+
* workspace:^ → ^<version>
|
|
420
|
+
* workspace:~ → ~<version>
|
|
421
|
+
* workspace:<range> → <range>
|
|
422
|
+
*
|
|
423
|
+
* Walks upward from packageDir to find the workspace root (the first
|
|
424
|
+
* ancestor whose package.json has a `workspaces` field), expands its
|
|
425
|
+
* workspace globs to locate sibling packages, and builds a
|
|
426
|
+
* `name → version` map.
|
|
427
|
+
*
|
|
428
|
+
* Throws if the package has `workspace:` specifiers but no workspace
|
|
429
|
+
* root can be found, or if a referenced sibling package is unknown.
|
|
430
|
+
* These failure modes indicate a broken workspace, not a ts-node-pack
|
|
431
|
+
* bug — surface them loudly rather than shipping an unpublishable
|
|
432
|
+
* tarball.
|
|
433
|
+
*/
|
|
434
|
+
export async function resolveWorkspaceDependencies(pkg, packageDir, log) {
|
|
435
|
+
if (!hasWorkspaceDeps(pkg)) return pkg;
|
|
436
|
+
|
|
437
|
+
const workspaceRoot = await findWorkspaceRoot(packageDir);
|
|
438
|
+
if (!workspaceRoot) {
|
|
439
|
+
throw new Error(
|
|
440
|
+
`${pkg.name || "package"}: found 'workspace:' dependencies but no workspace root ancestor has a 'workspaces' field`,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
log(`Resolving workspace: deps against workspace root ${workspaceRoot}`);
|
|
444
|
+
|
|
445
|
+
const versionMap = await buildWorkspaceVersionMap(workspaceRoot);
|
|
446
|
+
|
|
447
|
+
const result = { ...pkg };
|
|
448
|
+
for (const field of DEP_FIELDS_WITH_WORKSPACE) {
|
|
449
|
+
const deps = result[field];
|
|
450
|
+
if (!deps || typeof deps !== "object") continue;
|
|
451
|
+
const updated = { ...deps };
|
|
452
|
+
for (const [name, spec] of Object.entries(updated)) {
|
|
453
|
+
if (typeof spec !== "string" || !spec.startsWith("workspace:")) continue;
|
|
454
|
+
const version = versionMap.get(name);
|
|
455
|
+
if (!version) {
|
|
456
|
+
throw new Error(
|
|
457
|
+
`${pkg.name || "package"}: ${field}["${name}"] = "${spec}" but no workspace package named "${name}" was found under ${workspaceRoot}`,
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
const resolved = resolveWorkspaceSpec(spec, version);
|
|
461
|
+
updated[name] = resolved;
|
|
462
|
+
log(` ${field}["${name}"]: ${spec} → ${resolved}`);
|
|
463
|
+
}
|
|
464
|
+
result[field] = updated;
|
|
465
|
+
}
|
|
466
|
+
return result;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function hasWorkspaceDeps(pkg) {
|
|
470
|
+
for (const field of DEP_FIELDS_WITH_WORKSPACE) {
|
|
471
|
+
const deps = pkg[field];
|
|
472
|
+
if (!deps || typeof deps !== "object") continue;
|
|
473
|
+
for (const spec of Object.values(deps)) {
|
|
474
|
+
if (typeof spec === "string" && spec.startsWith("workspace:")) return true;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function findWorkspaceRoot(startDir) {
|
|
481
|
+
let dir = startDir;
|
|
482
|
+
while (true) {
|
|
483
|
+
try {
|
|
484
|
+
const pkg = JSON.parse(
|
|
485
|
+
await readFile(join(dir, "package.json"), "utf8"),
|
|
486
|
+
);
|
|
487
|
+
if (pkg.workspaces) return dir;
|
|
488
|
+
} catch {
|
|
489
|
+
// Missing or unreadable package.json — not the root, keep going.
|
|
490
|
+
}
|
|
491
|
+
const parent = dirname(dir);
|
|
492
|
+
if (parent === dir) return null;
|
|
493
|
+
dir = parent;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function buildWorkspaceVersionMap(workspaceRoot) {
|
|
498
|
+
const rootPkg = JSON.parse(
|
|
499
|
+
await readFile(join(workspaceRoot, "package.json"), "utf8"),
|
|
500
|
+
);
|
|
501
|
+
const patterns = Array.isArray(rootPkg.workspaces)
|
|
502
|
+
? rootPkg.workspaces
|
|
503
|
+
: rootPkg.workspaces?.packages || [];
|
|
504
|
+
|
|
505
|
+
const map = new Map ();
|
|
506
|
+
for (const pattern of patterns) {
|
|
507
|
+
const dirs = await expandWorkspacePattern(workspaceRoot, pattern);
|
|
508
|
+
for (const d of dirs) {
|
|
509
|
+
try {
|
|
510
|
+
const pkg = JSON.parse(
|
|
511
|
+
await readFile(join(d, "package.json"), "utf8"),
|
|
512
|
+
);
|
|
513
|
+
if (pkg.name && pkg.version) map.set(pkg.name, pkg.version);
|
|
514
|
+
} catch {
|
|
515
|
+
// Missing or malformed sibling package.json — skip.
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return map;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Expand a workspace pattern (e.g. "packages/*", "apps/*", "solo") into
|
|
524
|
+
* concrete directory paths. Supports `*` as a single-segment wildcard.
|
|
525
|
+
* `**` is not supported — no real-world agoric-sdk-style workspace uses
|
|
526
|
+
* it, and implementing recursive globs without a library is more code
|
|
527
|
+
* than it's worth for the common case.
|
|
528
|
+
*/
|
|
529
|
+
async function expandWorkspacePattern(workspaceRoot, pattern) {
|
|
530
|
+
if (pattern.includes("**")) {
|
|
531
|
+
throw new Error(`Unsupported workspace pattern: "${pattern}" (globstar ** is not supported)`);
|
|
532
|
+
}
|
|
533
|
+
const parts = pattern.split("/").filter((p) => p.length > 0);
|
|
534
|
+
let currentDirs = [workspaceRoot];
|
|
535
|
+
for (const part of parts) {
|
|
536
|
+
const next = [];
|
|
537
|
+
for (const base of currentDirs) {
|
|
538
|
+
if (part === "*") {
|
|
539
|
+
try {
|
|
540
|
+
const entries = await readdir(base, { withFileTypes: true });
|
|
541
|
+
for (const e of entries) {
|
|
542
|
+
if (e.isDirectory()) next.push(join(base, e.name));
|
|
543
|
+
}
|
|
544
|
+
} catch {
|
|
545
|
+
// base doesn't exist — skip.
|
|
546
|
+
}
|
|
547
|
+
} else if (part.includes("*")) {
|
|
548
|
+
const regex = new RegExp(
|
|
549
|
+
"^" +
|
|
550
|
+
part.replace(/[.+^$()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") +
|
|
551
|
+
"$",
|
|
552
|
+
);
|
|
553
|
+
try {
|
|
554
|
+
const entries = await readdir(base, { withFileTypes: true });
|
|
555
|
+
for (const e of entries) {
|
|
556
|
+
if (e.isDirectory() && regex.test(e.name)) next.push(join(base, e.name));
|
|
557
|
+
}
|
|
558
|
+
} catch {
|
|
559
|
+
// base doesn't exist — skip.
|
|
560
|
+
}
|
|
561
|
+
} else {
|
|
562
|
+
next.push(join(base, part));
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
currentDirs = next;
|
|
566
|
+
}
|
|
567
|
+
return currentDirs;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Translate a single `workspace:` specifier into the published form.
|
|
572
|
+
* workspace:* → <version>
|
|
573
|
+
* workspace:^ → ^<version>
|
|
574
|
+
* workspace:~ → ~<version>
|
|
575
|
+
* workspace:<range> → <range>
|
|
576
|
+
*/
|
|
577
|
+
export function resolveWorkspaceSpec(spec, version) {
|
|
578
|
+
if (typeof spec !== "string" || !spec.startsWith("workspace:")) return spec;
|
|
579
|
+
const rest = spec.slice("workspace:".length);
|
|
580
|
+
if (rest === "" || rest === "*") return version;
|
|
581
|
+
if (rest === "^") return `^${version}`;
|
|
582
|
+
if (rest === "~") return `~${version}`;
|
|
583
|
+
return rest;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export function rewritePackageJson(pkg) {
|
|
587
|
+
const result = { ...pkg };
|
|
588
|
+
|
|
589
|
+
// Remove development-only fields that aren't needed in the published package
|
|
590
|
+
delete result.devDependencies;
|
|
591
|
+
delete result.scripts;
|
|
592
|
+
|
|
593
|
+
// Rewrite entry points
|
|
594
|
+
if (result.main) result.main = rewriteTsToJs(result.main);
|
|
595
|
+
if (result.module) result.module = rewriteTsToJs(result.module);
|
|
596
|
+
|
|
597
|
+
// Rewrite or derive types
|
|
598
|
+
if (result.types) {
|
|
599
|
+
result.types = rewriteTsToDts(result.types);
|
|
600
|
+
} else if (result.typings) {
|
|
601
|
+
result.typings = rewriteTsToDts(result.typings);
|
|
602
|
+
} else if (result.main) {
|
|
603
|
+
result.types = result.main.replace(/\.js$/, ".d.ts");
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Rewrite bin
|
|
607
|
+
if (result.bin) {
|
|
608
|
+
if (typeof result.bin === "string") {
|
|
609
|
+
result.bin = rewriteTsToJs(result.bin);
|
|
610
|
+
} else if (typeof result.bin === "object") {
|
|
611
|
+
for (const [key, value] of Object.entries(result.bin)) {
|
|
612
|
+
result.bin[key] = rewriteTsToJs(value);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Rewrite exports
|
|
618
|
+
if (result.exports) {
|
|
619
|
+
result.exports = rewriteExportsValue(result.exports, false);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Rewrite files array
|
|
623
|
+
if (Array.isArray(result.files)) {
|
|
624
|
+
result.files = result.files.flatMap((f) => {
|
|
625
|
+
if (typeof f === "string" && /\.tsx?$/.test(f)) {
|
|
626
|
+
return [rewriteTsToJs(f), rewriteTsToDts(f)];
|
|
627
|
+
}
|
|
628
|
+
return [f];
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return result;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function rewriteTsToJs(p) {
|
|
636
|
+
return typeof p === "string" ? p.replace(/\.tsx?$/, ".js") : p;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function rewriteTsToDts(p) {
|
|
640
|
+
return typeof p === "string" ? p.replace(/\.tsx?$/, ".d.ts") : p;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function rewriteExportsValue(value, isTypesKey) {
|
|
644
|
+
if (typeof value === "string") {
|
|
645
|
+
return isTypesKey ? rewriteTsToDts(value) : rewriteTsToJs(value);
|
|
646
|
+
}
|
|
647
|
+
if (Array.isArray(value)) {
|
|
648
|
+
return value.map((v) => rewriteExportsValue(v, isTypesKey));
|
|
649
|
+
}
|
|
650
|
+
if (typeof value === "object" && value !== null) {
|
|
651
|
+
const result = {};
|
|
652
|
+
for (const [key, val] of Object.entries(value)) {
|
|
653
|
+
result[key] = rewriteExportsValue(val, key === "types");
|
|
654
|
+
}
|
|
655
|
+
return result;
|
|
656
|
+
}
|
|
657
|
+
return value;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
async function validate(stagingDir, pkg, log) {
|
|
661
|
+
const errors = [];
|
|
662
|
+
|
|
663
|
+
// Check .js and .d.ts files for remaining .ts specifiers
|
|
664
|
+
const allFiles = await findFiles(
|
|
665
|
+
stagingDir,
|
|
666
|
+
(name) => name.endsWith(".js") || name.endsWith(".d.ts"),
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
for (const filePath of allFiles) {
|
|
670
|
+
const raw = await readFile(filePath, "utf8");
|
|
671
|
+
// Strip block and line comments before scanning so JSDoc examples that
|
|
672
|
+
// literally contain strings like `import './foo.js'` don't register as
|
|
673
|
+
// false positives. Comment stripping here is intentionally coarse
|
|
674
|
+
// (doesn't understand strings-containing-comment-markers) — good enough
|
|
675
|
+
// for tsc-emitted output, where comments are well-behaved.
|
|
676
|
+
const content = raw
|
|
677
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
678
|
+
.replace(/(^|[^:])\/\/.*$/gm, "$1");
|
|
679
|
+
const relPath = relative(stagingDir, filePath);
|
|
680
|
+
|
|
681
|
+
for (const pattern of TS_SPECIFIER_PATTERNS) {
|
|
682
|
+
for (const m of content.matchAll(pattern)) {
|
|
683
|
+
errors.push(`${relPath}: remaining .ts specifier: ${m[0]}`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Check package.json for .ts references in entry points
|
|
689
|
+
const entryFields = ["main", "module", "types", "typings"];
|
|
690
|
+
for (const field of entryFields) {
|
|
691
|
+
if (typeof pkg[field] === "string" && /(?<!\.d)\.tsx?$/.test(pkg[field])) {
|
|
692
|
+
errors.push(`package.json "${field}" still references .ts: ${pkg[field]}`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Non-fatal: yarn/npm tolerate `bin`/`main` pointing at missing
|
|
697
|
+
// files. Real transformation bugs are caught by the specifier
|
|
698
|
+
// checks above.
|
|
699
|
+
const referencedFiles = collectReferencedFiles(pkg);
|
|
700
|
+
for (const ref of referencedFiles) {
|
|
701
|
+
if (!existsSync(join(stagingDir, ref))) {
|
|
702
|
+
log(` Warning: referenced file missing from tarball: ${ref}`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (errors.length > 0) {
|
|
707
|
+
const msg = "Validation failed:\n " + errors.join("\n ");
|
|
708
|
+
throw new Error(msg);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
log(`Validated ${allFiles.length} file(s), no issues found`);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function collectReferencedFiles(pkg) {
|
|
715
|
+
const refs = new Set ();
|
|
716
|
+
|
|
717
|
+
for (const field of ["main", "module", "types", "typings"]) {
|
|
718
|
+
if (typeof pkg[field] === "string") {
|
|
719
|
+
refs.add(pkg[field].replace(/^\.\//, ""));
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (typeof pkg.bin === "string") {
|
|
724
|
+
refs.add(pkg.bin.replace(/^\.\//, ""));
|
|
725
|
+
} else if (typeof pkg.bin === "object" && pkg.bin !== null) {
|
|
726
|
+
for (const v of Object.values(pkg.bin)) {
|
|
727
|
+
if (typeof v === "string") refs.add(v.replace(/^\.\//, ""));
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (pkg.exports) {
|
|
732
|
+
collectExportsRefs(pkg.exports, refs);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
return refs;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function collectExportsRefs(value, refs) {
|
|
739
|
+
if (typeof value === "string") {
|
|
740
|
+
refs.add(value.replace(/^\.\//, ""));
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (Array.isArray(value)) {
|
|
744
|
+
for (const v of value) collectExportsRefs(v, refs);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (typeof value === "object" && value !== null) {
|
|
748
|
+
for (const v of Object.values(value)) {
|
|
749
|
+
collectExportsRefs(v, refs);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
async function pack(stagingDir, log) {
|
|
755
|
+
const { stdout } = await execFileAsync("npm", ["pack"], {
|
|
756
|
+
cwd: stagingDir,
|
|
757
|
+
});
|
|
758
|
+
const tgzName = stdout.trim().split("\n").pop();
|
|
759
|
+
return join(stagingDir, tgzName);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async function findFiles(dir, test) {
|
|
763
|
+
const results = [];
|
|
764
|
+
const entries = await readdir(dir, { withFileTypes: true, recursive: true });
|
|
765
|
+
for (const entry of entries) {
|
|
766
|
+
if (entry.isFile() && test(entry.name)) {
|
|
767
|
+
results.push(join(entry.parentPath, entry.name));
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return results;
|
|
771
|
+
}
|