@git.zone/tsbundle 2.11.4 → 2.13.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/interfaces/index.d.ts +3 -0
- package/dist_ts/mod_custom/index.d.ts +10 -3
- package/dist_ts/mod_custom/index.js +278 -86
- package/dist_ts/mod_esbuild/index.child.d.ts +4 -8
- package/dist_ts/mod_esbuild/index.child.js +54 -44
- package/dist_ts/mod_esbuild/plugins.d.ts +5 -2
- package/dist_ts/mod_esbuild/plugins.js +5 -3
- package/dist_ts/mod_esbuild/workerplugin.d.ts +12 -0
- package/dist_ts/mod_esbuild/workerplugin.js +254 -0
- package/dist_ts/mod_output/artifactpublisher.d.ts +43 -0
- package/dist_ts/mod_output/artifactpublisher.js +925 -0
- package/dist_ts/mod_output/index.d.ts +1 -0
- package/dist_ts/mod_output/index.js +11 -3
- package/dist_ts/mod_presets/index.js +2 -1
- package/dist_ts/mod_rolldown/index.child.d.ts +4 -2
- package/dist_ts/mod_rolldown/index.child.js +48 -36
- package/dist_ts/mod_rspack/index.child.d.ts +4 -2
- package/dist_ts/mod_rspack/index.child.js +76 -115
- package/dist_ts/plugins.d.ts +3 -1
- package/dist_ts/plugins.js +4 -2
- package/dist_ts/tsbundle.class.tsbundle.js +98 -16
- package/package.json +7 -5
- package/readme.hints.md +20 -4
- package/readme.md +29 -6
- package/third-party-notices.md +29 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/interfaces/index.ts +3 -0
- package/ts/mod_custom/index.ts +348 -96
- package/ts/mod_esbuild/index.child.ts +81 -47
- package/ts/mod_esbuild/plugins.ts +9 -2
- package/ts/mod_esbuild/workerplugin.ts +328 -0
- package/ts/mod_output/artifactpublisher.ts +1185 -0
- package/ts/mod_output/index.ts +10 -2
- package/ts/mod_presets/index.ts +1 -0
- package/ts/mod_rolldown/index.child.ts +66 -42
- package/ts/mod_rspack/index.child.ts +98 -127
- package/ts/plugins.ts +3 -1
- package/ts/tsbundle.class.tsbundle.ts +102 -14
package/ts/mod_custom/index.ts
CHANGED
|
@@ -2,20 +2,36 @@ import * as plugins from './plugins.js';
|
|
|
2
2
|
import * as paths from '../paths.js';
|
|
3
3
|
import * as interfaces from '../interfaces/index.js';
|
|
4
4
|
import { TsBundle } from '../tsbundle.class.tsbundle.js';
|
|
5
|
-
import { HtmlHandler } from '../mod_html/index.js';
|
|
6
5
|
import { Base64TsOutput } from '../mod_output/index.js';
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import {
|
|
7
|
+
chunkManifestFile,
|
|
8
|
+
createProcessOwnership,
|
|
9
|
+
getChunkNamespace,
|
|
10
|
+
isProcessOwnershipActive,
|
|
11
|
+
publishGeneratedArtifacts,
|
|
12
|
+
startOwnershipHeartbeat,
|
|
13
|
+
withArtifactPublicationLocks,
|
|
14
|
+
type IAdditionalArtifact,
|
|
15
|
+
} from '../mod_output/artifactpublisher.js';
|
|
16
|
+
|
|
17
|
+
const legacyTempDir = '.nogit/tsbundle-temp';
|
|
18
|
+
const localTempPrefix = 'tsbundle-temp-';
|
|
19
|
+
const cleanupTempPrefix = '.tsbundle-temp-cleanup-';
|
|
9
20
|
const toolCacheMarkerFile = '.gitzone-tool-cache.json';
|
|
10
21
|
const toolCacheOwner = '@git.zone/tsbundle';
|
|
11
22
|
const tempCacheKind = 'bundle-temp-workspace';
|
|
12
23
|
const staleTempThresholdMs = 24 * 60 * 60 * 1000;
|
|
24
|
+
const minimumTempLeaseTimeoutMs = 2 * 60 * 1000;
|
|
13
25
|
|
|
14
26
|
interface IToolCacheMarker {
|
|
15
27
|
owner: string;
|
|
16
28
|
kind: string;
|
|
17
29
|
safeToPrune: boolean;
|
|
18
30
|
createdAt: string;
|
|
31
|
+
invocationId?: string;
|
|
32
|
+
leaseManaged?: boolean;
|
|
33
|
+
processId?: number;
|
|
34
|
+
processIdentity?: string;
|
|
19
35
|
schemaVersion: 1;
|
|
20
36
|
}
|
|
21
37
|
|
|
@@ -23,6 +39,7 @@ interface ITempWorkspace {
|
|
|
23
39
|
tempDir: string;
|
|
24
40
|
canCleanupWorkspace: boolean;
|
|
25
41
|
cleanupOnFailure: boolean;
|
|
42
|
+
marker?: IToolCacheMarker;
|
|
26
43
|
}
|
|
27
44
|
|
|
28
45
|
export class CustomBundleHandler {
|
|
@@ -30,6 +47,7 @@ export class CustomBundleHandler {
|
|
|
30
47
|
private config!: interfaces.ITsbundleConfig;
|
|
31
48
|
private cliOptions: interfaces.ICustomBundleCliOptions;
|
|
32
49
|
private didRunStartupCleanup = false;
|
|
50
|
+
private readonly invocationId = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
33
51
|
|
|
34
52
|
constructor(cwd: string = paths.cwd, cliOptions: interfaces.ICustomBundleCliOptions = {}) {
|
|
35
53
|
this.cwd = cwd;
|
|
@@ -78,16 +96,17 @@ export class CustomBundleHandler {
|
|
|
78
96
|
const tempWorkspace = await this.prepareTempWorkspace();
|
|
79
97
|
const tempDir = tempWorkspace.tempDir;
|
|
80
98
|
const tempBundlePath = plugins.path.join(tempDir, 'bundle.js');
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
await plugins.fsPromises.rm(tempBundlePath, { force: true });
|
|
85
|
-
await plugins.fsPromises.rm(`${tempBundlePath}.map`, { force: true });
|
|
99
|
+
const stopTempHeartbeat = tempWorkspace.marker
|
|
100
|
+
? startOwnershipHeartbeat(plugins.path.join(tempDir, toolCacheMarkerFile))
|
|
101
|
+
: undefined;
|
|
86
102
|
|
|
87
103
|
// Build the bundle to temp location
|
|
88
104
|
const tsbundle = new TsBundle();
|
|
89
105
|
let completedSuccessfully = false;
|
|
90
106
|
try {
|
|
107
|
+
// A marked workspace can survive a failed or explicitly retained build.
|
|
108
|
+
// Clear prior generated artifacts so stale chunks cannot leak into output.
|
|
109
|
+
await this.clearTempBuildArtifacts(tempDir);
|
|
91
110
|
await tsbundle.build(
|
|
92
111
|
this.cwd,
|
|
93
112
|
bundleConfig.from,
|
|
@@ -96,6 +115,8 @@ export class CustomBundleHandler {
|
|
|
96
115
|
bundler,
|
|
97
116
|
production: bundleConfig.production || false,
|
|
98
117
|
sourcemap: bundleConfig.sourcemap !== false,
|
|
118
|
+
banner: bundleConfig.banner,
|
|
119
|
+
chunkOwner: plugins.path.basename(bundleConfig.to),
|
|
99
120
|
}
|
|
100
121
|
);
|
|
101
122
|
|
|
@@ -106,18 +127,19 @@ export class CustomBundleHandler {
|
|
|
106
127
|
}
|
|
107
128
|
|
|
108
129
|
completedSuccessfully = true;
|
|
109
|
-
} catch (error:
|
|
130
|
+
} catch (error: unknown) {
|
|
110
131
|
console.error(`\n\x1b[31m❌ Bundle failed: ${bundleConfig.from} -> ${bundleConfig.to}\x1b[0m`);
|
|
111
132
|
// Don't re-print error details - they were already shown by the child process.
|
|
112
133
|
// Propagate the failure so package builds cannot succeed without their bundle.
|
|
113
134
|
throw error;
|
|
114
135
|
} finally {
|
|
136
|
+
stopTempHeartbeat?.();
|
|
115
137
|
if (completedSuccessfully && tempWorkspace.canCleanupWorkspace && !this.shouldKeepTemp(bundleConfig)) {
|
|
116
|
-
await this.cleanupTempWorkspace('successful bundle', tempDir);
|
|
138
|
+
await this.cleanupTempWorkspace('successful bundle', tempDir, tempWorkspace.marker);
|
|
117
139
|
} else if (completedSuccessfully) {
|
|
118
140
|
console.log(`Keeping tsbundle temp workspace: ${tempDir}`);
|
|
119
141
|
} else if (tempWorkspace.cleanupOnFailure && !this.shouldKeepTemp(bundleConfig)) {
|
|
120
|
-
await this.cleanupTempWorkspace('failed bundle', tempDir);
|
|
142
|
+
await this.cleanupTempWorkspace('failed bundle', tempDir, tempWorkspace.marker);
|
|
121
143
|
}
|
|
122
144
|
}
|
|
123
145
|
}
|
|
@@ -132,42 +154,77 @@ export class CustomBundleHandler {
|
|
|
132
154
|
return false;
|
|
133
155
|
}
|
|
134
156
|
|
|
135
|
-
|
|
136
|
-
|
|
157
|
+
let didCleanup = await this.cleanupStaleMarkedTempWorkspaces(
|
|
158
|
+
this.getProjectTempRoot(),
|
|
159
|
+
thresholdMsArg,
|
|
160
|
+
);
|
|
161
|
+
didCleanup = await this.cleanupStaleMarkedTempWorkspaces(
|
|
162
|
+
plugins.os.tmpdir(),
|
|
163
|
+
thresholdMsArg,
|
|
164
|
+
) || didCleanup;
|
|
165
|
+
|
|
166
|
+
const legacyTempPath = this.getTempDir();
|
|
167
|
+
const marker = await this.readTempMarker(legacyTempPath);
|
|
137
168
|
if (!marker) {
|
|
138
|
-
return
|
|
169
|
+
return didCleanup;
|
|
139
170
|
}
|
|
140
171
|
|
|
141
172
|
const createdAt = Date.parse(marker.createdAt);
|
|
142
|
-
if (Number.isFinite(createdAt)
|
|
143
|
-
return
|
|
173
|
+
if (!Number.isFinite(createdAt) || Date.now() - createdAt < thresholdMsArg) {
|
|
174
|
+
return didCleanup;
|
|
175
|
+
}
|
|
176
|
+
if (this.isTempMarkerActive(marker, legacyTempPath, thresholdMsArg)) {
|
|
177
|
+
return didCleanup;
|
|
144
178
|
}
|
|
145
179
|
|
|
146
|
-
await this.cleanupTempWorkspace(
|
|
147
|
-
|
|
180
|
+
didCleanup = await this.cleanupTempWorkspace(
|
|
181
|
+
'stale marked temp workspace',
|
|
182
|
+
legacyTempPath,
|
|
183
|
+
marker,
|
|
184
|
+
) || didCleanup;
|
|
185
|
+
return didCleanup;
|
|
148
186
|
}
|
|
149
187
|
|
|
150
188
|
private getTempDir(): string {
|
|
151
|
-
return plugins.path.join(this.cwd,
|
|
189
|
+
return plugins.path.join(this.cwd, legacyTempDir);
|
|
152
190
|
}
|
|
153
191
|
|
|
154
|
-
private
|
|
155
|
-
|
|
156
|
-
const actual = plugins.path.resolve(tempDirArg);
|
|
157
|
-
if (actual !== expected) {
|
|
158
|
-
throw new Error(`Refusing to clean unexpected tsbundle temp path: ${tempDirArg}`);
|
|
159
|
-
}
|
|
192
|
+
private getProjectTempRoot(): string {
|
|
193
|
+
return plugins.path.join(this.cwd, '.nogit');
|
|
160
194
|
}
|
|
161
195
|
|
|
162
196
|
private assertManagedTempDir(tempDirArg: string): void {
|
|
163
|
-
const expected = plugins.path.resolve(this.cwd,
|
|
197
|
+
const expected = plugins.path.resolve(this.cwd, legacyTempDir);
|
|
164
198
|
const actual = plugins.path.resolve(tempDirArg);
|
|
199
|
+
const projectTempRoot = plugins.path.resolve(this.getProjectTempRoot());
|
|
165
200
|
const osTempRoot = plugins.path.resolve(plugins.os.tmpdir());
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
201
|
+
const isExpectedTemp = actual === expected;
|
|
202
|
+
const basename = plugins.path.basename(actual);
|
|
203
|
+
const hasManagedName = basename.startsWith(localTempPrefix)
|
|
204
|
+
|| basename.startsWith(cleanupTempPrefix);
|
|
205
|
+
const isProjectTemp = plugins.path.dirname(actual) === projectTempRoot && hasManagedName;
|
|
206
|
+
const isOsTemp = plugins.path.dirname(actual) === osTempRoot && hasManagedName;
|
|
207
|
+
if (!isExpectedTemp && !isProjectTemp && !isOsTemp) {
|
|
169
208
|
throw new Error(`Refusing to clean unexpected tsbundle temp path: ${tempDirArg}`);
|
|
170
209
|
}
|
|
210
|
+
|
|
211
|
+
const stat = plugins.fsSync.lstatSync(actual);
|
|
212
|
+
if (stat.isSymbolicLink()) {
|
|
213
|
+
throw new Error(`Refusing to use symbolic-link temp path: ${tempDirArg}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const physicalActual = plugins.fsSync.realpathSync(actual);
|
|
217
|
+
const physicalParent = plugins.fsSync.realpathSync(plugins.path.dirname(actual));
|
|
218
|
+
const physicalCwd = plugins.fsSync.realpathSync(this.cwd);
|
|
219
|
+
const physicalExpected = plugins.path.join(physicalCwd, legacyTempDir);
|
|
220
|
+
const physicalProjectTempRoot = plugins.path.join(physicalCwd, '.nogit');
|
|
221
|
+
const physicalOsTempRoot = plugins.fsSync.realpathSync(osTempRoot);
|
|
222
|
+
const isPhysicalExpected = isExpectedTemp && physicalActual === physicalExpected;
|
|
223
|
+
const isPhysicalProjectTemp = isProjectTemp && physicalParent === physicalProjectTempRoot;
|
|
224
|
+
const isPhysicalOsTemp = isOsTemp && physicalParent === physicalOsTempRoot;
|
|
225
|
+
if (!isPhysicalExpected && !isPhysicalProjectTemp && !isPhysicalOsTemp) {
|
|
226
|
+
throw new Error(`Refusing to use temp path outside its physical owner: ${tempDirArg}`);
|
|
227
|
+
}
|
|
171
228
|
}
|
|
172
229
|
|
|
173
230
|
private isTruthy(valueArg: unknown): boolean {
|
|
@@ -181,6 +238,43 @@ export class CustomBundleHandler {
|
|
|
181
238
|
return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'y';
|
|
182
239
|
}
|
|
183
240
|
|
|
241
|
+
private hasErrorCode(errorArg: unknown): errorArg is NodeJS.ErrnoException {
|
|
242
|
+
return typeof errorArg === 'object' && errorArg !== null && 'code' in errorArg;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private isTempMarkerActive(
|
|
246
|
+
markerArg: IToolCacheMarker,
|
|
247
|
+
tempDirArg: string,
|
|
248
|
+
leaseTimeoutMsArg: number,
|
|
249
|
+
): boolean {
|
|
250
|
+
if (!markerArg.processId) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
return isProcessOwnershipActive(
|
|
254
|
+
{
|
|
255
|
+
leaseManaged: markerArg.leaseManaged,
|
|
256
|
+
processId: markerArg.processId,
|
|
257
|
+
processIdentity: markerArg.processIdentity,
|
|
258
|
+
},
|
|
259
|
+
plugins.path.join(tempDirArg, toolCacheMarkerFile),
|
|
260
|
+
Math.max(leaseTimeoutMsArg, minimumTempLeaseTimeoutMs),
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private markersMatch(
|
|
265
|
+
firstMarkerArg: IToolCacheMarker,
|
|
266
|
+
secondMarkerArg: IToolCacheMarker,
|
|
267
|
+
): boolean {
|
|
268
|
+
return firstMarkerArg.owner === secondMarkerArg.owner
|
|
269
|
+
&& firstMarkerArg.kind === secondMarkerArg.kind
|
|
270
|
+
&& firstMarkerArg.createdAt === secondMarkerArg.createdAt
|
|
271
|
+
&& firstMarkerArg.invocationId === secondMarkerArg.invocationId
|
|
272
|
+
&& firstMarkerArg.processId === secondMarkerArg.processId
|
|
273
|
+
&& firstMarkerArg.processIdentity === secondMarkerArg.processIdentity
|
|
274
|
+
&& firstMarkerArg.leaseManaged === secondMarkerArg.leaseManaged
|
|
275
|
+
&& firstMarkerArg.schemaVersion === secondMarkerArg.schemaVersion;
|
|
276
|
+
}
|
|
277
|
+
|
|
184
278
|
private shouldKeepTemp(bundleConfigArg?: interfaces.IBundleConfig): boolean {
|
|
185
279
|
return this.isTruthy(process.env.TSBUNDLE_KEEP_TEMP)
|
|
186
280
|
|| this.cliOptions.keepTemp === true
|
|
@@ -190,73 +284,184 @@ export class CustomBundleHandler {
|
|
|
190
284
|
}
|
|
191
285
|
|
|
192
286
|
private async prepareTempWorkspace(): Promise<ITempWorkspace> {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const marker = await this.readTempMarker(tempDirArg);
|
|
196
|
-
if (marker) {
|
|
197
|
-
return { tempDir: tempDirArg, canCleanupWorkspace: true, cleanupOnFailure: false };
|
|
198
|
-
}
|
|
287
|
+
return await this.createIsolatedTempWorkspace();
|
|
288
|
+
}
|
|
199
289
|
|
|
200
|
-
|
|
290
|
+
private async createIsolatedTempWorkspace(): Promise<ITempWorkspace> {
|
|
291
|
+
const projectTempRoot = this.getProjectTempRoot();
|
|
292
|
+
await plugins.fsPromises.mkdir(projectTempRoot, { recursive: true });
|
|
293
|
+
const rootStat = plugins.fsSync.lstatSync(projectTempRoot);
|
|
294
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
295
|
+
throw new Error(`Refusing to use unsafe project temp root: ${projectTempRoot}`);
|
|
296
|
+
}
|
|
297
|
+
const expectedPhysicalRoot = plugins.path.join(plugins.fsSync.realpathSync(this.cwd), '.nogit');
|
|
298
|
+
if (plugins.fsSync.realpathSync(projectTempRoot) !== expectedPhysicalRoot) {
|
|
299
|
+
throw new Error(`Refusing to use project temp root outside the project: ${projectTempRoot}`);
|
|
300
|
+
}
|
|
301
|
+
const tempDir = await plugins.fsPromises.mkdtemp(
|
|
302
|
+
plugins.path.join(projectTempRoot, localTempPrefix),
|
|
303
|
+
);
|
|
201
304
|
try {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
305
|
+
const marker = await this.writeTempMarker(tempDir);
|
|
306
|
+
return {
|
|
307
|
+
tempDir,
|
|
308
|
+
canCleanupWorkspace: true,
|
|
309
|
+
cleanupOnFailure: true,
|
|
310
|
+
marker,
|
|
311
|
+
};
|
|
312
|
+
} catch (error: unknown) {
|
|
313
|
+
await plugins.fsPromises.rm(tempDir, { recursive: true, force: true });
|
|
314
|
+
throw error;
|
|
205
315
|
}
|
|
316
|
+
}
|
|
206
317
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
318
|
+
private async clearTempBuildArtifacts(tempDirArg: string): Promise<void> {
|
|
319
|
+
this.assertManagedTempDir(tempDirArg);
|
|
320
|
+
const entries = await plugins.fsPromises.readdir(tempDirArg, { withFileTypes: true });
|
|
321
|
+
for (const entry of entries) {
|
|
322
|
+
if (entry.name === toolCacheMarkerFile) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
await plugins.fsPromises.rm(plugins.path.join(tempDirArg, entry.name), {
|
|
326
|
+
recursive: true,
|
|
327
|
+
force: true,
|
|
328
|
+
});
|
|
212
329
|
}
|
|
213
|
-
|
|
214
|
-
await this.writeTempMarker(tempDirArg);
|
|
215
|
-
return { tempDir: tempDirArg, canCleanupWorkspace: true, cleanupOnFailure: false };
|
|
216
330
|
}
|
|
217
331
|
|
|
218
|
-
private async
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
332
|
+
private async listBundleArtifacts(tempDirArg: string): Promise<string[]> {
|
|
333
|
+
const result: string[] = [];
|
|
334
|
+
const collect = async (directoryArg: string): Promise<void> => {
|
|
335
|
+
const entries = await plugins.fsPromises.readdir(directoryArg, { withFileTypes: true });
|
|
336
|
+
for (const entry of entries) {
|
|
337
|
+
if (entry.name === toolCacheMarkerFile || entry.name === chunkManifestFile) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const entryPath = plugins.path.join(directoryArg, entry.name);
|
|
341
|
+
if (entry.isDirectory()) {
|
|
342
|
+
await collect(entryPath);
|
|
343
|
+
} else if (entry.isFile()) {
|
|
344
|
+
result.push(entryPath);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
await collect(tempDirArg);
|
|
349
|
+
return result.sort((a, b) => a.localeCompare(b));
|
|
222
350
|
}
|
|
223
351
|
|
|
224
|
-
private async writeTempMarker(
|
|
352
|
+
private async writeTempMarker(
|
|
353
|
+
tempDirArg: string,
|
|
354
|
+
exclusive = false,
|
|
355
|
+
): Promise<IToolCacheMarker> {
|
|
356
|
+
await plugins.fsPromises.mkdir(tempDirArg, { recursive: true });
|
|
225
357
|
this.assertManagedTempDir(tempDirArg);
|
|
226
358
|
const marker: IToolCacheMarker = {
|
|
227
359
|
owner: toolCacheOwner,
|
|
228
360
|
kind: tempCacheKind,
|
|
229
361
|
safeToPrune: true,
|
|
230
362
|
createdAt: new Date().toISOString(),
|
|
363
|
+
invocationId: this.invocationId,
|
|
364
|
+
...createProcessOwnership(),
|
|
231
365
|
schemaVersion: 1,
|
|
232
366
|
};
|
|
233
|
-
await plugins.fsPromises.mkdir(tempDirArg, { recursive: true });
|
|
234
367
|
await plugins.fsPromises.writeFile(
|
|
235
368
|
plugins.path.join(tempDirArg, toolCacheMarkerFile),
|
|
236
369
|
`${JSON.stringify(marker, null, 2)}\n`,
|
|
370
|
+
{ flag: exclusive ? 'wx' : 'w', mode: 0o600 },
|
|
237
371
|
);
|
|
372
|
+
return marker;
|
|
238
373
|
}
|
|
239
374
|
|
|
240
375
|
private async readTempMarker(tempDirArg: string): Promise<IToolCacheMarker | undefined> {
|
|
241
|
-
this.assertManagedTempDir(tempDirArg);
|
|
242
376
|
try {
|
|
377
|
+
this.assertManagedTempDir(tempDirArg);
|
|
243
378
|
const markerRaw = await plugins.fsPromises.readFile(plugins.path.join(tempDirArg, toolCacheMarkerFile), 'utf8');
|
|
244
|
-
|
|
379
|
+
let marker: Partial<IToolCacheMarker>;
|
|
380
|
+
try {
|
|
381
|
+
marker = JSON.parse(markerRaw) as Partial<IToolCacheMarker>;
|
|
382
|
+
} catch {
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
245
385
|
if (
|
|
246
386
|
marker.owner === toolCacheOwner &&
|
|
247
387
|
marker.kind === tempCacheKind &&
|
|
248
|
-
|
|
249
|
-
|
|
388
|
+
marker.safeToPrune === true &&
|
|
389
|
+
(marker.invocationId === undefined || (
|
|
390
|
+
typeof marker.invocationId === 'string'
|
|
391
|
+
&& /^[A-Za-z0-9._-]{1,128}$/.test(marker.invocationId)
|
|
392
|
+
)) &&
|
|
393
|
+
(marker.processId === undefined || (
|
|
394
|
+
typeof marker.processId === 'number'
|
|
395
|
+
&& Number.isSafeInteger(marker.processId)
|
|
396
|
+
&& marker.processId > 0
|
|
397
|
+
)) &&
|
|
398
|
+
(marker.processIdentity === undefined || typeof marker.processIdentity === 'string') &&
|
|
399
|
+
(marker.leaseManaged === undefined || typeof marker.leaseManaged === 'boolean') &&
|
|
400
|
+
marker.schemaVersion === 1
|
|
250
401
|
) {
|
|
251
402
|
return marker as IToolCacheMarker;
|
|
252
403
|
}
|
|
253
|
-
} catch {
|
|
254
|
-
|
|
404
|
+
} catch (error: unknown) {
|
|
405
|
+
if (this.hasErrorCode(error) && error.code === 'ENOENT') {
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
408
|
+
throw error;
|
|
255
409
|
}
|
|
256
410
|
|
|
257
411
|
return undefined;
|
|
258
412
|
}
|
|
259
413
|
|
|
414
|
+
private async cleanupStaleMarkedTempWorkspaces(
|
|
415
|
+
rootDirectoryArg: string,
|
|
416
|
+
thresholdMsArg: number,
|
|
417
|
+
): Promise<boolean> {
|
|
418
|
+
let didCleanup = false;
|
|
419
|
+
let entries;
|
|
420
|
+
try {
|
|
421
|
+
entries = await plugins.fsPromises.readdir(rootDirectoryArg, { withFileTypes: true });
|
|
422
|
+
} catch (error: unknown) {
|
|
423
|
+
if (this.hasErrorCode(error) && error.code === 'ENOENT') {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
throw error;
|
|
427
|
+
}
|
|
428
|
+
for (const entry of entries) {
|
|
429
|
+
if (
|
|
430
|
+
!entry.isDirectory()
|
|
431
|
+
|| (!entry.name.startsWith(localTempPrefix) && !entry.name.startsWith(cleanupTempPrefix))
|
|
432
|
+
) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const tempDir = plugins.path.join(rootDirectoryArg, entry.name);
|
|
436
|
+
let marker: IToolCacheMarker | undefined;
|
|
437
|
+
try {
|
|
438
|
+
marker = await this.readTempMarker(tempDir);
|
|
439
|
+
} catch (error: unknown) {
|
|
440
|
+
if (this.hasErrorCode(error) && error.code === 'ENOENT') {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
445
|
+
if (!marker) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
const createdAt = Date.parse(marker.createdAt);
|
|
449
|
+
if (
|
|
450
|
+
!Number.isFinite(createdAt)
|
|
451
|
+
|| Date.now() - createdAt < thresholdMsArg
|
|
452
|
+
|| this.isTempMarkerActive(marker, tempDir, thresholdMsArg)
|
|
453
|
+
) {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
didCleanup = await this.cleanupTempWorkspace(
|
|
457
|
+
'stale isolated temp workspace',
|
|
458
|
+
tempDir,
|
|
459
|
+
marker,
|
|
460
|
+
) || didCleanup;
|
|
461
|
+
}
|
|
462
|
+
return didCleanup;
|
|
463
|
+
}
|
|
464
|
+
|
|
260
465
|
private async getDirectorySize(pathArg: string): Promise<number> {
|
|
261
466
|
let total = 0;
|
|
262
467
|
let entries;
|
|
@@ -294,15 +499,53 @@ export class CustomBundleHandler {
|
|
|
294
499
|
return `${value.toFixed(decimals)} ${units[unitIndex]}`;
|
|
295
500
|
}
|
|
296
501
|
|
|
297
|
-
private async cleanupTempWorkspace(
|
|
298
|
-
|
|
502
|
+
private async cleanupTempWorkspace(
|
|
503
|
+
reasonArg: string,
|
|
504
|
+
tempDir = this.getTempDir(),
|
|
505
|
+
expectedMarkerArg?: IToolCacheMarker,
|
|
506
|
+
): Promise<boolean> {
|
|
507
|
+
try {
|
|
508
|
+
this.assertManagedTempDir(tempDir);
|
|
509
|
+
} catch (error: unknown) {
|
|
510
|
+
if (this.hasErrorCode(error) && error.code === 'ENOENT') {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
299
515
|
const marker = await this.readTempMarker(tempDir);
|
|
300
|
-
if (!marker) {
|
|
301
|
-
return;
|
|
516
|
+
if (!marker || (expectedMarkerArg && !this.markersMatch(marker, expectedMarkerArg))) {
|
|
517
|
+
return false;
|
|
302
518
|
}
|
|
303
|
-
const
|
|
304
|
-
|
|
519
|
+
const quarantinePath = plugins.path.join(
|
|
520
|
+
plugins.path.dirname(tempDir),
|
|
521
|
+
`${cleanupTempPrefix}${this.invocationId}-${Math.random().toString(36).slice(2)}`,
|
|
522
|
+
);
|
|
523
|
+
try {
|
|
524
|
+
await plugins.fsPromises.rename(tempDir, quarantinePath);
|
|
525
|
+
} catch (error: unknown) {
|
|
526
|
+
if (this.hasErrorCode(error) && error.code === 'ENOENT') {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
throw error;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const quarantinedMarker = await this.readTempMarker(quarantinePath);
|
|
533
|
+
if (!quarantinedMarker || !this.markersMatch(marker, quarantinedMarker)) {
|
|
534
|
+
try {
|
|
535
|
+
if (!plugins.fsSync.existsSync(tempDir)) {
|
|
536
|
+
await plugins.fsPromises.rename(quarantinePath, tempDir);
|
|
537
|
+
}
|
|
538
|
+
} catch (error: unknown) {
|
|
539
|
+
if (!this.hasErrorCode(error) || !['EEXIST', 'ENOENT'].includes(error.code || '')) {
|
|
540
|
+
throw error;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
const reclaimedBytes = await this.getDirectorySize(quarantinePath);
|
|
546
|
+
await plugins.fsPromises.rm(quarantinePath, { recursive: true, force: true });
|
|
305
547
|
console.log(`Cleaned tsbundle temp workspace after ${reasonArg}, reclaimed ${this.formatBytes(reclaimedBytes)}`);
|
|
548
|
+
return true;
|
|
306
549
|
}
|
|
307
550
|
|
|
308
551
|
/**
|
|
@@ -313,10 +556,12 @@ export class CustomBundleHandler {
|
|
|
313
556
|
tempBundlePath: string
|
|
314
557
|
): Promise<void> {
|
|
315
558
|
const base64Output = new Base64TsOutput(this.cwd);
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
559
|
+
const tempDir = plugins.path.dirname(tempBundlePath);
|
|
560
|
+
for (const artifactPath of await this.listBundleArtifacts(tempDir)) {
|
|
561
|
+
const relativePath = plugins.path.relative(tempDir, artifactPath);
|
|
562
|
+
const artifactContent = await plugins.fs.file(artifactPath).read();
|
|
563
|
+
base64Output.addFile(relativePath, artifactContent);
|
|
564
|
+
}
|
|
320
565
|
|
|
321
566
|
// Add included files
|
|
322
567
|
if (bundleConfig.includeFiles && bundleConfig.includeFiles.length > 0) {
|
|
@@ -330,7 +575,10 @@ export class CustomBundleHandler {
|
|
|
330
575
|
}
|
|
331
576
|
|
|
332
577
|
// Write the TypeScript output
|
|
333
|
-
|
|
578
|
+
const outputPath = plugins.smartpath.transform.toAbsolute(bundleConfig.to, this.cwd) as string;
|
|
579
|
+
await withArtifactPublicationLocks(outputPath, async () => {
|
|
580
|
+
await base64Output.writeToFile(bundleConfig.to, bundleConfig.maxLineLength);
|
|
581
|
+
});
|
|
334
582
|
}
|
|
335
583
|
|
|
336
584
|
/**
|
|
@@ -340,35 +588,42 @@ export class CustomBundleHandler {
|
|
|
340
588
|
bundleConfig: interfaces.IBundleConfig,
|
|
341
589
|
tempBundlePath: string
|
|
342
590
|
): Promise<void> {
|
|
343
|
-
// Move bundle to final destination
|
|
344
591
|
const toPath = plugins.smartpath.transform.toAbsolute(bundleConfig.to, this.cwd) as string;
|
|
345
592
|
const toDir = plugins.path.dirname(toPath);
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
await plugins.fs.file(toPath).write(bundleContent);
|
|
350
|
-
console.log(`Bundle written to: ${bundleConfig.to}`);
|
|
351
|
-
|
|
352
|
-
if (bundleConfig.sourcemap === false) {
|
|
353
|
-
await plugins.fsPromises.rm(`${toPath}.map`, { force: true });
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// Process included files (copy them)
|
|
593
|
+
const tempDir = plugins.path.dirname(tempBundlePath);
|
|
594
|
+
const logicalOutputName = plugins.path.basename(toPath);
|
|
595
|
+
const additionalArtifacts: IAdditionalArtifact[] = [];
|
|
357
596
|
if (bundleConfig.includeFiles && bundleConfig.includeFiles.length > 0) {
|
|
358
|
-
const htmlHandler = new HtmlHandler();
|
|
359
|
-
const outputDir = plugins.path.dirname(toPath);
|
|
360
|
-
|
|
361
597
|
for (const entry of bundleConfig.includeFiles) {
|
|
362
598
|
const pattern = typeof entry === 'string' ? entry : entry.from;
|
|
363
|
-
await this.
|
|
599
|
+
additionalArtifacts.push(...await this.collectIncludedFiles(pattern, toDir));
|
|
364
600
|
}
|
|
365
601
|
}
|
|
602
|
+
await publishGeneratedArtifacts({
|
|
603
|
+
additionalArtifacts,
|
|
604
|
+
sourceDirectory: tempDir,
|
|
605
|
+
sourceMainPath: tempBundlePath,
|
|
606
|
+
targetPath: toPath,
|
|
607
|
+
lockTargetPath: plugins.path.join(toDir, '.tsbundle-output-directory'),
|
|
608
|
+
logicalOutputName,
|
|
609
|
+
chunkNamespace: getChunkNamespace(logicalOutputName),
|
|
610
|
+
sourceMapsEnabled: bundleConfig.sourcemap !== false,
|
|
611
|
+
});
|
|
612
|
+
console.log(`Bundle written to: ${bundleConfig.to}`);
|
|
613
|
+
for (const artifact of additionalArtifacts) {
|
|
614
|
+
const relativePath = plugins.path.relative(this.cwd, artifact.sourcePath);
|
|
615
|
+
console.log(`Copied: ${relativePath} -> ${artifact.targetPath}`);
|
|
616
|
+
}
|
|
366
617
|
}
|
|
367
618
|
|
|
368
619
|
/**
|
|
369
|
-
*
|
|
620
|
+
* Resolve files matching a pattern for transactional publication.
|
|
370
621
|
*/
|
|
371
|
-
private async
|
|
622
|
+
private async collectIncludedFiles(
|
|
623
|
+
pattern: string,
|
|
624
|
+
outputDir: string,
|
|
625
|
+
): Promise<IAdditionalArtifact[]> {
|
|
626
|
+
const artifacts: IAdditionalArtifact[] = [];
|
|
372
627
|
const absolutePattern = plugins.smartpath.transform.toAbsolute(pattern, this.cwd) as string;
|
|
373
628
|
const patternDir = plugins.path.dirname(absolutePattern);
|
|
374
629
|
const patternBase = plugins.path.basename(absolutePattern);
|
|
@@ -379,7 +634,7 @@ export class CustomBundleHandler {
|
|
|
379
634
|
const dirExists = await plugins.fs.directory(dirPath).exists();
|
|
380
635
|
if (!dirExists) {
|
|
381
636
|
console.log(`Directory does not exist: ${dirPath}`);
|
|
382
|
-
return;
|
|
637
|
+
return artifacts;
|
|
383
638
|
}
|
|
384
639
|
|
|
385
640
|
const isRecursive = pattern.includes('**');
|
|
@@ -397,24 +652,21 @@ export class CustomBundleHandler {
|
|
|
397
652
|
if (!entry.isDirectory && regex.test(entry.name)) {
|
|
398
653
|
// entry.path is already absolute from smartfs
|
|
399
654
|
const fullPath = entry.path;
|
|
400
|
-
const relativePath = plugins.path.relative(this.cwd, fullPath);
|
|
401
655
|
const destPath = plugins.path.join(outputDir, plugins.path.basename(entry.path));
|
|
402
|
-
|
|
403
|
-
await plugins.fs.file(fullPath).copy(destPath);
|
|
404
|
-
console.log(`Copied: ${relativePath} -> ${destPath}`);
|
|
656
|
+
artifacts.push({ sourcePath: fullPath, targetPath: destPath });
|
|
405
657
|
}
|
|
406
658
|
}
|
|
407
659
|
} else {
|
|
408
660
|
const fileExists = await plugins.fs.file(absolutePattern).exists();
|
|
409
661
|
if (!fileExists) {
|
|
410
662
|
console.log(`File does not exist: ${absolutePattern}`);
|
|
411
|
-
return;
|
|
663
|
+
return artifacts;
|
|
412
664
|
}
|
|
413
665
|
const fileName = plugins.path.basename(absolutePattern);
|
|
414
666
|
const destPath = plugins.path.join(outputDir, fileName);
|
|
415
|
-
|
|
416
|
-
console.log(`Copied: ${pattern} -> ${destPath}`);
|
|
667
|
+
artifacts.push({ sourcePath: absolutePattern, targetPath: destPath });
|
|
417
668
|
}
|
|
669
|
+
return artifacts;
|
|
418
670
|
}
|
|
419
671
|
}
|
|
420
672
|
|