@git.zone/tspublish 1.11.6 โ†’ 1.12.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 CHANGED
@@ -1,309 +1,209 @@
1
- # @git.zone/tspublish ๐Ÿš€
1
+ # @git.zone/tspublish
2
2
 
3
- > **Effortlessly publish multiple TypeScript packages from your monorepo**
4
-
5
- [![npm version](https://img.shields.io/npm/v/@git.zone/tspublish.svg)](https://www.npmjs.com/package/@git.zone/tspublish)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
-
8
- ## ๐ŸŒŸ What is tspublish?
9
-
10
- `@git.zone/tspublish` is a powerful CLI tool and library for managing and publishing multiple TypeScript packages from a monorepo. It automates the tedious parts of package publishing โ€” discovery, dependency resolution, building, version validation, and multi-registry publishing โ€” while giving you full control over the process. Whether you're maintaining a suite of microservices, a component library, or any collection of related packages, tspublish makes your life dramatically easier.
3
+ Prepare and publish independently installable TypeScript packages from one repository.
4
+ Each component has its own dependency list and package metadata; components share
5
+ the repository version. TsBuild owns compilation. A release coordinator can prepare
6
+ all packages before packing, journaling and publishing any of them.
11
7
 
12
8
  ## Issue Reporting and Security
13
9
 
14
10
  For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
15
11
 
16
- ### โœจ Key Features
17
-
18
- - ๐Ÿ“ฆ **Automatic Package Discovery** โ€” Scans your monorepo for publishable `ts*` directories
19
- - ๐ŸŽจ **Beautiful CLI Output** โ€” Color-coded logging with progress bars and status indicators
20
- - ๐Ÿ” **Version Collision Detection** โ€” Prevents accidental overwrites by checking the registry first
21
- - ๐Ÿ—๏ธ **Build Integration** โ€” Automatically compiles TypeScript before publishing via `@git.zone/tsbuild`
22
- - ๐ŸŽฏ **Smart Dependency Management** โ€” Inherits dependency versions from your monorepo's `package.json`
23
- - ๐ŸŒ **Multi-Registry Support** โ€” Publish to npm, GitHub Packages, Gitea, or private registries
24
- - ๐Ÿ”— **Base Registry Inheritance** โ€” Use `useBase` / `extendBase` to inherit registries from `.smartconfig.json`
25
- - โšก **CLI Binary Support** โ€” Automatically generates `cli.js` wrappers for publishable CLI packages
26
- - ๐Ÿงน **Clean Builds** โ€” Creates isolated `dist_publish_*` directories for each package
27
-
28
- ## ๐Ÿ“ฅ Installation
29
-
30
- ```bash
31
- # Using pnpm (recommended)
32
- pnpm add -D @git.zone/tspublish
33
-
34
- # Global installation for CLI usage
35
- pnpm add -g @git.zone/tspublish
36
- ```
37
-
38
- ## ๐Ÿš€ Quick Start
39
-
40
- ### 1๏ธโƒฃ Structure Your Monorepo
41
-
42
- Organize your packages using directories that start with `ts`:
43
-
44
- ```
45
- my-awesome-monorepo/
46
- โ”œโ”€โ”€ package.json # Main monorepo package.json (version is inherited)
47
- โ”œโ”€โ”€ tsconfig.json # Shared TypeScript config
48
- โ”œโ”€โ”€ .smartconfig.json # Optional: base registry configuration
49
- โ”œโ”€โ”€ ts_core/ # Core package
50
- โ”‚ โ”œโ”€โ”€ index.ts # Entry point
51
- โ”‚ โ”œโ”€โ”€ readme.md # Package-specific documentation
52
- โ”‚ โ””โ”€โ”€ tspublish.json # Publishing configuration
53
- โ”œโ”€โ”€ ts_utils/ # Utilities package
54
- โ”‚ โ”œโ”€โ”€ index.ts
55
- โ”‚ โ”œโ”€โ”€ readme.md
56
- โ”‚ โ””โ”€โ”€ tspublish.json
57
- โ””โ”€โ”€ ts_cli/ # CLI package
58
- โ”œโ”€โ”€ index.ts
59
- โ”œโ”€โ”€ readme.md
60
- โ””โ”€โ”€ tspublish.json
61
- ```
62
-
63
- ### 2๏ธโƒฃ Configure Each Package
64
-
65
- Create a `tspublish.json` in each publishable directory:
66
-
67
- ```json
68
- {
69
- "name": "@myorg/core",
70
- "order": 1,
71
- "dependencies": [
72
- "@push.rocks/smartpromise",
73
- "@push.rocks/smartfile"
74
- ],
75
- "registries": [
76
- "registry.npmjs.org:public"
77
- ],
78
- "bin": []
79
- }
80
- ```
81
-
82
- #### Configuration Options
12
+ ## Install
83
13
 
84
- | Field | Type | Description |
85
- |-------|------|-------------|
86
- | `name` | `string` | The published package name (e.g., `@myorg/core`) |
87
- | `order` | `number` | Build order for interdependent packages (lower builds first) |
88
- | `dependencies` | `string[]` | Dependencies to include โ€” versions are resolved from the monorepo's `package.json` |
89
- | `registries` | `string[]` | Target registries with access level (format: `"url:accessLevel"`) |
90
- | `bin` | `string[]` | CLI executable names (generates a `cli.js` wrapper automatically) |
91
-
92
- ### 3๏ธโƒฃ Publish
93
-
94
- ```bash
95
- # From your monorepo root
96
- npx tspublish
14
+ ```sh
15
+ pnpm install --save-dev @git.zone/tspublish @git.zone/tsbuild
97
16
  ```
98
17
 
99
- That's it! tspublish will discover all `ts*` directories containing `tspublish.json`, build them in order, validate versions against the registry, and publish.
100
-
101
- ## ๐ŸŽฏ Advanced Usage
102
-
103
- ### Registry Configuration
18
+ ## Repository structure
104
19
 
105
- TSPublish offers three approaches for configuring target registries:
20
+ Keep the repository manifest private when it holds dependencies for the entire
21
+ toolbox. Each named module becomes a separate public package with a filtered
22
+ dependency manifest.
106
23
 
107
- #### 1. Explicit Registries
108
-
109
- Define specific registries directly in `tspublish.json`:
110
-
111
- ```json
112
- {
113
- "registries": [
114
- "registry.npmjs.org:public",
115
- "npm.pkg.github.com:private"
116
- ]
117
- }
24
+ ```text
25
+ package.json
26
+ tsconfig.json
27
+ license.md
28
+ ts_core/
29
+ index.ts
30
+ readme.md
31
+ tspublish.json
32
+ ts_adapter/
33
+ index.ts
34
+ readme.md
35
+ tspublish.json
118
36
  ```
119
37
 
120
- The format is `"registryUrl:accessLevel"` where `accessLevel` is `public` or `private`.
38
+ The repository's `package.json` supplies the shared version and external dependency
39
+ versions. Internal package dependencies use the exact shared version, even when an
40
+ older published version appears in the root manifest. Unknown external dependencies
41
+ fail validation instead of being assigned the repository version.
121
42
 
122
- #### 2. Use Base Configuration (`useBase`)
43
+ Each package requires its own `readme.md`. The root license is copied into every
44
+ prepared package. `license.md` is preferred; existing repositories using `license`
45
+ remain supported.
123
46
 
124
- Inherit registries from your project's `.smartconfig.json` (managed by `@git.zone/cli`):
47
+ ## Module configuration
125
48
 
126
49
  ```json
127
50
  {
128
- "registries": ["useBase"]
51
+ "name": "@example/adapter",
52
+ "order": 2,
53
+ "description": "Optional provider adapter for the example toolbox.",
54
+ "dependencies": ["@example/core", "external-sdk"],
55
+ "registries": ["useBase"],
56
+ "bin": [],
57
+ "engines": { "node": ">=24" },
58
+ "sideEffects": false,
59
+ "include": ["third-party-notices.md"]
129
60
  }
130
61
  ```
131
62
 
132
- This reads from `.smartconfig.json` at the key `@git.zone/cli.release.registries`.
133
-
134
- #### 3. Extend Base Configuration (`extendBase`)
135
-
136
- Start with base registries and add or remove specific ones:
63
+ Supported fields:
64
+
65
+ | Field | Meaning |
66
+ | --- | --- |
67
+ | `name` | Published npm package name. Unnamed descriptors remain available to TsBuild for ordering only. |
68
+ | `order` | Ordering preference among modules whose dependencies are ready. Dependencies always precede consumers. |
69
+ | `dependencies` | Required package names, resolved from sibling modules or root production dependencies. |
70
+ | `optionalDependencies` | Optional dependency names, resolved by the same rules. |
71
+ | `peerDependencies` | Explicit peer version ranges. |
72
+ | `peerDependenciesMeta` | Optional-peer metadata. |
73
+ | `description` | Package description; defaults to the root description. |
74
+ | `engines` | Runtime requirements; defaults to the root engines. |
75
+ | `sideEffects` | Standard package side-effect declaration. |
76
+ | `folders` | Additional `ts_*` folders owned by this package. A folder may have only one owner. |
77
+ | `exports` | Explicit subpath/conditional exports into the package's compiled folders. Defaults to typed root exports. |
78
+ | `include` | Exact repository-relative asset files or directories, including applicable notices. No globs or component source/compiled folders; use `folders` to declare source ownership. |
79
+ | `bin` | Executable names using the module's exported `runCli()` entrypoint. Wrappers are generated locally. |
80
+ | `registries` | Destination intent for the standalone publisher; also returned to release coordinators for validation. |
81
+
82
+ Root author, license, repository, homepage, bugs, keywords and funding metadata are
83
+ preserved. The private repository flag, root scripts, unrelated dependencies and
84
+ development dependencies are not copied into prepared packages.
85
+
86
+ Legacy `main` and `types` fields are derived from the root export's `node`,
87
+ `import`, `default`, and `types` conditions when available. They never point at
88
+ an unrelated default entrypoint when root exports are customized.
89
+
90
+ For a subpath, include the additional source folder and declare both runtime and
91
+ type entrypoints:
137
92
 
138
93
  ```json
139
94
  {
140
- "registries": [
141
- "extendBase",
142
- "custom-registry.example.com:public",
143
- "-https://registry.npmjs.org"
144
- ]
145
- }
146
- ```
147
-
148
- The `-` prefix excludes a registry from the base configuration. All other entries (besides `extendBase`) are added.
149
-
150
- #### Empty Registries
151
-
152
- If `registries` is an empty array `[]`, the package will be built but **not published** โ€” useful for internal-only packages that other packages depend on.
153
-
154
- ### Build Order Management
155
-
156
- When packages depend on each other, use the `order` field to control build sequence:
157
-
158
- ```jsonc
159
- // ts_core/tspublish.json โ€” builds first
160
- {
161
- "name": "@myorg/core",
95
+ "name": "@example/core",
162
96
  "order": 1,
163
97
  "dependencies": [],
164
98
  "registries": ["useBase"],
165
- "bin": []
166
- }
167
-
168
- // ts_utils/tspublish.json โ€” builds second, depends on core
169
- {
170
- "name": "@myorg/utils",
171
- "order": 2,
172
- "dependencies": ["@myorg/core"],
173
- "registries": ["useBase"],
174
- "bin": []
99
+ "folders": ["ts_migration"],
100
+ "exports": {
101
+ ".": {
102
+ "types": "./dist_ts_core/index.d.ts",
103
+ "import": "./dist_ts_core/index.js"
104
+ },
105
+ "./migration": {
106
+ "types": "./dist_ts_migration/index.d.ts",
107
+ "import": "./dist_ts_migration/index.js"
108
+ }
109
+ }
175
110
  }
176
111
  ```
177
112
 
178
- ### CLI Binary Packages
113
+ ## Imports and local development
179
114
 
180
- For packages that ship CLI tools, specify the binary names in the `bin` array:
115
+ Import sibling components through their published package names and declare them
116
+ in `dependencies`. Configure TypeScript paths to their source entrypoints within
117
+ the same repository; GitZone's TypeScript formatter discovers these mappings from
118
+ `tspublish.json`. This requires no workspace symlinks or local package dependencies.
181
119
 
182
- ```json
183
- {
184
- "name": "@myorg/cli",
185
- "order": 3,
186
- "dependencies": ["commander", "@myorg/core"],
187
- "registries": ["registry.npmjs.org:public"],
188
- "bin": ["my-cli", "my-tool"]
189
- }
120
+ Run the normal repository build before preparation:
121
+
122
+ ```sh
123
+ pnpm build
190
124
  ```
191
125
 
192
- TSPublish will:
193
- 1. Fetch the standard `cli.js` template from the `@git.zone/cli` assets repository
194
- 2. Adjust the import path to point to the correct `dist_*` folder
195
- 3. Configure the `bin` field in the generated `package.json`
126
+ Preparation requires the matching `dist_ts*` outputs and verifies every export
127
+ target. It inspects literal imports in sources and declarations and rejects
128
+ undeclared dependencies and relative imports outside the component. Computed
129
+ runtime imports remain the responsibility of the module author. Built-in Node
130
+ modules are allowed. Browser compatibility must still be tested by each component.
196
131
 
197
- ### Programmatic Usage
132
+ Source folders, compiled folders and explicit assets are copied separately for
133
+ each package. Symlinks, credential files and local dependency directories are
134
+ rejected. Preparation does not bundle dependency implementations.
135
+
136
+ ## Plan and prepare
198
137
 
199
138
  ```typescript
200
139
  import { TsPublish } from '@git.zone/tspublish';
201
140
 
202
141
  const publisher = new TsPublish();
142
+ const plan = await publisher.plan(process.cwd());
143
+ // plan.modules is in deterministic dependency order; order equals its array index.
203
144
 
204
- // Publish all discovered modules in the current directory
205
- await publisher.publish(process.cwd());
206
-
207
- // Or just discover modules without publishing
208
- const modules = await publisher.getModuleSubDirs('./my-monorepo');
209
- console.log(modules);
210
- // => { ts_core: { name: '@myorg/core', order: 1, ... }, ts_utils: { ... } }
145
+ const packages = await publisher.prepare(
146
+ process.cwd(),
147
+ '/absolute/existing-parent/new-package-directory',
148
+ );
149
+ // Each result contains folder, folders, name, version, order, manifest,
150
+ // include, licenseFile, registries and its prepared directory.
211
151
  ```
212
152
 
213
- ## ๐Ÿ”ง How It Works
214
-
215
- ```
216
- โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
217
- โ”‚ tspublish pipeline โ”‚
218
- โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
219
- โ”‚ โ”‚
220
- โ”‚ 1. ๐Ÿ” Discovery โ”‚
221
- โ”‚ Scan for ts* directories containing tspublish.json โ”‚
222
- โ”‚ โ”‚
223
- โ”‚ 2. ๐Ÿ“‹ Preparation โ”‚
224
- โ”‚ Create dist_publish_* with generated package.json, โ”‚
225
- โ”‚ tsconfig.json, source files, readme, and license โ”‚
226
- โ”‚ โ”‚
227
- โ”‚ 3. ๐Ÿ”จ Build โ”‚
228
- โ”‚ Run `tsbuild tsfolders` in the publish directory โ”‚
229
- โ”‚ โ”‚
230
- โ”‚ 4. โœ… Validation โ”‚
231
- โ”‚ Check npm registry โ€” abort if version already exists โ”‚
232
- โ”‚ โ”‚
233
- โ”‚ 5. ๐Ÿš€ Publish โ”‚
234
- โ”‚ pnpm publish to each configured registry โ”‚
235
- โ”‚ โ”‚
236
- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
237
- ```
153
+ `plan()` reads manifests and license availability without writes, builds or network
154
+ calls. It rejects duplicate package names, dependency cycles, unknown external
155
+ dependency versions, local dependency links and invalid package paths.
238
156
 
239
- For each discovered module, tspublish:
157
+ `prepare()` plans again and materializes the already-built packages. The output
158
+ directory must not exist; its parent must already exist at a canonical path. Output
159
+ cannot be nested inside an input folder. Preparation removes its own incomplete
160
+ output on failure and never overwrites a prior result. The caller owns successful
161
+ prepared directories and their cleanup.
240
162
 
241
- 1. **Discovers** all directories starting with `ts` that contain a `tspublish.json`
242
- 2. **Resolves dependencies** from the monorepo's `package.json`, using the monorepo version for packages not found in `dependencies`
243
- 3. **Creates an isolated publish directory** (`dist_publish_<folder>`) with a generated `package.json`, `tsconfig.json`, source code copy, readme, and license
244
- 4. **Builds** the package using `pnpm run build` (which calls `tsbuild tsfolders`)
245
- 5. **Validates** against the target registry โ€” if the name+version already exists, it exits with an error
246
- 6. **Publishes** to each configured registry via `pnpm publish`
163
+ Neither method installs dependencies, fetches templates, contacts registries,
164
+ publishes packages or terminates the process.
247
165
 
248
- ## ๐Ÿ“š API Reference
249
-
250
- ### TsPublish
251
-
252
- The main class that orchestrates the entire publishing pipeline.
253
-
254
- ```typescript
255
- class TsPublish {
256
- /** Publish all discovered modules in a monorepo directory */
257
- async publish(monorepoDirPath: string): Promise<void>;
166
+ The CLI exposes the same operations:
258
167
 
259
- /** Discover and return all publishable modules with their tspublish.json configs */
260
- async getModuleSubDirs(dirPath: string): Promise<Record<string, ITsPublishJson>>;
261
- }
168
+ ```sh
169
+ pnpm exec tspublish plan
170
+ pnpm exec tspublish prepare /absolute/existing-parent/new-package-directory
262
171
  ```
263
172
 
264
- ### ITsPublishJson
265
-
266
- The configuration interface for each `tspublish.json` file:
173
+ A release coordinator should validate every module's destination intent, prepare
174
+ all modules, pack every package, record the complete immutable artifact set, and
175
+ then publish in the returned dependency order. Publication recovery must reuse
176
+ those retained tarballs rather than rebuilding or preparing again.
267
177
 
268
- ```typescript
269
- interface ITsPublishJson {
270
- name: string; // Published package name
271
- order: number; // Build sequence (lower = earlier)
272
- dependencies: string[]; // Dependencies to include from monorepo
273
- registries: string[]; // Target registries ("url:accessLevel", "useBase", or "extendBase")
274
- bin: string[]; // CLI binary names
275
- }
276
- ```
178
+ ## Existing standalone publisher
277
179
 
278
- ### GiteaAssets
180
+ The existing `TsPublish.publish(repository)` API and no-argument CLI remain
181
+ available. They build and publish modules sequentially and do not provide
182
+ transaction recovery. Use the owning repository's approved release workflow.
279
183
 
280
- Utility class for fetching files from Gitea repositories (used internally for CLI template retrieval):
184
+ The standalone publisher supports explicit `registry.npmjs.org:public` entries,
185
+ `useBase`, and `extendBase` with optional `-URL` exclusions. Base settings come
186
+ from `@git.zone/cli.release.targets.npm` in `.smartconfig.json`; the previous
187
+ `release.registries` and `release.accessLevel` fields remain readable.
188
+ An empty registry list means build without publishing.
281
189
 
282
- ```typescript
283
- class GiteaAssets {
284
- constructor(options: { giteaBaseUrl: string; token?: string });
190
+ Standalone generated builds use the repository's declared TsBuild version rather
191
+ than looking up a mutable latest version. The `GiteaAssets` helper remains
192
+ available from its source module for existing integrations.
285
193
 
286
- /** Fetch files from a Gitea repository directory */
287
- async getFiles(owner: string, repo: string, directory: string, branch?: string): Promise<IRepoFile[]>;
194
+ ## Development
288
195
 
289
- /** Get the standard cli.js entry file template */
290
- async getBinCliEntryFile(): Promise<IRepoFile>;
291
- }
196
+ ```sh
197
+ pnpm build
198
+ pnpm test
199
+ pnpm exec tsbuild check 'test/**/*'
292
200
  ```
293
201
 
294
- ## ๐Ÿ› Troubleshooting
295
-
296
- | Problem | Solution |
297
- |---------|----------|
298
- | **"Package X already exists with version Y"** | Bump the version in your monorepo's `package.json` |
299
- | **No publish modules found** | Ensure directories start with `ts` and contain a valid `tspublish.json` |
300
- | **Build failures** | Check TypeScript errors โ€” tspublish runs `tsbuild tsfolders` in the generated directory |
301
- | **useBase/extendBase error** | Ensure `.smartconfig.json` has registries at `@git.zone/cli.release.registries` |
302
- | **Missing dependency versions** | Add the dependency to your monorepo's `package.json` `dependencies` field |
202
+ Packaging tests use disposable directories and do not publish anything.
303
203
 
304
204
  ## License and Legal Information
305
205
 
306
- This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
206
+ This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license.md](./license.md) file.
307
207
 
308
208
  **Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
309
209
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@git.zone/tspublish',
6
- version: '1.11.6',
6
+ version: '1.12.1',
7
7
  description: 'A tool to publish multiple, concise, and small packages from monorepos, specifically for TypeScript projects within a git environment.'
8
8
  }
@@ -0,0 +1,135 @@
1
+ import * as plugins from './plugins.js';
2
+ import { assertPackagePath } from './functions.packageplan.js';
3
+ import type { ITsPublishModulePlan, ITsPublishPlan, ITsPublishPreparedModule, TTsPublishExport } from './interfaces/index.js';
4
+
5
+ /** Prepares already built modules. It never builds, installs, contacts a registry or publishes. */
6
+ export class PackagePreparer {
7
+ constructor(private readonly root: string) {}
8
+
9
+ public async prepare(plan: ITsPublishPlan, outputDirectory: string): Promise<ITsPublishPreparedModule[]> {
10
+ const root = await plugins.fs.realpath(this.root);
11
+ const output = plugins.path.resolve(outputDirectory);
12
+ if (output === root || root.startsWith(`${output}${plugins.path.sep}`)) {
13
+ throw new Error('Package preparation requires a separate output directory.');
14
+ }
15
+ for (const module of plan.modules) {
16
+ for (const input of [...module.folders.flatMap((folder) => [folder, `dist_${folder}`]), ...module.include]) {
17
+ const inputPath = plugins.path.join(root, input);
18
+ if (output === inputPath || output.startsWith(`${inputPath}${plugins.path.sep}`)) {
19
+ throw new Error('Package output cannot be nested inside a package input.');
20
+ }
21
+ }
22
+ }
23
+ // The caller owns the parent; refusing existing output prevents overwriting artifacts.
24
+ const parent = await plugins.fs.realpath(plugins.path.dirname(output));
25
+ if (parent !== plugins.path.dirname(output)) throw new Error('Package output parent must be canonical.');
26
+ await plugins.fs.mkdir(output, { mode: 0o700 });
27
+ const prepared: ITsPublishPreparedModule[] = [];
28
+ try {
29
+ for (const module of plan.modules) {
30
+ const directory = plugins.path.join(output, module.folder);
31
+ await plugins.fs.mkdir(directory, { mode: 0o700 });
32
+ for (const folder of module.folders) {
33
+ await this.copyTree(root, folder, directory, folder, module);
34
+ await this.copyTree(root, `dist_${folder}`, directory, `dist_${folder}`, module);
35
+ }
36
+ for (const path of module.include) await this.copyTree(root, path, directory, path, module);
37
+ await this.copyTree(root, `${module.folder}/readme.md`, directory, 'readme.md', module);
38
+ await this.copyTree(root, module.licenseFile, directory, module.licenseFile, module);
39
+ if (module.manifest.bin) {
40
+ await plugins.fs.writeFile(plugins.path.join(directory, 'cli.js'),
41
+ `#!/usr/bin/env node\nprocess.env.CLI_CALL = 'true';\nconst cli = await import(${JSON.stringify(`./dist_${module.folder}/index.js`)});\nawait cli.runCli();\n`,
42
+ { mode: 0o755, flag: 'wx' });
43
+ }
44
+ for (const value of Object.values(module.manifest.exports)) await this.assertExport(directory, value);
45
+ await plugins.fs.writeFile(plugins.path.join(directory, 'package.json'), `${JSON.stringify(module.manifest, null, 2)}\n`, { flag: 'wx' });
46
+ prepared.push({ ...module, directory });
47
+ }
48
+ return prepared;
49
+ } catch (error) {
50
+ await plugins.fs.rm(output, { recursive: true, force: true });
51
+ throw error;
52
+ }
53
+ }
54
+
55
+ private async assertExport(directory: string, value: TTsPublishExport): Promise<void> {
56
+ if (value === null) return;
57
+ if (typeof value === 'string') {
58
+ const target = await plugins.fs.lstat(plugins.path.join(directory, value));
59
+ if (!target.isFile() || target.isSymbolicLink()) throw new Error(`Missing regular export file: ${value}`);
60
+ return;
61
+ }
62
+ for (const child of Object.values(value)) await this.assertExport(directory, child);
63
+ }
64
+
65
+ private async copyTree(root: string, source: string, output: string, destination: string, module: ITsPublishModulePlan): Promise<void> {
66
+ assertPackagePath(source);
67
+ assertPackagePath(destination);
68
+ if (source.split('/').some((part) => ['node_modules', '.git', '.npmrc', '.env'].includes(part) || part.startsWith('.env.'))) {
69
+ throw new Error('Package contents cannot include credentials or local runtime directories.');
70
+ }
71
+ // Check every ancestor, including explicitly included nested paths.
72
+ const segments = source.split('/');
73
+ for (let index = 1; index <= segments.length; index++) {
74
+ const stat = await plugins.fs.lstat(plugins.path.join(root, ...segments.slice(0, index)));
75
+ if (stat.isSymbolicLink()) throw new Error(`Package input cannot be a symlink: ${source}`);
76
+ }
77
+ const input = plugins.path.join(root, source);
78
+ const target = plugins.path.join(output, destination);
79
+ const stat = await plugins.fs.lstat(input);
80
+ if (stat.isDirectory()) {
81
+ await plugins.fs.mkdir(target, { recursive: true });
82
+ const entries = (await plugins.fs.readdir(input)).sort();
83
+ for (const entry of entries) {
84
+ if (entry === 'tspublish.json') continue;
85
+ await this.copyTree(root, `${source}/${entry}`, output, `${destination}/${entry}`, module);
86
+ }
87
+ } else if (stat.isFile()) {
88
+ if (/\.(?:[cm]?js|[cm]?ts|tsx|jsx)$/.test(source)) {
89
+ this.assertImports(source, await plugins.fs.readFile(input, 'utf8'), module);
90
+ }
91
+ await plugins.fs.mkdir(plugins.path.dirname(target), { recursive: true });
92
+ await plugins.fs.copyFile(input, target, 1); // COPYFILE_EXCL: generated metadata cannot be overwritten.
93
+ } else {
94
+ throw new Error(`Package input must be a regular file or directory: ${source}`);
95
+ }
96
+ }
97
+
98
+ private assertImports(file: string, content: string, module: ITsPublishModulePlan): void {
99
+ const ts = plugins.typescript;
100
+ const source = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
101
+ const allowed = new Set([
102
+ module.name,
103
+ ...Object.keys(module.manifest.dependencies),
104
+ ...Object.keys(module.manifest.optionalDependencies ?? {}),
105
+ ...Object.keys(module.manifest.peerDependencies ?? {}),
106
+ ]);
107
+ const check = (specifier: string): void => {
108
+ if (plugins.isBuiltin(specifier)) return;
109
+ if (specifier.startsWith('.')) {
110
+ const target = plugins.path.posix.normalize(plugins.path.posix.join(plugins.path.posix.dirname(file), specifier));
111
+ if (!module.folders.some((folder) => target.startsWith(`${folder}/`) || target.startsWith(`dist_${folder}/`))
112
+ && !module.include.some((path) => target === path || target.startsWith(`${path}/`))) {
113
+ throw new Error(`${module.name}: cross-package relative import ${specifier} in ${file}; use the published package name.`);
114
+ }
115
+ return;
116
+ }
117
+ const name = specifier.startsWith('@') ? specifier.split('/').slice(0, 2).join('/') : specifier.split('/')[0];
118
+ if (!allowed.has(name)) throw new Error(`${module.name}: undeclared dependency ${specifier} in ${file}.`);
119
+ };
120
+ const visit = (node: plugins.typescript.Node): void => {
121
+ if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) {
122
+ check(node.moduleSpecifier.text);
123
+ } else if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument) && ts.isStringLiteralLike(node.argument.literal)) {
124
+ check(node.argument.literal.text);
125
+ } else if (ts.isExternalModuleReference(node) && node.expression && ts.isStringLiteralLike(node.expression)) {
126
+ check(node.expression.text);
127
+ } else if (ts.isCallExpression(node) && node.arguments.length && ts.isStringLiteralLike(node.arguments[0])
128
+ && (node.expression.kind === ts.SyntaxKind.ImportKeyword || (ts.isIdentifier(node.expression) && node.expression.text === 'require'))) {
129
+ check(node.arguments[0].text);
130
+ }
131
+ ts.forEachChild(node, visit);
132
+ };
133
+ visit(source);
134
+ }
135
+ }