srcpack 0.3.0 → 1.0.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/README.md +22 -5
- package/dist/args.d.ts +26 -0
- package/dist/bundle.d.ts +0 -21
- package/dist/cli.js +4846 -14943
- package/dist/config.d.ts +43 -3
- package/dist/fs.d.ts +41 -0
- package/dist/index.js +423 -10459
- package/dist/plan.d.ts +64 -0
- package/dist/screenshot.d.ts +113 -0
- package/package.json +10 -1
- package/src/args.ts +221 -0
- package/src/bundle.ts +1 -24
- package/src/cli.ts +238 -340
- package/src/config.ts +79 -9
- package/src/fs.ts +80 -0
- package/src/plan.ts +238 -0
- package/src/screenshot.ts +545 -0
package/src/config.ts
CHANGED
|
@@ -77,6 +77,45 @@ const LinearSourceSchema = z.union([
|
|
|
77
77
|
}),
|
|
78
78
|
]);
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* An absolute http(s) URL. A scheme-less `localhost:5173` is called out by
|
|
82
|
+
* name: `URL` would otherwise parse it as protocol `localhost:`.
|
|
83
|
+
*/
|
|
84
|
+
const HttpUrlSchema = z.string().superRefine((value, ctx) => {
|
|
85
|
+
if (!value.includes("://")) {
|
|
86
|
+
ctx.addIssue({
|
|
87
|
+
code: "custom",
|
|
88
|
+
message: 'URL needs a scheme, e.g. "http://localhost:5173/"',
|
|
89
|
+
});
|
|
90
|
+
} else if (!/^https?:\/\//i.test(value) || !URL.canParse(value)) {
|
|
91
|
+
ctx.addIssue({ code: "custom", message: "Expected an http(s) URL" });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A rendered page as numbered PNGs in `outDir`, independent of the text
|
|
97
|
+
* output path. See ADR 006 for why it is a separate source key.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* bundles: {
|
|
102
|
+
* home: { screenshot: "http://localhost:5173/", onDemand: true },
|
|
103
|
+
* phone: { screenshot: { url: "http://localhost:5173/", viewport: "mobile" } },
|
|
104
|
+
* }
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
const ScreenshotSourceSchema = z.union([
|
|
108
|
+
/** Shorthand for `{ url: "<url>" }`. */
|
|
109
|
+
HttpUrlSchema,
|
|
110
|
+
z.strictObject({
|
|
111
|
+
url: HttpUrlSchema,
|
|
112
|
+
/** Defaults to "desktop". */
|
|
113
|
+
viewport: z.enum(["desktop", "mobile"]).optional(),
|
|
114
|
+
/** CSS selectors hidden during capture: cookie banners, chat widgets. */
|
|
115
|
+
hide: z.array(z.string().min(1)).optional(),
|
|
116
|
+
}),
|
|
117
|
+
]);
|
|
118
|
+
|
|
80
119
|
/**
|
|
81
120
|
* Bundle configuration. Accepts a string pattern, array of patterns, or object.
|
|
82
121
|
* Patterns prefixed with `!` are exclusions. Patterns prefixed with `+` force
|
|
@@ -86,7 +125,8 @@ const LinearSourceSchema = z.union([
|
|
|
86
125
|
* `git:unstaged`, `git:untracked`, `git:dirty`, or `git:<rev>` (e.g.
|
|
87
126
|
* `git:main`, `git:HEAD~3`).
|
|
88
127
|
*
|
|
89
|
-
* The object form takes files (`include`), Linear issues (`linear`),
|
|
128
|
+
* The object form takes files (`include`), Linear issues (`linear`), page
|
|
129
|
+
* screenshots (`screenshot`), or any combination.
|
|
90
130
|
*
|
|
91
131
|
* @example
|
|
92
132
|
* ```ts
|
|
@@ -105,15 +145,46 @@ const BundleConfigSchema = z.union([
|
|
|
105
145
|
include: PatternsSchema.optional(),
|
|
106
146
|
/** Linear issues to include in the bundle. */
|
|
107
147
|
linear: LinearSourceSchema.optional(),
|
|
148
|
+
/** Page to capture as numbered PNGs in `outDir`. */
|
|
149
|
+
screenshot: ScreenshotSourceSchema.optional(),
|
|
108
150
|
/** Custom output file path. Defaults to `<outDir>/<bundleName>.txt`. */
|
|
109
151
|
outfile: z.string().min(1).optional(),
|
|
110
|
-
/**
|
|
111
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Include file index header in output. Defaults to true — read as
|
|
154
|
+
* `index ?? true`, so screenshot-only bundles can reject an explicit value.
|
|
155
|
+
*/
|
|
156
|
+
index: z.boolean().optional(),
|
|
112
157
|
/** Text to prepend to bundle (e.g., review instructions for LLMs). */
|
|
113
158
|
prompt: z.string().optional(),
|
|
159
|
+
/**
|
|
160
|
+
* Skipped by a full run, built when named: `srcpack <name>`. For bundles
|
|
161
|
+
* too slow, remote or situational to rebuild every time. A full run that
|
|
162
|
+
* empties `outDir` still removes their output there — preserving it
|
|
163
|
+
* would make emptying a growing list of exceptions (ADR 005).
|
|
164
|
+
*/
|
|
165
|
+
onDemand: z.boolean().optional(),
|
|
114
166
|
})
|
|
115
|
-
.
|
|
116
|
-
|
|
167
|
+
.superRefine((bundle, ctx) => {
|
|
168
|
+
if (bundle.include || bundle.linear) return;
|
|
169
|
+
if (!bundle.screenshot) {
|
|
170
|
+
ctx.addIssue({
|
|
171
|
+
code: "custom",
|
|
172
|
+
message:
|
|
173
|
+
'Bundle needs a source: "include" patterns, "linear", or "screenshot"',
|
|
174
|
+
});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// These shape a text file that a screenshot-only bundle never writes, so
|
|
178
|
+
// setting one is a misunderstanding rather than a harmless no-op
|
|
179
|
+
for (const key of ["prompt", "index", "outfile"] as const) {
|
|
180
|
+
if (bundle[key] !== undefined) {
|
|
181
|
+
ctx.addIssue({
|
|
182
|
+
code: "custom",
|
|
183
|
+
path: [key],
|
|
184
|
+
message: `"${key}" applies to the text file, and a screenshot-only bundle writes none. Add "include" or "linear", or remove it`,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
117
188
|
}),
|
|
118
189
|
]);
|
|
119
190
|
|
|
@@ -164,10 +235,8 @@ const ConfigSchema = z
|
|
|
164
235
|
bundles: z.record(BundleNameSchema, BundleConfigSchema),
|
|
165
236
|
})
|
|
166
237
|
.superRefine((config, ctx) => {
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
// the one failure mode where a typo is worse than a missing line. A stale
|
|
170
|
-
// entry left over from a deleted bundle is cheap to fix by comparison.
|
|
238
|
+
// Reject misspelled exclusions so a bundle intended to stay local cannot
|
|
239
|
+
// silently upload. Removed bundles must also be removed from this list.
|
|
171
240
|
const uploads = config.upload
|
|
172
241
|
? Array.isArray(config.upload)
|
|
173
242
|
? config.upload
|
|
@@ -193,6 +262,7 @@ const ConfigSchema = z
|
|
|
193
262
|
|
|
194
263
|
export type UploadConfig = z.infer<typeof UploadConfigSchema>;
|
|
195
264
|
export type LinearSourceInput = z.input<typeof LinearSourceSchema>;
|
|
265
|
+
export type ScreenshotSource = z.infer<typeof ScreenshotSourceSchema>;
|
|
196
266
|
export type BundleConfig = z.infer<typeof BundleConfigSchema>;
|
|
197
267
|
export type BundleConfigInput = z.input<typeof BundleConfigSchema>;
|
|
198
268
|
export type Config = z.infer<typeof ConfigSchema>;
|
package/src/fs.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
import { realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Compare paths using Unicode NFC followed by lowercase, on every platform.
|
|
8
|
+
* Normalizing first gives equivalent spellings the same input to lowercasing.
|
|
9
|
+
* This catches case and normalization collisions even for unwritten outputs
|
|
10
|
+
* and unresolved final entries, where realpath cannot canonicalize spelling.
|
|
11
|
+
* Applying one rule everywhere prevents a config from passing on Linux and
|
|
12
|
+
* overwriting a bundle on a case-insensitive filesystem (ADR 004).
|
|
13
|
+
*
|
|
14
|
+
* Comparison only: I/O retains the original spelling. Ownership and deletion
|
|
15
|
+
* use exact matches so folding cannot widen what srcpack removes.
|
|
16
|
+
*/
|
|
17
|
+
export function pathKey(path: string): string {
|
|
18
|
+
return path.normalize("NFC").toLowerCase();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Where a path physically is, with symlinks resolved. Destructive decisions are
|
|
23
|
+
* made on this rather than the lexical path: `.srcpack -> ../shared` is inside
|
|
24
|
+
* the project by name and somewhere else in fact, and it is the somewhere else
|
|
25
|
+
* whose contents `rm` would take.
|
|
26
|
+
*
|
|
27
|
+
* Resolves as much of the path as exists, however deep that is. Stopping at the
|
|
28
|
+
* immediate parent would call `.srcpack/nested/x.txt` and `alias/nested/x.txt`
|
|
29
|
+
* different files until `mkdir -p` runs, which is one step too late to still be
|
|
30
|
+
* a check: aliasing is a property of the ancestors, not of when they were made.
|
|
31
|
+
*/
|
|
32
|
+
export async function physicalPath(path: string): Promise<string> {
|
|
33
|
+
try {
|
|
34
|
+
return await realpath(path);
|
|
35
|
+
} catch {
|
|
36
|
+
const parent = dirname(path);
|
|
37
|
+
// dirname("/") === "/": nothing above the filesystem root left to resolve
|
|
38
|
+
if (parent === path) return path;
|
|
39
|
+
return join(await physicalPath(parent), basename(path));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Where `rename` puts a directory entry: ancestors resolved, the entry itself
|
|
45
|
+
* left alone. Writing replaces the entry instead of following it, so a bundle
|
|
46
|
+
* whose output is a symlink is identified as the link rather than its target —
|
|
47
|
+
* writing to the link path and to its target produces two separate files.
|
|
48
|
+
*/
|
|
49
|
+
export async function entryPath(path: string): Promise<string> {
|
|
50
|
+
return join(await physicalPath(dirname(path)), basename(path));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isInside(path: string, dir: string): boolean {
|
|
54
|
+
const rel = relative(dir, path);
|
|
55
|
+
// Compare against ".." as a whole segment — "..cache/x" is a child, not an escape
|
|
56
|
+
return rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Write a file by replacing the directory entry rather than the file behind
|
|
61
|
+
* it. Writing in place follows a symlink sitting at the output path, so
|
|
62
|
+
* `.srcpack/web.txt -> ~/.ssh/config` would be written through; rename replaces
|
|
63
|
+
* the link itself. It also makes each file appear whole or not at all.
|
|
64
|
+
*
|
|
65
|
+
* The temp name carries the pid so two runs can't rename each other's file.
|
|
66
|
+
*/
|
|
67
|
+
export async function writeFileAtomic(
|
|
68
|
+
path: string,
|
|
69
|
+
data: string | Uint8Array,
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
72
|
+
try {
|
|
73
|
+
await writeFile(temp, data);
|
|
74
|
+
await rename(temp, path);
|
|
75
|
+
} finally {
|
|
76
|
+
// A failed write or rename would otherwise leave a partial file behind:
|
|
77
|
+
// stale inside outDir, and bundled by the next run beside a custom outfile.
|
|
78
|
+
await rm(temp, { force: true });
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/plan.ts
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
4
|
+
import type { BundleResult } from "./bundle.ts";
|
|
5
|
+
import { ConfigError, type BundleConfig } from "./config.ts";
|
|
6
|
+
import { entryPath, pathKey, physicalPath } from "./fs.ts";
|
|
7
|
+
import {
|
|
8
|
+
isImageOf,
|
|
9
|
+
toScreenshotTarget,
|
|
10
|
+
type CapturedPage,
|
|
11
|
+
type ScreenshotTarget,
|
|
12
|
+
} from "./screenshot.ts";
|
|
13
|
+
|
|
14
|
+
// Check output destinations before resolving sources or launching a browser.
|
|
15
|
+
// Retain each active plan through writing, reporting and upload.
|
|
16
|
+
|
|
17
|
+
/** Where a bundle writes. */
|
|
18
|
+
export interface PlannedBundle {
|
|
19
|
+
name: string;
|
|
20
|
+
source: BundleConfig;
|
|
21
|
+
/** Absolute path of the text output. Absent for a screenshot-only bundle. */
|
|
22
|
+
text?: { outfile: string };
|
|
23
|
+
/** Page to capture into `<dir>/<name>-NN.png`. */
|
|
24
|
+
images?: { target: ScreenshotTarget; dir: string };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** What a bundle produced, ready to write. */
|
|
28
|
+
export interface ResolvedBundle {
|
|
29
|
+
plan: PlannedBundle;
|
|
30
|
+
text?: BundleResult;
|
|
31
|
+
images?: CapturedPage;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface BundleSelection {
|
|
35
|
+
/** Bundles this run builds, in the order named or configured. */
|
|
36
|
+
names: string[];
|
|
37
|
+
/** On-demand bundles a full run left out. Empty when bundles are named. */
|
|
38
|
+
skipped: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isOnDemand(config: BundleConfig): boolean {
|
|
42
|
+
return (
|
|
43
|
+
typeof config === "object" &&
|
|
44
|
+
!Array.isArray(config) &&
|
|
45
|
+
config.onDemand === true
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Pick the bundles a run builds. Naming a bundle always builds it; a full run
|
|
51
|
+
* builds everything not marked `onDemand`.
|
|
52
|
+
*/
|
|
53
|
+
export function selectBundles(
|
|
54
|
+
bundles: Record<string, BundleConfig>,
|
|
55
|
+
requested: string[],
|
|
56
|
+
): BundleSelection {
|
|
57
|
+
for (const name of requested) {
|
|
58
|
+
// hasOwn, not `in`: `srcpack toString` would otherwise find Object.prototype
|
|
59
|
+
if (!Object.hasOwn(bundles, name)) {
|
|
60
|
+
throw new ConfigError(`Unknown bundle: ${name}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (requested.length) {
|
|
64
|
+
return { names: [...new Set(requested)], skipped: [] };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const names: string[] = [];
|
|
68
|
+
const skipped: string[] = [];
|
|
69
|
+
for (const [name, config] of Object.entries(bundles)) {
|
|
70
|
+
(isOnDemand(config) ? skipped : names).push(name);
|
|
71
|
+
}
|
|
72
|
+
return { names, skipped };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface OutputPlan {
|
|
76
|
+
/** The bundles this run builds, in order. */
|
|
77
|
+
bundles: PlannedBundle[];
|
|
78
|
+
/**
|
|
79
|
+
* Absolute paths srcpack writes, never to be bundled: every configured text
|
|
80
|
+
* output and every active one, each in lexical and entry spelling. A glob
|
|
81
|
+
* rooted at a symlink yields lexical paths, one rooted at the real directory
|
|
82
|
+
* yields physical ones, and either can name a file the previous run wrote.
|
|
83
|
+
*
|
|
84
|
+
* Image families need no entry: they always live in outDir, which the CLI
|
|
85
|
+
* excludes, and when outDir holds the root a PNG is skipped as binary anyway.
|
|
86
|
+
*/
|
|
87
|
+
ownOutputs: string[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function planBundle(
|
|
91
|
+
name: string,
|
|
92
|
+
source: BundleConfig,
|
|
93
|
+
root: string,
|
|
94
|
+
outDir: string,
|
|
95
|
+
): PlannedBundle {
|
|
96
|
+
const object =
|
|
97
|
+
typeof source === "object" && !Array.isArray(source) ? source : undefined;
|
|
98
|
+
const plan: PlannedBundle = { name, source };
|
|
99
|
+
// Every bundle writes text unless a screenshot is all it declares
|
|
100
|
+
if (!object || object.include || object.linear) {
|
|
101
|
+
const outfile = object?.outfile ?? join(outDir, `${name}.txt`);
|
|
102
|
+
plan.text = { outfile: resolve(root, outfile) };
|
|
103
|
+
}
|
|
104
|
+
// Keep image cleanup within outDir, independent of any text outfile.
|
|
105
|
+
if (object?.screenshot) {
|
|
106
|
+
plan.images = {
|
|
107
|
+
target: toScreenshotTarget(object.screenshot),
|
|
108
|
+
dir: resolve(root, outDir),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return plan;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* A claim on a destination: a text file, or an image family — a directory
|
|
116
|
+
* plus a name prefix. Compared folded (`pathKey`) and physically.
|
|
117
|
+
*
|
|
118
|
+
* A text file's directory is its entry path's parent, as for any output. A
|
|
119
|
+
* family's directory resolves fully: it is an ancestor of every PNG, and
|
|
120
|
+
* `rename` replaces only the last component (ADR 004).
|
|
121
|
+
*/
|
|
122
|
+
interface Claim {
|
|
123
|
+
owner: string;
|
|
124
|
+
kind: "file" | "family";
|
|
125
|
+
dir: string;
|
|
126
|
+
/** Folded file name, or folded bundle name for a family. */
|
|
127
|
+
name: string;
|
|
128
|
+
/** Absolute path shown in a collision message. */
|
|
129
|
+
display: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function overlaps(a: Claim, b: Claim): boolean {
|
|
133
|
+
if (a.dir !== b.dir) return false;
|
|
134
|
+
if (a.kind === b.kind) return a.name === b.name;
|
|
135
|
+
const [file, family] = a.kind === "file" ? [a, b] : [b, a];
|
|
136
|
+
return isImageOf(family.name, file.name);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** `entry` is the text output's entry path, resolved once by the caller. */
|
|
140
|
+
async function claimsOf(
|
|
141
|
+
plan: PlannedBundle,
|
|
142
|
+
entry: string | undefined,
|
|
143
|
+
): Promise<Claim[]> {
|
|
144
|
+
const claims: Claim[] = [];
|
|
145
|
+
if (plan.text && entry) {
|
|
146
|
+
claims.push({
|
|
147
|
+
owner: plan.name,
|
|
148
|
+
kind: "file",
|
|
149
|
+
dir: pathKey(dirname(entry)),
|
|
150
|
+
name: pathKey(basename(entry)),
|
|
151
|
+
display: plan.text.outfile,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (plan.images) {
|
|
155
|
+
claims.push({
|
|
156
|
+
owner: plan.name,
|
|
157
|
+
kind: "family",
|
|
158
|
+
dir: pathKey(await physicalPath(plan.images.dir)),
|
|
159
|
+
name: pathKey(plan.name),
|
|
160
|
+
display: join(plan.images.dir, `${plan.name}-NN.png`),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return claims;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function collision(first: Claim, second: Claim, root: string): ConfigError {
|
|
167
|
+
// Name the later bundle's destination, unless only the earlier one is a file:
|
|
168
|
+
// a file is what the user can change
|
|
169
|
+
const { display } =
|
|
170
|
+
first.kind === "file" && second.kind === "family" ? first : second;
|
|
171
|
+
const where = relative(root, display) || display;
|
|
172
|
+
if (first.owner === second.owner) {
|
|
173
|
+
return new ConfigError(
|
|
174
|
+
`Bundle "${first.owner}" writes its text and its images to "${where}". Give it another outfile.`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return new ConfigError(
|
|
178
|
+
`Bundles "${first.owner}" and "${second.owner}" both write to "${where}". ` +
|
|
179
|
+
(first.kind === "family" && second.kind === "family"
|
|
180
|
+
? "Rename one of them."
|
|
181
|
+
: "Give one of them its own outfile."),
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Derive where every bundle writes and reject two bundles sharing a file.
|
|
187
|
+
*
|
|
188
|
+
* Resolve directory aliases and compare destinations with `pathKey` to prevent
|
|
189
|
+
* silent overwrites. Text files also collide with numbered images in the same
|
|
190
|
+
* directory, including their own bundle's images.
|
|
191
|
+
*
|
|
192
|
+
* Configured bundles are checked against each other on every run, so a config
|
|
193
|
+
* error doesn't depend on what was asked for. Active bundles — the ones `active`
|
|
194
|
+
* names, ad-hoc included — are checked against configured bundles of a
|
|
195
|
+
* different name: an ad-hoc bundle shadows a configured bundle of its own name
|
|
196
|
+
* (`--staged` over a `staged` bundle) but never overwrites another's output.
|
|
197
|
+
* Active bundles need no check among themselves: they are either configured,
|
|
198
|
+
* and already checked, or a single ad-hoc bundle.
|
|
199
|
+
*/
|
|
200
|
+
export async function planOutputs(
|
|
201
|
+
root: string,
|
|
202
|
+
outDir: string,
|
|
203
|
+
configured: Record<string, BundleConfig>,
|
|
204
|
+
active: [name: string, source: BundleConfig][],
|
|
205
|
+
): Promise<OutputPlan> {
|
|
206
|
+
const ownOutputs = new Set<string>();
|
|
207
|
+
const claims: Claim[] = [];
|
|
208
|
+
|
|
209
|
+
const claim = async (plan: PlannedBundle, shadowsOwnName: boolean) => {
|
|
210
|
+
const entry = plan.text && (await entryPath(plan.text.outfile));
|
|
211
|
+
for (const next of await claimsOf(plan, entry)) {
|
|
212
|
+
const taken = claims.find(
|
|
213
|
+
(prior) =>
|
|
214
|
+
!(shadowsOwnName && prior.owner === next.owner) &&
|
|
215
|
+
overlaps(prior, next),
|
|
216
|
+
);
|
|
217
|
+
if (taken) throw collision(taken, next, root);
|
|
218
|
+
claims.push(next);
|
|
219
|
+
}
|
|
220
|
+
if (plan.text && entry) {
|
|
221
|
+
ownOutputs.add(plan.text.outfile);
|
|
222
|
+
ownOutputs.add(entry);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
for (const [name, source] of Object.entries(configured)) {
|
|
227
|
+
await claim(planBundle(name, source, root, outDir), false);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const bundles: PlannedBundle[] = [];
|
|
231
|
+
for (const [name, source] of active) {
|
|
232
|
+
const plan = planBundle(name, source, root, outDir);
|
|
233
|
+
await claim(plan, true);
|
|
234
|
+
bundles.push(plan);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return { bundles, ownOutputs: [...ownOutputs] };
|
|
238
|
+
}
|