vouchington-tooling 0.0.14 → 0.0.16

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.
Files changed (79) hide show
  1. package/README.md +13 -0
  2. package/dist/ci-local/index.d.mts +5 -0
  3. package/dist/ci-local/index.mjs +79 -0
  4. package/dist/ci-local/parse.d.mts +3 -0
  5. package/dist/ci-local/parse.mjs +43 -0
  6. package/dist/ci-local/types.d.mts +41 -0
  7. package/dist/ci-local/types.mjs +1 -0
  8. package/dist/cli/index.mjs +5 -0
  9. package/dist/cli/parse.d.mts +1 -1
  10. package/dist/cli/parse.mjs +5 -0
  11. package/dist/cli/usage.d.mts +1 -1
  12. package/dist/cli/usage.mjs +10 -0
  13. package/dist/coverage-transport/constants.d.mts +3 -0
  14. package/dist/coverage-transport/constants.mjs +11 -0
  15. package/dist/coverage-transport/control.d.mts +47 -0
  16. package/dist/coverage-transport/control.mjs +112 -0
  17. package/dist/coverage-transport/http.d.mts +6 -0
  18. package/dist/coverage-transport/http.mjs +99 -0
  19. package/dist/coverage-transport/index.d.mts +6 -0
  20. package/dist/coverage-transport/index.mjs +6 -0
  21. package/dist/coverage-transport/lib.d.mts +17 -0
  22. package/dist/coverage-transport/lib.mjs +84 -0
  23. package/dist/coverage-transport/outcome.d.mts +29 -0
  24. package/dist/coverage-transport/outcome.mjs +70 -0
  25. package/dist/coverage-transport/presign.d.mts +22 -0
  26. package/dist/coverage-transport/presign.mjs +51 -0
  27. package/dist/coverage-transport/vitest-blob-transport.d.mts +5 -0
  28. package/dist/coverage-transport/vitest-blob-transport.mjs +129 -0
  29. package/dist/dockerfile-parse/index.d.mts +4 -0
  30. package/dist/dockerfile-parse/index.mjs +2 -0
  31. package/dist/dockerfile-parse/prewarm-ports.d.mts +12 -0
  32. package/dist/dockerfile-parse/prewarm-ports.mjs +59 -0
  33. package/dist/dockerfile-parse/runtime-cmd.d.mts +11 -0
  34. package/dist/dockerfile-parse/runtime-cmd.mjs +142 -0
  35. package/dist/dockerfile-parse/stages.d.mts +7 -0
  36. package/dist/dockerfile-parse/stages.mjs +18 -0
  37. package/dist/gha-pr-checkpoint/index.d.mts +58 -0
  38. package/dist/gha-pr-checkpoint/index.mjs +100 -0
  39. package/dist/gha-rate-limit/index.d.mts +15 -0
  40. package/dist/gha-rate-limit/index.mjs +69 -0
  41. package/dist/index.d.mts +15 -0
  42. package/dist/index.mjs +8 -0
  43. package/dist/pnpm-deploy/index.d.mts +6 -0
  44. package/dist/pnpm-deploy/index.mjs +3 -0
  45. package/dist/pnpm-deploy/normalize.d.mts +15 -0
  46. package/dist/pnpm-deploy/normalize.mjs +64 -0
  47. package/dist/pnpm-deploy/prune.d.mts +14 -0
  48. package/dist/pnpm-deploy/prune.mjs +48 -0
  49. package/dist/pnpm-deploy/restore.d.mts +31 -0
  50. package/dist/pnpm-deploy/restore.mjs +109 -0
  51. package/dist/scc-complexity/index.d.mts +19 -0
  52. package/dist/scc-complexity/index.mjs +91 -0
  53. package/dist/transient-retry/decision-evaluator.d.mts +2 -0
  54. package/dist/transient-retry/decision-evaluator.mjs +6 -0
  55. package/dist/transient-retry/types.d.mts +3 -3
  56. package/dist/workspace-gates/dependabot.d.mts +3 -0
  57. package/dist/workspace-gates/dependabot.mjs +26 -0
  58. package/dist/workspace-gates/exclude-registry.d.mts +3 -0
  59. package/dist/workspace-gates/exclude-registry.mjs +30 -0
  60. package/dist/workspace-gates/first-party-graph.d.mts +4 -0
  61. package/dist/workspace-gates/first-party-graph.mjs +87 -0
  62. package/dist/workspace-gates/index.d.mts +2 -0
  63. package/dist/workspace-gates/index.mjs +1 -0
  64. package/dist/workspace-gates/options.d.mts +27 -0
  65. package/dist/workspace-gates/options.mjs +15 -0
  66. package/dist/workspace-gates/policy.d.mts +5 -0
  67. package/dist/workspace-gates/policy.mjs +27 -0
  68. package/dist/workspace-gates/test-helpers.d.mts +21 -0
  69. package/dist/workspace-gates/test-helpers.mjs +69 -0
  70. package/dist/workspace-gates/yaml-loader.d.mts +2 -0
  71. package/dist/workspace-gates/yaml-loader.mjs +28 -0
  72. package/dist/workspace-glob.d.mts +1 -0
  73. package/dist/workspace-glob.mjs +46 -0
  74. package/package.json +44 -2
  75. package/scripts/gha/check-cache-size.sh +38 -0
  76. package/scripts/gha/clean-workspace.sh +227 -0
  77. package/scripts/gha/install-github-release.sh +136 -0
  78. package/scripts/gha/load-runner-env.sh +75 -0
  79. package/scripts/gha/make-shard-matrix.sh +30 -0
@@ -0,0 +1,58 @@
1
+ export declare const CHECKPOINT_MARKER = "pr-checkpoint:v1";
2
+ export type CheckpointStatus = 'queued' | 'running' | 'awaiting_verification' | 'failed' | 'deadline' | 'unresumable' | 'complete';
3
+ export type Checkpoint = {
4
+ marker: string;
5
+ repository: string;
6
+ pr: number;
7
+ /** Absent on checkpoints posted before trigger ids were recorded; keep optional so resume can still parse them. */
8
+ triggerCommentId?: number;
9
+ headRef: string;
10
+ startSha: string;
11
+ sessionStartSha: string;
12
+ runId: string;
13
+ runUrl: string;
14
+ actor: string;
15
+ sessionId: string;
16
+ /** Absent from v1 checkpoints created before session URLs were exposed. */
17
+ sessionUrl?: string;
18
+ resumeSourceRunId: string;
19
+ status: CheckpointStatus;
20
+ createdAt: string;
21
+ updatedAt: string;
22
+ };
23
+ export type GitHubComment = {
24
+ id: number;
25
+ user?: {
26
+ login?: string;
27
+ type?: string;
28
+ };
29
+ performed_via_github_app?: {
30
+ slug?: string;
31
+ } | null;
32
+ body?: string;
33
+ created_at?: string;
34
+ };
35
+ export interface CheckpointCodecOptions {
36
+ marker?: string;
37
+ sessionIdPattern?: RegExp;
38
+ }
39
+ export interface TrustedCheckpointActor {
40
+ actor: string;
41
+ userType?: string;
42
+ appSlug?: string;
43
+ }
44
+ /**
45
+ * Anti-forgery predicate: a checkpoint HTML comment is plain text any PR commenter can paste,
46
+ * so `parseCheckpoint` alone proves shape, not provenance. This proves the comment itself was
47
+ * authored by the expected App identity.
48
+ */
49
+ export declare function isTrustedCheckpointComment(comment: GitHubComment, context: TrustedCheckpointActor): boolean;
50
+ export declare function renderCheckpoint(checkpoint: Checkpoint, options?: {
51
+ marker?: string;
52
+ }): string;
53
+ export declare function parseCheckpoint(body: string, options?: CheckpointCodecOptions): Checkpoint | undefined;
54
+ export declare function validateCheckpoint(value: unknown, options?: CheckpointCodecOptions): Checkpoint | undefined;
55
+ export declare function sortedCheckpointCandidates(comments: GitHubComment[], options?: CheckpointCodecOptions): {
56
+ comment: GitHubComment;
57
+ checkpoint: Checkpoint;
58
+ }[];
@@ -0,0 +1,100 @@
1
+ export const CHECKPOINT_MARKER = 'pr-checkpoint:v1';
2
+ /**
3
+ * Anti-forgery predicate: a checkpoint HTML comment is plain text any PR commenter can paste,
4
+ * so `parseCheckpoint` alone proves shape, not provenance. This proves the comment itself was
5
+ * authored by the expected App identity.
6
+ */
7
+ export function isTrustedCheckpointComment(comment, context) {
8
+ return (comment.user?.login === context.actor &&
9
+ comment.user?.type === (context.userType ?? 'Bot') &&
10
+ comment.performed_via_github_app?.slug === (context.appSlug ?? 'github-actions'));
11
+ }
12
+ export function renderCheckpoint(checkpoint, options = {}) {
13
+ const marker = options.marker ?? checkpoint.marker;
14
+ const payload = Buffer.from(JSON.stringify({ ...checkpoint, marker }), 'utf8').toString('base64url');
15
+ const session = checkpoint.sessionUrl
16
+ ? `[${checkpoint.sessionId}](${checkpoint.sessionUrl})`
17
+ : `\`${checkpoint.sessionId || 'pending'}\``;
18
+ return [
19
+ `👀 [View the automation workflow run](${checkpoint.runUrl}).`,
20
+ '',
21
+ `Status: \`${checkpoint.status}\` · Session: ${session}`,
22
+ '',
23
+ `<!-- ${marker} ${payload} -->`,
24
+ ].join('\n');
25
+ }
26
+ export function parseCheckpoint(body, options = {}) {
27
+ const marker = options.marker ?? CHECKPOINT_MARKER;
28
+ const match = body.match(new RegExp(`<!-- ${escapeRegExp(marker)} ([A-Za-z0-9_-]+) -->`, 'u'));
29
+ if (!match?.[1])
30
+ return undefined;
31
+ try {
32
+ return validateCheckpoint(JSON.parse(Buffer.from(match[1], 'base64url').toString('utf8')), options);
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ }
38
+ export function validateCheckpoint(value, options = {}) {
39
+ if (!value || typeof value !== 'object')
40
+ return undefined;
41
+ const checkpoint = value;
42
+ const marker = options.marker ?? CHECKPOINT_MARKER;
43
+ if (checkpoint.marker !== marker ||
44
+ !Number.isInteger(checkpoint.pr) ||
45
+ (checkpoint.triggerCommentId !== undefined && !Number.isInteger(checkpoint.triggerCommentId)) ||
46
+ typeof checkpoint.startSha !== 'string' ||
47
+ !/^[0-9a-f]{40}$/u.test(checkpoint.startSha) ||
48
+ typeof checkpoint.sessionStartSha !== 'string' ||
49
+ !/^[0-9a-f]{40}$/u.test(checkpoint.sessionStartSha) ||
50
+ typeof checkpoint.runId !== 'string' ||
51
+ !/^[0-9]+$/u.test(checkpoint.runId) ||
52
+ typeof checkpoint.resumeSourceRunId !== 'string' ||
53
+ (checkpoint.resumeSourceRunId !== '' && !/^[0-9]+$/u.test(checkpoint.resumeSourceRunId)) ||
54
+ typeof checkpoint.repository !== 'string' ||
55
+ typeof checkpoint.headRef !== 'string' ||
56
+ typeof checkpoint.runUrl !== 'string' ||
57
+ typeof checkpoint.actor !== 'string' ||
58
+ typeof checkpoint.sessionId !== 'string' ||
59
+ typeof checkpoint.createdAt !== 'string' ||
60
+ !Number.isFinite(Date.parse(checkpoint.createdAt)) ||
61
+ typeof checkpoint.updatedAt !== 'string' ||
62
+ !Number.isFinite(Date.parse(checkpoint.updatedAt)) ||
63
+ typeof checkpoint.status !== 'string' ||
64
+ ![
65
+ 'queued',
66
+ 'running',
67
+ 'awaiting_verification',
68
+ 'failed',
69
+ 'deadline',
70
+ 'unresumable',
71
+ 'complete',
72
+ ].includes(checkpoint.status)) {
73
+ return undefined;
74
+ }
75
+ if (checkpoint.sessionId &&
76
+ options.sessionIdPattern &&
77
+ !matchesPattern(options.sessionIdPattern, checkpoint.sessionId)) {
78
+ return undefined;
79
+ }
80
+ if (checkpoint.sessionUrl !== undefined &&
81
+ (!URL.canParse(checkpoint.sessionUrl) || !checkpoint.sessionUrl.startsWith('https:'))) {
82
+ return undefined;
83
+ }
84
+ return checkpoint;
85
+ }
86
+ export function sortedCheckpointCandidates(comments, options = {}) {
87
+ return comments
88
+ .map((comment) => ({ comment, checkpoint: parseCheckpoint(comment.body ?? '', options) }))
89
+ .filter((candidate) => candidate.checkpoint !== undefined)
90
+ .toSorted((left, right) => {
91
+ const timestampOrder = String(right.comment.created_at ?? '').localeCompare(String(left.comment.created_at ?? ''));
92
+ return timestampOrder || right.comment.id - left.comment.id;
93
+ });
94
+ }
95
+ function matchesPattern(pattern, value) {
96
+ return new RegExp(pattern.source, pattern.flags.replaceAll(/[gy]/g, '')).test(value);
97
+ }
98
+ function escapeRegExp(value) {
99
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
100
+ }
@@ -0,0 +1,15 @@
1
+ export declare const MAX_RATE_LIMIT_WAIT_MS = 120000;
2
+ export declare class GitHubRateLimitError extends Error {
3
+ readonly code: string;
4
+ readonly httpStatus?: number | undefined;
5
+ readonly retryAfter?: string | undefined;
6
+ readonly rateLimitRemaining?: string | undefined;
7
+ readonly rateLimitReset?: string | undefined;
8
+ constructor(code: string, message: string, httpStatus?: number, retryAfter?: string, rateLimitRemaining?: string, rateLimitReset?: string);
9
+ }
10
+ export declare function isRetryableCancellationError(error: unknown): boolean;
11
+ export declare function rateLimitDelay(error: GitHubRateLimitError): number;
12
+ export declare function isRateLimited(error: unknown): error is GitHubRateLimitError;
13
+ export declare function reserveRateLimitDelay(used: {
14
+ milliseconds: number;
15
+ }, delay: number): void;
@@ -0,0 +1,69 @@
1
+ const MAX_DELAY_MS = 60_000;
2
+ export const MAX_RATE_LIMIT_WAIT_MS = 120_000;
3
+ export class GitHubRateLimitError extends Error {
4
+ code;
5
+ httpStatus;
6
+ retryAfter;
7
+ rateLimitRemaining;
8
+ rateLimitReset;
9
+ constructor(code, message, httpStatus, retryAfter, rateLimitRemaining, rateLimitReset) {
10
+ super(message);
11
+ this.code = code;
12
+ this.httpStatus = httpStatus;
13
+ this.retryAfter = retryAfter;
14
+ this.rateLimitRemaining = rateLimitRemaining;
15
+ this.rateLimitReset = rateLimitReset;
16
+ this.name = 'GitHubRateLimitError';
17
+ }
18
+ }
19
+ function invalid() {
20
+ throw new GitHubRateLimitError('INVALID_RESPONSE', 'GitHub rate-limit response is invalid');
21
+ }
22
+ function rateLimit403(error) {
23
+ return (error.httpStatus === 403 && (error.retryAfter !== undefined || error.rateLimitRemaining === '0'));
24
+ }
25
+ export function isRetryableCancellationError(error) {
26
+ return (error instanceof GitHubRateLimitError &&
27
+ (error.code === 'REQUEST_FAILED' ||
28
+ (error.code === 'HTTP_STATUS' &&
29
+ (error.httpStatus === 429 ||
30
+ rateLimit403(error) ||
31
+ ((error.httpStatus ?? 0) >= 500 && (error.httpStatus ?? 0) <= 599)))));
32
+ }
33
+ export function rateLimitDelay(error) {
34
+ if (error.retryAfter !== undefined) {
35
+ if (!/^\d+$/u.test(error.retryAfter))
36
+ invalid();
37
+ const milliseconds = Number(error.retryAfter) * 1000;
38
+ if (!Number.isSafeInteger(milliseconds) || milliseconds > MAX_DELAY_MS)
39
+ invalid();
40
+ return milliseconds;
41
+ }
42
+ if (error.rateLimitRemaining !== undefined && !/^\d+$/u.test(error.rateLimitRemaining))
43
+ invalid();
44
+ if (error.rateLimitRemaining !== '0') {
45
+ if (error.httpStatus !== 429)
46
+ invalid();
47
+ return MAX_DELAY_MS;
48
+ }
49
+ if (!error.rateLimitReset || !/^\d+$/u.test(error.rateLimitReset))
50
+ invalid();
51
+ const reset = Number(error.rateLimitReset);
52
+ if (!Number.isSafeInteger(reset))
53
+ invalid();
54
+ const milliseconds = Math.max(0, reset * 1000 - Date.now());
55
+ if (milliseconds > MAX_DELAY_MS)
56
+ invalid();
57
+ return milliseconds;
58
+ }
59
+ export function isRateLimited(error) {
60
+ return (error instanceof GitHubRateLimitError &&
61
+ error.code === 'HTTP_STATUS' &&
62
+ (error.httpStatus === 429 || rateLimit403(error)));
63
+ }
64
+ export function reserveRateLimitDelay(used, delay) {
65
+ if (used.milliseconds + delay > MAX_RATE_LIMIT_WAIT_MS) {
66
+ throw new GitHubRateLimitError('RATE_LIMIT_WAIT_EXHAUSTED', 'GitHub rate-limit wait budget is exhausted');
67
+ }
68
+ used.milliseconds += delay;
69
+ }
package/dist/index.d.mts CHANGED
@@ -34,3 +34,18 @@ export type { AstGrepRuleInvocation, RunAstGrepRuleOptions } from './ast-grep-ru
34
34
  export { bodyOnlyReviewFallback, indexReviewFiles, MAX_REVIEW_COMMENTS, MAX_REVIEW_PAYLOAD_BYTES, nearestReviewLine, parsePatchCommentable, parseReviewFilesJson, parseReviewPayload, readRegularReviewPayload, remapReviewComments, ReviewPayloadError, reviewCommentSubject, rewriteSnappedSuggestion, snapReviewNote, stageReviewPayload, writeStagedOutput, } from './gha-review-payload/index.mts';
35
35
  export type { CommentableIndex, CommentableLine, LineKind, PayloadRequirement, ReviewComment, ReviewFile, ReviewSide, SanitizedReview, } from './gha-review-payload/index.mts';
36
36
  export { nextPageCursorFromLinkHeader, nextPageUrlFromLinkHeader, validatePaginationRequestUrl, } from './http-link-pagination/index.mts';
37
+ export { cmdDownloadCoverage, cmdDownloadVitestBlobs, cmdUpload, mintPresignedControl, transportObjectKeys, } from './coverage-transport/index.mts';
38
+ export type { ExpectedTransportIdentity, ObjectSigner, PresignIdentity, TransportControl, } from './coverage-transport/index.mts';
39
+ export { EPOCH_PRUNED_AT, normalizeDeployedLayer, pruneDeployedRuntimeDeps, restoreDeployedWorkspacePackages, } from './pnpm-deploy/index.mts';
40
+ export type { NormalizeDeployedLayerResult, PruneResult, RestoreWorkspacePackagesOptions, } from './pnpm-deploy/index.mts';
41
+ export { parseDockerfilePrewarmStages, parseDockerfileRuntimeImages, } from './dockerfile-parse/index.mts';
42
+ export type { DockerfilePrewarmStage, DockerfileRuntimeImage, ParseDockerfilePrewarmOptions, ParseDockerfileRuntimeImagesOptions, } from './dockerfile-parse/index.mts';
43
+ export { buildSccArgs, checkSccComplexity, parseSccComplexityViolations, SCC_COMPLEXITY_LIMIT, } from './scc-complexity/index.mts';
44
+ export type { SccComplexityOptions, SccComplexityViolation } from './scc-complexity/index.mts';
45
+ export { assertWorkflowCommandDrift, parseCiLocalArgs, runCiLocal } from './ci-local/index.mts';
46
+ export type { CiLocalCommand, CiLocalSpawn, CiLocalSpawnOptions, CiLocalSpawnResult, CiLocalTarget, RunCiLocalOptions, } from './ci-local/index.mts';
47
+ export { GitHubRateLimitError, isRateLimited, isRetryableCancellationError, MAX_RATE_LIMIT_WAIT_MS, rateLimitDelay, reserveRateLimitDelay, } from './gha-rate-limit/index.mts';
48
+ export { CHECKPOINT_MARKER, isTrustedCheckpointComment, parseCheckpoint, renderCheckpoint, sortedCheckpointCandidates, validateCheckpoint, } from './gha-pr-checkpoint/index.mts';
49
+ export type { Checkpoint, CheckpointCodecOptions, GitHubComment, } from './gha-pr-checkpoint/index.mts';
50
+ export { checkWorkspaceGatesPolicy } from './workspace-gates/index.mts';
51
+ export type { WorkspaceGatesOptions } from './workspace-gates/index.mts';
package/dist/index.mjs CHANGED
@@ -20,3 +20,11 @@ export { MissingResponseBodyError, readResponseBody, readResponseBodyAsBuffer, R
20
20
  export { parseAstGrepRuleArgs, runAstGrepRule } from './ast-grep-rule/index.mjs';
21
21
  export { bodyOnlyReviewFallback, indexReviewFiles, MAX_REVIEW_COMMENTS, MAX_REVIEW_PAYLOAD_BYTES, nearestReviewLine, parsePatchCommentable, parseReviewFilesJson, parseReviewPayload, readRegularReviewPayload, remapReviewComments, ReviewPayloadError, reviewCommentSubject, rewriteSnappedSuggestion, snapReviewNote, stageReviewPayload, writeStagedOutput, } from './gha-review-payload/index.mjs';
22
22
  export { nextPageCursorFromLinkHeader, nextPageUrlFromLinkHeader, validatePaginationRequestUrl, } from './http-link-pagination/index.mjs';
23
+ export { cmdDownloadCoverage, cmdDownloadVitestBlobs, cmdUpload, mintPresignedControl, transportObjectKeys, } from './coverage-transport/index.mjs';
24
+ export { EPOCH_PRUNED_AT, normalizeDeployedLayer, pruneDeployedRuntimeDeps, restoreDeployedWorkspacePackages, } from './pnpm-deploy/index.mjs';
25
+ export { parseDockerfilePrewarmStages, parseDockerfileRuntimeImages, } from './dockerfile-parse/index.mjs';
26
+ export { buildSccArgs, checkSccComplexity, parseSccComplexityViolations, SCC_COMPLEXITY_LIMIT, } from './scc-complexity/index.mjs';
27
+ export { assertWorkflowCommandDrift, parseCiLocalArgs, runCiLocal } from './ci-local/index.mjs';
28
+ export { GitHubRateLimitError, isRateLimited, isRetryableCancellationError, MAX_RATE_LIMIT_WAIT_MS, rateLimitDelay, reserveRateLimitDelay, } from './gha-rate-limit/index.mjs';
29
+ export { CHECKPOINT_MARKER, isTrustedCheckpointComment, parseCheckpoint, renderCheckpoint, sortedCheckpointCandidates, validateCheckpoint, } from './gha-pr-checkpoint/index.mjs';
30
+ export { checkWorkspaceGatesPolicy } from './workspace-gates/index.mjs';
@@ -0,0 +1,6 @@
1
+ export { pruneDeployedRuntimeDeps, runPruneDeployedRuntimeDepsCli } from './prune.mts';
2
+ export type { PruneResult } from './prune.mts';
3
+ export { EPOCH_PRUNED_AT, normalizeDeployedLayer, runNormalizeDeployedLayerCli, } from './normalize.mts';
4
+ export type { NormalizeDeployedLayerResult } from './normalize.mts';
5
+ export { restoreDeployedWorkspacePackages, runRestoreDeployedWorkspacePackagesCli, } from './restore.mts';
6
+ export type { RestoreWorkspacePackagesOptions } from './restore.mts';
@@ -0,0 +1,3 @@
1
+ export { pruneDeployedRuntimeDeps, runPruneDeployedRuntimeDepsCli } from './prune.mjs';
2
+ export { EPOCH_PRUNED_AT, normalizeDeployedLayer, runNormalizeDeployedLayerCli, } from './normalize.mjs';
3
+ export { restoreDeployedWorkspacePackages, runRestoreDeployedWorkspacePackagesCli, } from './restore.mjs';
@@ -0,0 +1,15 @@
1
+ export declare const EPOCH_PRUNED_AT = "Thu, 01 Jan 1970 00:00:00 GMT";
2
+ export interface NormalizeDeployedLayerResult {
3
+ prunedAtPinned: boolean;
4
+ entriesTouched: number;
5
+ }
6
+ export declare function normalizeDeployedLayer(prodDir: string): NormalizeDeployedLayerResult;
7
+ interface NormalizeCliOptions {
8
+ args: readonly string[];
9
+ env: NodeJS.ProcessEnv;
10
+ isMain: boolean;
11
+ normalize?: typeof normalizeDeployedLayer;
12
+ stdout: (message: string) => void;
13
+ }
14
+ export declare function runNormalizeDeployedLayerCli({ args, env, isMain, normalize, stdout, }: NormalizeCliOptions): void;
15
+ export {};
@@ -0,0 +1,64 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export const EPOCH_PRUNED_AT = 'Thu, 01 Jan 1970 00:00:00 GMT';
4
+ export function normalizeDeployedLayer(prodDir) {
5
+ return {
6
+ prunedAtPinned: pinPrunedAt(prodDir),
7
+ entriesTouched: clampTreeMtimes(prodDir),
8
+ };
9
+ }
10
+ function pinPrunedAt(prodDir) {
11
+ const modulesPath = path.join(prodDir, 'node_modules', '.modules.yaml');
12
+ if (!fs.existsSync(modulesPath)) {
13
+ throw new Error(`${modulesPath} is missing; expected pnpm deploy to write it`);
14
+ }
15
+ // pnpm 11.13.1 writeModulesManifest serializes this file with JSON.stringify
16
+ // despite the .yaml name. A pnpm bump that switches to YAML must update this parse.
17
+ const parsed = JSON.parse(fs.readFileSync(modulesPath, 'utf8'));
18
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
19
+ throw new Error(`${modulesPath} is not a JSON object`);
20
+ }
21
+ const manifest = parsed;
22
+ manifest.prunedAt = EPOCH_PRUNED_AT;
23
+ fs.writeFileSync(modulesPath, `${JSON.stringify(manifest, null, 2)}\n`);
24
+ return true;
25
+ }
26
+ function clampTreeMtimes(root) {
27
+ let entriesTouched = 0;
28
+ function visit(entryPath, isDirectory) {
29
+ if (isDirectory) {
30
+ for (const entry of fs.readdirSync(entryPath, { withFileTypes: true })) {
31
+ visit(path.join(entryPath, entry.name), entry.isDirectory());
32
+ }
33
+ }
34
+ else {
35
+ const stats = fs.lstatSync(entryPath);
36
+ if (stats.isFile() && stats.nlink > 1) {
37
+ const copyPath = `${entryPath}.${process.pid}.copy`;
38
+ fs.copyFileSync(entryPath, copyPath);
39
+ fs.renameSync(copyPath, entryPath);
40
+ }
41
+ }
42
+ fs.lutimesSync(entryPath, 0, 0);
43
+ entriesTouched += 1;
44
+ }
45
+ visit(root, fs.lstatSync(root).isDirectory());
46
+ return entriesTouched;
47
+ }
48
+ function resolveCliProdDirs(args, env) {
49
+ if (args.length > 0)
50
+ return [...args];
51
+ const prodDir = env.PROD_DIR;
52
+ if (!prodDir)
53
+ throw new Error('PROD_DIR or a directory argument is required');
54
+ return [prodDir];
55
+ }
56
+ export function runNormalizeDeployedLayerCli({ args, env, isMain, normalize = normalizeDeployedLayer, stdout, }) {
57
+ if (!isMain)
58
+ return;
59
+ for (const prodDir of resolveCliProdDirs(args, env)) {
60
+ const result = normalize(prodDir);
61
+ const prunedAt = result.prunedAtPinned ? 'prunedAt pinned' : 'prunedAt absent';
62
+ stdout(`Normalized deployed layer at ${prodDir} (${prunedAt}, ${result.entriesTouched} entries)`);
63
+ }
64
+ }
@@ -0,0 +1,14 @@
1
+ export interface PruneResult {
2
+ bytesRemoved: number;
3
+ filesRemoved: number;
4
+ }
5
+ export declare function pruneDeployedRuntimeDeps(prodDir: string): PruneResult;
6
+ interface PruneCliOptions {
7
+ args: readonly string[];
8
+ env: NodeJS.ProcessEnv;
9
+ isMain: boolean;
10
+ prune?: typeof pruneDeployedRuntimeDeps;
11
+ stdout: (message: string) => void;
12
+ }
13
+ export declare function runPruneDeployedRuntimeDepsCli({ args, env, isMain, prune, stdout, }: PruneCliOptions): void;
14
+ export {};
@@ -0,0 +1,48 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const DECLARATION_SUFFIXES = ['.d.ts', '.d.mts', '.d.cts', '.d.ts.map', '.d.mts.map', '.d.cts.map'];
4
+ function shouldPruneRuntimeDependencyFile(filePath) {
5
+ return (DECLARATION_SUFFIXES.some((suffix) => filePath.endsWith(suffix)) ||
6
+ filePath.endsWith('.tsbuildinfo'));
7
+ }
8
+ function pruneDirectory(dir) {
9
+ const result = { bytesRemoved: 0, filesRemoved: 0 };
10
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
11
+ const entryPath = path.join(dir, entry.name);
12
+ if (entry.isDirectory()) {
13
+ const child = pruneDirectory(entryPath);
14
+ result.bytesRemoved += child.bytesRemoved;
15
+ result.filesRemoved += child.filesRemoved;
16
+ continue;
17
+ }
18
+ if (!entry.isFile() || !shouldPruneRuntimeDependencyFile(entryPath))
19
+ continue;
20
+ const { size } = fs.statSync(entryPath);
21
+ fs.rmSync(entryPath, { force: true });
22
+ result.bytesRemoved += size;
23
+ result.filesRemoved += 1;
24
+ }
25
+ return result;
26
+ }
27
+ export function pruneDeployedRuntimeDeps(prodDir) {
28
+ const pnpmStore = path.join(prodDir, 'node_modules', '.pnpm');
29
+ if (!fs.existsSync(pnpmStore))
30
+ return { bytesRemoved: 0, filesRemoved: 0 };
31
+ return pruneDirectory(pnpmStore);
32
+ }
33
+ function resolveCliProdDirs(args, env) {
34
+ if (args.length > 0)
35
+ return [...args];
36
+ const prodDir = env.PROD_DIR;
37
+ if (!prodDir)
38
+ throw new Error('PROD_DIR or a directory argument is required');
39
+ return [prodDir];
40
+ }
41
+ export function runPruneDeployedRuntimeDepsCli({ args, env, isMain, prune = pruneDeployedRuntimeDeps, stdout, }) {
42
+ if (!isMain)
43
+ return;
44
+ for (const prodDir of resolveCliProdDirs(args, env)) {
45
+ const result = prune(prodDir);
46
+ stdout(`Pruned ${result.filesRemoved} runtime dependency artifact(s) from ${prodDir} (${result.bytesRemoved} bytes)`);
47
+ }
48
+ }
@@ -0,0 +1,31 @@
1
+ export interface RestoreWorkspacePackagesOptions {
2
+ backendDir?: string;
3
+ prodDir: string;
4
+ workspaceRoot?: string;
5
+ }
6
+ /**
7
+ * Node's --experimental-strip-types refuses to load TypeScript whose real path
8
+ * contains a node_modules segment, but pnpm deploy places workspace packages
9
+ * (shipped as TypeScript source) inside the virtual store at
10
+ * node_modules/.pnpm/<store-dir>/node_modules/<name>.
11
+ *
12
+ * Relocate every workspace-package copy outside node_modules while preserving
13
+ * pnpm's resolution topology, without enumerating dependencies:
14
+ *
15
+ * - move the content dir to workspace-packages/<store-dir>/<name>
16
+ * - leave a relative symlink at the original virtual-store location, so every
17
+ * pnpm-created link (top-level node_modules entries, sibling links from other
18
+ * store dirs) keeps resolving through it
19
+ * - add a node_modules backlink inside the moved dir pointing at the package's
20
+ * original virtual-store sibling directory, so the package's own imports
21
+ * resolve exactly as pnpm laid them out
22
+ */
23
+ export declare function restoreDeployedWorkspacePackages({ backendDir, prodDir, workspaceRoot, }: RestoreWorkspacePackagesOptions): void;
24
+ interface RestoreCliOptions {
25
+ args: readonly string[];
26
+ env: NodeJS.ProcessEnv;
27
+ isMain: boolean;
28
+ restore?: typeof restoreDeployedWorkspacePackages;
29
+ }
30
+ export declare function runRestoreDeployedWorkspacePackagesCli({ args, env, isMain, restore, }: RestoreCliOptions): void;
31
+ export {};
@@ -0,0 +1,109 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { expandWorkspaceGlob } from '../workspace-glob.mjs';
4
+ function packagePath(packageName) {
5
+ return path.join(...packageName.split('/'));
6
+ }
7
+ function linkTarget(fromDir, toPath) {
8
+ return path.relative(fromDir, toPath);
9
+ }
10
+ function readWorkspacePackageNames(backendDir) {
11
+ const backendPackage = JSON.parse(fs.readFileSync(path.join(backendDir, 'package.json'), 'utf8'));
12
+ const workspaces = Array.isArray(backendPackage.workspaces) ? backendPackage.workspaces : [];
13
+ const names = new Set();
14
+ for (const workspace of workspaces.flatMap((pattern) => expandWorkspaceGlob(backendDir, pattern))) {
15
+ const packageJson = path.join(workspace, 'package.json');
16
+ if (!fs.existsSync(packageJson))
17
+ continue;
18
+ const { name } = JSON.parse(fs.readFileSync(packageJson, 'utf8'));
19
+ if (name)
20
+ names.add(name);
21
+ }
22
+ return names;
23
+ }
24
+ function mergeSibling(backlink, storeModules, sibling) {
25
+ const siblingLink = path.join(backlink, sibling);
26
+ const source = path.join(storeModules, sibling);
27
+ const existing = fs.lstatSync(siblingLink, { throwIfNoEntry: false });
28
+ if (!existing) {
29
+ fs.symlinkSync(linkTarget(backlink, source), siblingLink, 'dir');
30
+ return;
31
+ }
32
+ if (existing.isSymbolicLink() || !existing.isDirectory())
33
+ return;
34
+ const sourceStats = fs.statSync(source, { throwIfNoEntry: false });
35
+ if (!sourceStats?.isDirectory())
36
+ return;
37
+ for (const child of fs.readdirSync(source))
38
+ mergeSibling(siblingLink, source, child);
39
+ }
40
+ /**
41
+ * Node's --experimental-strip-types refuses to load TypeScript whose real path
42
+ * contains a node_modules segment, but pnpm deploy places workspace packages
43
+ * (shipped as TypeScript source) inside the virtual store at
44
+ * node_modules/.pnpm/<store-dir>/node_modules/<name>.
45
+ *
46
+ * Relocate every workspace-package copy outside node_modules while preserving
47
+ * pnpm's resolution topology, without enumerating dependencies:
48
+ *
49
+ * - move the content dir to workspace-packages/<store-dir>/<name>
50
+ * - leave a relative symlink at the original virtual-store location, so every
51
+ * pnpm-created link (top-level node_modules entries, sibling links from other
52
+ * store dirs) keeps resolving through it
53
+ * - add a node_modules backlink inside the moved dir pointing at the package's
54
+ * original virtual-store sibling directory, so the package's own imports
55
+ * resolve exactly as pnpm laid them out
56
+ */
57
+ export function restoreDeployedWorkspacePackages({ backendDir, prodDir, workspaceRoot, }) {
58
+ if (!prodDir)
59
+ throw new Error('prodDir is required');
60
+ if (!backendDir)
61
+ throw new Error('backendDir is required');
62
+ const resolvedWorkspaceRoot = workspaceRoot ?? path.join(prodDir, 'workspace-packages');
63
+ const workspacePackageNames = readWorkspacePackageNames(backendDir);
64
+ const pnpmRoot = path.join(prodDir, 'node_modules', '.pnpm');
65
+ if (!fs.existsSync(pnpmRoot))
66
+ return;
67
+ for (const storeEntry of fs.readdirSync(pnpmRoot, { withFileTypes: true })) {
68
+ if (!storeEntry.isDirectory() || storeEntry.name === 'node_modules')
69
+ continue;
70
+ const storeModules = path.join(pnpmRoot, storeEntry.name, 'node_modules');
71
+ for (const name of workspacePackageNames) {
72
+ const contentDir = path.join(storeModules, packagePath(name));
73
+ const stats = fs.lstatSync(contentDir, { throwIfNoEntry: false });
74
+ if (!stats?.isDirectory())
75
+ continue;
76
+ const relocated = path.join(resolvedWorkspaceRoot, storeEntry.name, packagePath(name));
77
+ fs.rmSync(relocated, { recursive: true, force: true });
78
+ fs.mkdirSync(path.dirname(relocated), { recursive: true });
79
+ // copy+delete, not rename: renaming a directory out of a lower Docker
80
+ // overlayfs layer fails with EXDEV
81
+ fs.cpSync(contentDir, relocated, { recursive: true, verbatimSymlinks: true });
82
+ fs.rmSync(contentDir, { recursive: true, force: true });
83
+ fs.symlinkSync(linkTarget(path.dirname(contentDir), relocated), contentDir, 'dir');
84
+ const backlink = path.join(relocated, 'node_modules');
85
+ const backlinkStats = fs.lstatSync(backlink, { throwIfNoEntry: false });
86
+ if (!backlinkStats) {
87
+ fs.symlinkSync(linkTarget(relocated, storeModules), backlink, 'dir');
88
+ }
89
+ else if (backlinkStats.isDirectory()) {
90
+ // pnpm deploy leaves a real node_modules (containing .bin) inside some
91
+ // injected packages; merge sibling links into it instead of replacing it
92
+ for (const sibling of fs.readdirSync(storeModules)) {
93
+ mergeSibling(backlink, storeModules, sibling);
94
+ }
95
+ }
96
+ }
97
+ }
98
+ }
99
+ export function runRestoreDeployedWorkspacePackagesCli({ args, env, isMain, restore = restoreDeployedWorkspacePackages, }) {
100
+ if (!isMain)
101
+ return;
102
+ const prodDir = args[0] ?? env.PROD_DIR;
103
+ const backendDir = args[1] ?? env.BACKEND_DIR;
104
+ if (!prodDir)
105
+ throw new Error('PROD_DIR or a directory argument is required');
106
+ if (!backendDir)
107
+ throw new Error('BACKEND_DIR or a backend directory argument is required');
108
+ restore({ prodDir, backendDir });
109
+ }
@@ -0,0 +1,19 @@
1
+ import type { SharedContext } from '../shared-context/index.mts';
2
+ export declare const SCC_COMPLEXITY_LIMIT = 50;
3
+ export interface SccComplexityOptions {
4
+ limit?: number;
5
+ includeExt?: string;
6
+ excludeDir?: string;
7
+ notMatch?: string;
8
+ tmpdirPrefix?: string;
9
+ command?: string;
10
+ }
11
+ export interface SccComplexityViolation {
12
+ file: string;
13
+ complexity: number;
14
+ }
15
+ export declare function buildSccArgs(options?: SccComplexityOptions): string[];
16
+ export declare function checkSccComplexity(ctx: SharedContext, options?: SccComplexityOptions, runScc?: (outputPath: string) => Promise<string>): Promise<{
17
+ errors: string[];
18
+ }>;
19
+ export declare function parseSccComplexityViolations(json: string, trackedFileSet: ReadonlySet<string>, limit?: number): SccComplexityViolation[];