@git.zone/tspublish 1.11.7 โ 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/.smartconfig.json +23 -7
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.packagepreparer.d.ts +10 -0
- package/dist_ts/classes.packagepreparer.js +146 -0
- package/dist_ts/classes.publishmodule.d.ts +2 -1
- package/dist_ts/classes.publishmodule.js +30 -58
- package/dist_ts/classes.tspublish.d.ts +5 -3
- package/dist_ts/classes.tspublish.js +15 -2
- package/dist_ts/functions.packageplan.d.ts +4 -0
- package/dist_ts/functions.packageplan.js +190 -0
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +14 -2
- package/dist_ts/interfaces/tspublish.d.ts +56 -0
- package/dist_ts/plugins.d.ts +5 -2
- package/dist_ts/plugins.js +6 -3
- package/package.json +15 -14
- package/readme.md +146 -246
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.packagepreparer.ts +135 -0
- package/ts/classes.publishmodule.ts +27 -61
- package/ts/classes.tspublish.ts +16 -1
- package/ts/functions.packageplan.ts +182 -0
- package/ts/index.ts +11 -1
- package/ts/interfaces/tspublish.ts +55 -0
- package/ts/plugins.ts +5 -2
- /package/{license โ license.md} +0 -0
package/readme.md
CHANGED
|
@@ -1,309 +1,209 @@
|
|
|
1
|
-
# @git.zone/tspublish
|
|
1
|
+
# @git.zone/tspublish
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
pnpm exec tspublish
|
|
14
|
+
```sh
|
|
15
|
+
pnpm install --save-dev @git.zone/tspublish @git.zone/tsbuild
|
|
97
16
|
```
|
|
98
17
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
## ๐ฏ Advanced Usage
|
|
102
|
-
|
|
103
|
-
### Registry Configuration
|
|
18
|
+
## Repository structure
|
|
104
19
|
|
|
105
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
47
|
+
## Module configuration
|
|
125
48
|
|
|
126
49
|
```json
|
|
127
50
|
{
|
|
128
|
-
"
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
113
|
+
## Imports and local development
|
|
179
114
|
|
|
180
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
242
|
-
|
|
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
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
168
|
+
```sh
|
|
169
|
+
pnpm exec tspublish plan
|
|
170
|
+
pnpm exec tspublish prepare /absolute/existing-parent/new-package-directory
|
|
262
171
|
```
|
|
263
172
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
287
|
-
async getFiles(owner: string, repo: string, directory: string, branch?: string): Promise<IRepoFile[]>;
|
|
194
|
+
## Development
|
|
288
195
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
196
|
+
```sh
|
|
197
|
+
pnpm build
|
|
198
|
+
pnpm test
|
|
199
|
+
pnpm exec tsbuild check 'test/**/*'
|
|
292
200
|
```
|
|
293
201
|
|
|
294
|
-
|
|
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
|
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -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
|
+
}
|