@smallpen/local-package 0.1.0-alpha.1 → 0.1.0-alpha.11
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/assets/fonts/SourceSansPro-OFL.txt +46 -0
- package/assets/fonts/WorkSans-OFL.txt +46 -0
- package/package.json +4 -4
- package/runtime/fonts/OFL.txt +46 -0
- package/runtime/fonts/sourcesanspro-black.ttf +0 -0
- package/runtime/fonts/sourcesanspro-blackitalic.ttf +0 -0
- package/runtime/fonts/sourcesanspro-bold.ttf +0 -0
- package/runtime/fonts/sourcesanspro-bolditalic.ttf +0 -0
- package/runtime/fonts/sourcesanspro-extralight.ttf +0 -0
- package/runtime/fonts/sourcesanspro-extralightitalic.ttf +0 -0
- package/runtime/fonts/sourcesanspro-italic.ttf +0 -0
- package/runtime/fonts/sourcesanspro-light.ttf +0 -0
- package/runtime/fonts/sourcesanspro-lightitalic.ttf +0 -0
- package/runtime/fonts/sourcesanspro-regular.ttf +0 -0
- package/runtime/fonts/sourcesanspro-semibold.ttf +0 -0
- package/runtime/fonts/sourcesanspro-semibolditalic.ttf +0 -0
- package/runtime/manifest.json +7 -0
- package/runtime/render-wasm.js +15 -0
- package/runtime/render-wasm.wasm +0 -0
- package/runtime/renderer.mjs +169652 -0
- package/src/draft-import.mjs +1 -11
- package/src/font-convert.mjs +23 -1
- package/src/fs-utils.mjs +95 -0
- package/src/index.mjs +19 -3
- package/src/initialize.mjs +363 -136
- package/src/kiwi-runtime.mjs +38 -36
- package/src/library-network.mjs +189 -0
- package/src/local-package-backend.mjs +215 -24
- package/src/local-package.mjs +589 -177
- package/src/local-workspace-session.mjs +278 -96
- package/src/migrate-themes.mjs +116 -0
- package/src/remote-library.mjs +167 -28
- package/src/render-fonts.mjs +172 -0
- package/src/render-shared.mjs +346 -0
- package/src/render.mjs +71 -3557
- package/src/workspace.mjs +178 -17
- package/src/webp-decode.mjs +0 -73
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { dirname, join, resolve } from "node:path";
|
|
2
3
|
|
|
3
|
-
import { SmallPenError } from "@smallpen/core";
|
|
4
|
+
import { followedVariantOperations, SmallPenError } from "@smallpen/core";
|
|
4
5
|
|
|
5
6
|
import { LocalPackageBackend } from "./local-package-backend.mjs";
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
localLibraryLocator,
|
|
9
|
+
MAX_LIBRARY_DECLARATIONS,
|
|
10
|
+
resolveWorkspace,
|
|
11
|
+
} from "./workspace.mjs";
|
|
12
|
+
|
|
13
|
+
// A Package in Repair because a linked Foundation or Library is unavailable
|
|
14
|
+
// gets no event when that link is fixed, so its status is resolved again.
|
|
15
|
+
const LINK_REPAIR_RETRY_MS = 1000;
|
|
7
16
|
|
|
8
17
|
function collectStableIds(value, result) {
|
|
9
18
|
if (Array.isArray(value)) {
|
|
@@ -49,22 +58,6 @@ function defaultTokenLibrary() {
|
|
|
49
58
|
};
|
|
50
59
|
}
|
|
51
60
|
|
|
52
|
-
async function seedDefaultTheme(backend, snapshot) {
|
|
53
|
-
if (snapshot.manifest.entries.tokens.length > 0) return snapshot;
|
|
54
|
-
await backend.commit({
|
|
55
|
-
baseRevision: snapshot.revision,
|
|
56
|
-
batchId: `seed-default-theme-${snapshot.revision.slice(0, 12)}`,
|
|
57
|
-
operations: [
|
|
58
|
-
{
|
|
59
|
-
entry: "tokens/tokens.json",
|
|
60
|
-
library: defaultTokenLibrary(),
|
|
61
|
-
type: "replace-token-library",
|
|
62
|
-
},
|
|
63
|
-
],
|
|
64
|
-
});
|
|
65
|
-
return backend.snapshot;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
61
|
export function reconcilePackageViewState(snapshot, value = {}) {
|
|
69
62
|
const firstScreenEntry = snapshot.manifest.entries.screens[0];
|
|
70
63
|
const defaultScreen =
|
|
@@ -94,13 +87,21 @@ export function reconcilePackageViewState(snapshot, value = {}) {
|
|
|
94
87
|
}
|
|
95
88
|
|
|
96
89
|
export class LocalWorkspaceSession {
|
|
90
|
+
#fetchImpl;
|
|
97
91
|
#libraryCacheRoot;
|
|
98
92
|
#listeners = new Set();
|
|
93
|
+
// Opens still running: requests, dependency events and Repair retries.
|
|
94
|
+
#opening = new Set();
|
|
99
95
|
#packages = new Map();
|
|
96
|
+
// Remote Library snapshots verified by this session, keyed by manifest URL.
|
|
97
|
+
// Status updates after commits reuse them so a slow or unreachable remote
|
|
98
|
+
// never blocks a local write; open() and refresh() read them again.
|
|
99
|
+
#remoteLibraries = new Map();
|
|
100
100
|
|
|
101
101
|
activeLocator;
|
|
102
102
|
|
|
103
|
-
constructor({ libraryCacheRoot } = {}) {
|
|
103
|
+
constructor({ fetchImpl, libraryCacheRoot } = {}) {
|
|
104
|
+
this.#fetchImpl = fetchImpl;
|
|
104
105
|
this.#libraryCacheRoot = libraryCacheRoot;
|
|
105
106
|
}
|
|
106
107
|
|
|
@@ -120,40 +121,67 @@ export class LocalWorkspaceSession {
|
|
|
120
121
|
}));
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
|
|
124
|
+
open(locator, viewState = {}) {
|
|
125
|
+
const opening = this.#open(locator, viewState);
|
|
126
|
+
this.#opening.add(opening);
|
|
127
|
+
const settled = () => this.#opening.delete(opening);
|
|
128
|
+
opening.then(settled, settled);
|
|
129
|
+
return opening;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async #open(locator, viewState) {
|
|
124
133
|
const requested = resolve(locator);
|
|
125
134
|
const existing = this.#find(requested);
|
|
126
135
|
if (existing) {
|
|
127
136
|
this.activeLocator = existing.locator;
|
|
128
137
|
return this.describe(existing.locator);
|
|
129
138
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
// Opening never writes: a Package without Tokens is served with the
|
|
140
|
+
// default Theme projected in memory and persisted by its first commit.
|
|
141
|
+
const backend = new LocalPackageBackend({
|
|
142
|
+
defaultTokenLibrary: defaultTokenLibrary(),
|
|
143
|
+
});
|
|
144
|
+
let session;
|
|
145
|
+
let opened;
|
|
146
|
+
try {
|
|
147
|
+
const snapshot = await backend.open(requested);
|
|
148
|
+
// A symlinked or non-canonical spelling of an open Package, or a
|
|
149
|
+
// concurrent open of the same one, selects the session already open.
|
|
150
|
+
opened = this.#packages.get(snapshot.locator);
|
|
151
|
+
if (!opened) {
|
|
152
|
+
const duplicate = [...this.#packages.values()].find(
|
|
153
|
+
(candidate) =>
|
|
154
|
+
candidate.backend.snapshot.runtime.file === snapshot.runtime.file,
|
|
155
|
+
);
|
|
156
|
+
if (duplicate) {
|
|
157
|
+
throw new SmallPenError(
|
|
158
|
+
"duplicate_file_id",
|
|
159
|
+
"Another open Package has the same persistent file identity",
|
|
160
|
+
{
|
|
161
|
+
fileId: snapshot.runtime.file,
|
|
162
|
+
locator: snapshot.locator,
|
|
163
|
+
openLocator: duplicate.locator,
|
|
164
|
+
packageId: snapshot.manifest.packageId,
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
session = {
|
|
169
|
+
backend,
|
|
143
170
|
locator: snapshot.locator,
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
171
|
+
unsubscribe: undefined,
|
|
172
|
+
statusSequence: 0,
|
|
173
|
+
viewState: reconcilePackageViewState(snapshot, viewState),
|
|
174
|
+
workspaceStatus: structuredClone(backend.status),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
} finally {
|
|
178
|
+
if (!session) await backend.close();
|
|
148
179
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
viewState: reconcilePackageViewState(snapshot, viewState),
|
|
155
|
-
workspaceStatus: structuredClone(backend.status),
|
|
156
|
-
};
|
|
180
|
+
if (opened) {
|
|
181
|
+
this.activeLocator = opened.locator;
|
|
182
|
+
return this.describe(opened.locator);
|
|
183
|
+
}
|
|
184
|
+
const { snapshot } = backend;
|
|
157
185
|
session.unsubscribe = backend.subscribe((event) => {
|
|
158
186
|
if (event.snapshot) {
|
|
159
187
|
session.viewState = reconcilePackageViewState(
|
|
@@ -161,7 +189,11 @@ export class LocalWorkspaceSession {
|
|
|
161
189
|
session.viewState,
|
|
162
190
|
);
|
|
163
191
|
}
|
|
164
|
-
|
|
192
|
+
// An external edit can retarget the Foundation or a Library.
|
|
193
|
+
const linked = event.snapshot
|
|
194
|
+
? this.#openLinkedPackages(event.snapshot)
|
|
195
|
+
: Promise.resolve();
|
|
196
|
+
void linked.then(() => this.#updateWorkspaceStatus(session));
|
|
165
197
|
this.#emit({ ...event, locator: session.locator });
|
|
166
198
|
if (
|
|
167
199
|
event.type === "external-revision" ||
|
|
@@ -173,25 +205,8 @@ export class LocalWorkspaceSession {
|
|
|
173
205
|
});
|
|
174
206
|
this.#packages.set(session.locator, session);
|
|
175
207
|
this.activeLocator ??= session.locator;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
try {
|
|
179
|
-
await this.open(join(dirname(snapshot.locator), dependency.path));
|
|
180
|
-
} catch {
|
|
181
|
-
// Composite status below retains Product and exposes dependency Repair.
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
for (const library of snapshot.manifest.libraries ?? []) {
|
|
185
|
-
if (library.source.type !== "local") continue;
|
|
186
|
-
try {
|
|
187
|
-
await this.open(
|
|
188
|
-
resolve(dirname(snapshot.locator), library.source.path),
|
|
189
|
-
);
|
|
190
|
-
} catch {
|
|
191
|
-
// Composite status below retains the owner and exposes Library Repair.
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
await this.#updateWorkspaceStatus(session);
|
|
208
|
+
await this.#openLinkedPackages(snapshot);
|
|
209
|
+
await this.#updateWorkspaceStatus(session, { reuseRemoteLibraries: false });
|
|
195
210
|
this.#emit({ locator: session.locator, snapshot, type: "package-opened" });
|
|
196
211
|
return this.describe(session.locator);
|
|
197
212
|
}
|
|
@@ -219,7 +234,7 @@ export class LocalWorkspaceSession {
|
|
|
219
234
|
|
|
220
235
|
async refresh(locator) {
|
|
221
236
|
const session = this.#required(locator);
|
|
222
|
-
await this.#updateWorkspaceStatus(session);
|
|
237
|
+
await this.#updateWorkspaceStatus(session, { reuseRemoteLibraries: false });
|
|
223
238
|
return this.describe(session.locator);
|
|
224
239
|
}
|
|
225
240
|
|
|
@@ -244,7 +259,11 @@ export class LocalWorkspaceSession {
|
|
|
244
259
|
async commit(locator, batch) {
|
|
245
260
|
const session = this.#required(locator);
|
|
246
261
|
this.#assertWorkspaceWritable(session);
|
|
262
|
+
const before = session.backend.snapshot;
|
|
247
263
|
const result = await session.backend.commit(batch);
|
|
264
|
+
const followed = await this.#followVariants(session, before);
|
|
265
|
+
if (followed.length > 0) result.followedVariants = followed;
|
|
266
|
+
await this.#openLinkedPackages(session.backend.snapshot);
|
|
248
267
|
await this.#updateWorkspaceStatus(session);
|
|
249
268
|
session.viewState = reconcilePackageViewState(
|
|
250
269
|
session.backend.snapshot,
|
|
@@ -256,32 +275,41 @@ export class LocalWorkspaceSession {
|
|
|
256
275
|
result,
|
|
257
276
|
type: "package-committed",
|
|
258
277
|
});
|
|
278
|
+
this.#emitDependencyChanges(session, { type: "package-committed" });
|
|
259
279
|
return result;
|
|
260
280
|
}
|
|
261
281
|
|
|
262
282
|
async undo(locator) {
|
|
263
283
|
const session = this.#required(locator);
|
|
264
284
|
this.#assertWorkspaceWritable(session);
|
|
285
|
+
const before = session.backend.snapshot;
|
|
265
286
|
const result = await session.backend.undo();
|
|
287
|
+
const followed = await this.#followVariants(session, before);
|
|
288
|
+
if (followed.length > 0) result.followedVariants = followed;
|
|
266
289
|
await this.#updateWorkspaceStatus(session);
|
|
267
290
|
session.viewState = reconcilePackageViewState(
|
|
268
291
|
session.backend.snapshot,
|
|
269
292
|
session.viewState,
|
|
270
293
|
);
|
|
271
294
|
this.#emit({ locator: session.locator, result, type: "package-undone" });
|
|
295
|
+
this.#emitDependencyChanges(session, { type: "package-undone" });
|
|
272
296
|
return result;
|
|
273
297
|
}
|
|
274
298
|
|
|
275
299
|
async redo(locator) {
|
|
276
300
|
const session = this.#required(locator);
|
|
277
301
|
this.#assertWorkspaceWritable(session);
|
|
302
|
+
const before = session.backend.snapshot;
|
|
278
303
|
const result = await session.backend.redo();
|
|
304
|
+
const followed = await this.#followVariants(session, before);
|
|
305
|
+
if (followed.length > 0) result.followedVariants = followed;
|
|
279
306
|
await this.#updateWorkspaceStatus(session);
|
|
280
307
|
session.viewState = reconcilePackageViewState(
|
|
281
308
|
session.backend.snapshot,
|
|
282
309
|
session.viewState,
|
|
283
310
|
);
|
|
284
311
|
this.#emit({ locator: session.locator, result, type: "package-redone" });
|
|
312
|
+
this.#emitDependencyChanges(session, { type: "package-redone" });
|
|
285
313
|
return result;
|
|
286
314
|
}
|
|
287
315
|
|
|
@@ -330,6 +358,8 @@ export class LocalWorkspaceSession {
|
|
|
330
358
|
|
|
331
359
|
async close(locator) {
|
|
332
360
|
const session = this.#required(locator);
|
|
361
|
+
clearTimeout(session.repairRetry);
|
|
362
|
+
session.repairRetry = undefined;
|
|
333
363
|
session.unsubscribe?.();
|
|
334
364
|
await session.backend.close();
|
|
335
365
|
this.#packages.delete(session.locator);
|
|
@@ -339,8 +369,14 @@ export class LocalWorkspaceSession {
|
|
|
339
369
|
this.#emit({ locator: session.locator, type: "package-closed" });
|
|
340
370
|
}
|
|
341
371
|
|
|
372
|
+
// An open still in flight can register a Package (or its Foundation and
|
|
373
|
+
// Libraries) after the loop has passed; its watchers would keep the
|
|
374
|
+
// process alive, so close until no open is pending.
|
|
342
375
|
async closeAll() {
|
|
343
|
-
|
|
376
|
+
while (this.#packages.size > 0 || this.#opening.size > 0) {
|
|
377
|
+
for (const locator of [...this.#packages.keys()]) await this.close(locator);
|
|
378
|
+
await Promise.allSettled([...this.#opening]);
|
|
379
|
+
}
|
|
344
380
|
}
|
|
345
381
|
|
|
346
382
|
#find(locator) {
|
|
@@ -363,37 +399,178 @@ export class LocalWorkspaceSession {
|
|
|
363
399
|
return session;
|
|
364
400
|
}
|
|
365
401
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
402
|
+
// Opens the Foundation and local Libraries a Package links to, so their
|
|
403
|
+
// external edits reach it. Links that cannot be opened stay in Repair and
|
|
404
|
+
// are retried while the owner's status is not ready. The active Package
|
|
405
|
+
// never changes here.
|
|
406
|
+
async #openLinkedPackages(snapshot) {
|
|
407
|
+
const locators = [];
|
|
408
|
+
if (snapshot.manifest.role === "product") {
|
|
409
|
+
const dependency = snapshot.manifest.dependencies[0];
|
|
410
|
+
locators.push(join(dirname(snapshot.locator), dependency.path));
|
|
411
|
+
}
|
|
412
|
+
// Resolution never follows more declarations than this, so retrying
|
|
413
|
+
// the rest would only spend CPU on every Repair retry.
|
|
414
|
+
for (const library of (snapshot.manifest.libraries ?? []).slice(
|
|
415
|
+
0,
|
|
416
|
+
MAX_LIBRARY_DECLARATIONS,
|
|
417
|
+
)) {
|
|
418
|
+
if (library.source.type !== "local") continue;
|
|
419
|
+
try {
|
|
420
|
+
locators.push(localLibraryLocator(snapshot.locator, library.source));
|
|
421
|
+
} catch {
|
|
422
|
+
// Composite status exposes the invalid Library source for Repair.
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
for (const locator of locators) {
|
|
426
|
+
if (this.#find(locator)) continue;
|
|
427
|
+
const active = this.activeLocator;
|
|
428
|
+
try {
|
|
429
|
+
await this.open(locator);
|
|
430
|
+
} catch {
|
|
431
|
+
// Composite status retains the owner and exposes the Repair.
|
|
432
|
+
} finally {
|
|
433
|
+
this.activeLocator = active ?? this.activeLocator;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// A write renamed variant values in a Package other open Packages use:
|
|
439
|
+
// each of those re-points its Instances to the same variants in a batch
|
|
440
|
+
// of its own (its own undo entry), so it never turns stale for a rename.
|
|
441
|
+
// Instances of deleted variants stay for Repair. Returns what followed.
|
|
442
|
+
async #followVariants(session, before) {
|
|
443
|
+
const after = session.backend.snapshot;
|
|
444
|
+
if (!before || !after || before === after) return [];
|
|
445
|
+
const followed = [];
|
|
446
|
+
for (const dependent of this.#dependentSessions(session)) {
|
|
447
|
+
const snapshot = dependent.backend.snapshot;
|
|
448
|
+
if (!snapshot) continue;
|
|
449
|
+
const operations = followedVariantOperations(before, after, snapshot);
|
|
450
|
+
if (operations.length === 0) continue;
|
|
451
|
+
try {
|
|
452
|
+
const result = await dependent.backend.commit({
|
|
453
|
+
baseRevision: snapshot.revision,
|
|
454
|
+
batchId: `follow_variants_${randomUUID()}`,
|
|
455
|
+
operations,
|
|
456
|
+
});
|
|
457
|
+
followed.push({
|
|
458
|
+
batchId: result.batchId,
|
|
459
|
+
instances: operations.length,
|
|
460
|
+
locator: dependent.locator,
|
|
461
|
+
revision: result.revision,
|
|
462
|
+
});
|
|
463
|
+
await this.#updateWorkspaceStatus(dependent);
|
|
464
|
+
this.#emit({
|
|
465
|
+
change: dependent.backend.changes.at(-1),
|
|
466
|
+
locator: dependent.locator,
|
|
467
|
+
result,
|
|
468
|
+
type: "package-committed",
|
|
387
469
|
});
|
|
470
|
+
} catch (error) {
|
|
471
|
+
// The dependent stays as it was; its Instances show in Repair.
|
|
472
|
+
followed.push({
|
|
473
|
+
error: { code: error?.code ?? "follow_variants_failed", message: error?.message },
|
|
474
|
+
instances: operations.length,
|
|
475
|
+
locator: dependent.locator,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return followed;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Every open Package whose composite Workspace includes the changed one,
|
|
483
|
+
// directly or through a Foundation or Library chain.
|
|
484
|
+
#dependentSessions(changed) {
|
|
485
|
+
const dependents = [];
|
|
486
|
+
const seen = new Set([changed]);
|
|
487
|
+
const pending = [changed];
|
|
488
|
+
while (pending.length > 0) {
|
|
489
|
+
const packageId = pending.shift().backend.snapshot?.manifest.packageId;
|
|
490
|
+
if (!packageId) continue;
|
|
491
|
+
for (const session of this.#packages.values()) {
|
|
492
|
+
if (seen.has(session)) continue;
|
|
493
|
+
const manifest = session.backend.snapshot?.manifest;
|
|
494
|
+
if (
|
|
495
|
+
manifest?.dependencies?.some(
|
|
496
|
+
(dependency) => dependency.packageId === packageId,
|
|
497
|
+
) ||
|
|
498
|
+
manifest?.libraries?.some((library) => library.packageId === packageId)
|
|
499
|
+
) {
|
|
500
|
+
seen.add(session);
|
|
501
|
+
dependents.push(session);
|
|
502
|
+
pending.push(session);
|
|
503
|
+
}
|
|
388
504
|
}
|
|
389
505
|
}
|
|
506
|
+
return dependents;
|
|
390
507
|
}
|
|
391
508
|
|
|
392
|
-
|
|
509
|
+
#emitDependencyChanges(changed, event) {
|
|
510
|
+
for (const session of this.#dependentSessions(changed)) {
|
|
511
|
+
void this.#updateWorkspaceStatus(session).then(() => {
|
|
512
|
+
this.#emit({
|
|
513
|
+
dependencyEvent: event.type,
|
|
514
|
+
dependencyLocator: changed.locator,
|
|
515
|
+
locator: session.locator,
|
|
516
|
+
status: structuredClone(session.workspaceStatus),
|
|
517
|
+
type: "package-dependency-changed",
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
#scheduleLinkRepairRetry(session) {
|
|
524
|
+
clearTimeout(session.repairRetry);
|
|
525
|
+
session.repairRetry = undefined;
|
|
526
|
+
// A Package whose own state is invalid recovers through its Backend.
|
|
527
|
+
if (
|
|
528
|
+
session.workspaceStatus.state !== "repair" ||
|
|
529
|
+
session.backend.status.state !== "ready"
|
|
530
|
+
) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
session.repairRetry = setTimeout(async () => {
|
|
534
|
+
session.repairRetry = undefined;
|
|
535
|
+
if (this.#packages.get(session.locator) !== session) return;
|
|
536
|
+
const before = JSON.stringify(session.workspaceStatus);
|
|
537
|
+
await this.#openLinkedPackages(session.backend.snapshot);
|
|
538
|
+
if (this.#packages.get(session.locator) !== session) return;
|
|
539
|
+
await this.#updateWorkspaceStatus(session, { emitUnchanged: false });
|
|
540
|
+
if (JSON.stringify(session.workspaceStatus) === before) return;
|
|
541
|
+
this.#emit({
|
|
542
|
+
dependencyEvent: "link-repair-retry",
|
|
543
|
+
locator: session.locator,
|
|
544
|
+
status: structuredClone(session.workspaceStatus),
|
|
545
|
+
type: "package-dependency-changed",
|
|
546
|
+
});
|
|
547
|
+
}, LINK_REPAIR_RETRY_MS);
|
|
548
|
+
session.repairRetry.unref?.();
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async #updateWorkspaceStatus(
|
|
552
|
+
session,
|
|
553
|
+
{ emitUnchanged = true, reuseRemoteLibraries = true } = {},
|
|
554
|
+
) {
|
|
555
|
+
// Updates run concurrently (events do not await them); only the latest
|
|
556
|
+
// one started may set the status, so a slow older read never wins.
|
|
557
|
+
session.statusSequence += 1;
|
|
558
|
+
const sequence = session.statusSequence;
|
|
559
|
+
const before = JSON.stringify(session.workspaceStatus);
|
|
560
|
+
let resolution;
|
|
561
|
+
let failure;
|
|
393
562
|
try {
|
|
394
|
-
|
|
563
|
+
resolution = await resolveWorkspace(session.locator, {
|
|
564
|
+
fetchImpl: this.#fetchImpl,
|
|
395
565
|
libraryCacheRoot: this.#libraryCacheRoot,
|
|
566
|
+
remoteLibrarySnapshots: this.#remoteLibraries,
|
|
567
|
+
reuseRemoteLibrarySnapshots: reuseRemoteLibraries,
|
|
396
568
|
});
|
|
569
|
+
} catch (error) {
|
|
570
|
+
failure = error;
|
|
571
|
+
}
|
|
572
|
+
if (sequence !== session.statusSequence) return session.workspaceStatus;
|
|
573
|
+
if (resolution) {
|
|
397
574
|
if (resolution.status === "ready") {
|
|
398
575
|
session.lastValidWorkspace = resolution.workspace;
|
|
399
576
|
session.repairWorkspace = undefined;
|
|
@@ -429,7 +606,8 @@ export class LocalWorkspaceSession {
|
|
|
429
606
|
state: "repair",
|
|
430
607
|
};
|
|
431
608
|
}
|
|
432
|
-
}
|
|
609
|
+
} else {
|
|
610
|
+
const error = failure;
|
|
433
611
|
session.workspaceStatus = {
|
|
434
612
|
error: {
|
|
435
613
|
code: error?.code ?? "invalid_workspace_state",
|
|
@@ -444,11 +622,14 @@ export class LocalWorkspaceSession {
|
|
|
444
622
|
state: "repair",
|
|
445
623
|
};
|
|
446
624
|
}
|
|
447
|
-
this.#
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
625
|
+
this.#scheduleLinkRepairRetry(session);
|
|
626
|
+
if (emitUnchanged || JSON.stringify(session.workspaceStatus) !== before) {
|
|
627
|
+
this.#emit({
|
|
628
|
+
locator: session.locator,
|
|
629
|
+
status: structuredClone(session.workspaceStatus),
|
|
630
|
+
type: "package-workspace-status",
|
|
631
|
+
});
|
|
632
|
+
}
|
|
452
633
|
return session.workspaceStatus;
|
|
453
634
|
}
|
|
454
635
|
|
|
@@ -467,6 +648,7 @@ export class LocalWorkspaceSession {
|
|
|
467
648
|
type,
|
|
468
649
|
value,
|
|
469
650
|
});
|
|
651
|
+
this.#emitDependencyChanges(session, { type });
|
|
470
652
|
return value;
|
|
471
653
|
}
|
|
472
654
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// `smallpen migrate-themes`: ADR 0003 upgrade by copy. The original Package
|
|
2
|
+
// (or Foundation + Product pair) stays untouched; the converted copy is
|
|
3
|
+
// written beside it in one rename.
|
|
4
|
+
import { cp, mkdir, mkdtemp, rename, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
canonicalJSON,
|
|
9
|
+
migrateTokenThemes,
|
|
10
|
+
SmallPenError,
|
|
11
|
+
} from "@smallpen/core";
|
|
12
|
+
|
|
13
|
+
import { pathExists } from "./fs-utils.mjs";
|
|
14
|
+
import { openWorkspace } from "./workspace.mjs";
|
|
15
|
+
|
|
16
|
+
async function writePackage(packagePath, { deletedEntries, entries, manifest }) {
|
|
17
|
+
await writeFile(join(packagePath, "manifest.json"), canonicalJSON(manifest), "utf8");
|
|
18
|
+
for (const [entry, value] of Object.entries(entries)) {
|
|
19
|
+
const output = join(packagePath, entry);
|
|
20
|
+
await mkdir(dirname(output), { recursive: true });
|
|
21
|
+
await writeFile(output, canonicalJSON(value), "utf8");
|
|
22
|
+
}
|
|
23
|
+
for (const entry of deletedEntries) {
|
|
24
|
+
if (Object.hasOwn(entries, entry)) continue;
|
|
25
|
+
await rm(join(packagePath, entry), { force: true });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function migrateThemesByCopy(locator, outputLocator) {
|
|
30
|
+
if (typeof outputLocator !== "string" || outputLocator.length === 0) {
|
|
31
|
+
throw new SmallPenError(
|
|
32
|
+
"missing_migration_output",
|
|
33
|
+
"migrate-themes requires --output NEW_PATH; the original is never changed",
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const workspace = await openWorkspace(locator);
|
|
37
|
+
const { foundation, product } = workspace;
|
|
38
|
+
const pair = Boolean(foundation) && foundation !== product;
|
|
39
|
+
const outputPath = resolve(outputLocator);
|
|
40
|
+
if (!pair && !outputPath.toLowerCase().endsWith(".smallpen")) {
|
|
41
|
+
throw new SmallPenError(
|
|
42
|
+
"invalid_package_path",
|
|
43
|
+
"--output must be a new <name>.smallpen path for a single Package",
|
|
44
|
+
{ outputPath },
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (await pathExists(outputPath)) {
|
|
48
|
+
throw new SmallPenError(
|
|
49
|
+
"migration_output_exists",
|
|
50
|
+
`Migration output already exists: ${outputPath}`,
|
|
51
|
+
{ outputPath },
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const result = migrateTokenThemes({ foundation, product });
|
|
55
|
+
const parent = dirname(outputPath);
|
|
56
|
+
await mkdir(parent, { recursive: true });
|
|
57
|
+
const transactionPath = await mkdtemp(join(parent, ".smallpen-migrate-"));
|
|
58
|
+
const candidatePath = join(transactionPath, basename(outputPath));
|
|
59
|
+
const dependency = pair
|
|
60
|
+
? product.manifest.dependencies.find(
|
|
61
|
+
({ packageId }) => packageId === foundation.manifest.packageId,
|
|
62
|
+
)
|
|
63
|
+
: undefined;
|
|
64
|
+
// Package directory of each migrated Package, relative to candidatePath.
|
|
65
|
+
const targets = new Map(
|
|
66
|
+
pair
|
|
67
|
+
? [
|
|
68
|
+
[foundation.manifest.packageId, dependency.path],
|
|
69
|
+
[product.manifest.packageId, basename(product.locator)],
|
|
70
|
+
]
|
|
71
|
+
: [[product.manifest.packageId, "."]],
|
|
72
|
+
);
|
|
73
|
+
const sources = new Map(
|
|
74
|
+
[foundation, product].filter(Boolean).map((snapshot) => [
|
|
75
|
+
snapshot.manifest.packageId,
|
|
76
|
+
snapshot.locator,
|
|
77
|
+
]),
|
|
78
|
+
);
|
|
79
|
+
try {
|
|
80
|
+
for (const migrated of result.packages) {
|
|
81
|
+
const target = join(candidatePath, targets.get(migrated.packageId));
|
|
82
|
+
// Copy everything first so blobs and other files travel along.
|
|
83
|
+
await cp(sources.get(migrated.packageId), target, { recursive: true });
|
|
84
|
+
await writePackage(target, migrated);
|
|
85
|
+
}
|
|
86
|
+
const productTarget = join(candidatePath, targets.get(product.manifest.packageId));
|
|
87
|
+
const opened = await openWorkspace(productTarget);
|
|
88
|
+
await rename(candidatePath, outputPath);
|
|
89
|
+
await rm(transactionPath, { force: true, recursive: true });
|
|
90
|
+
const finalPath = (packageId) => join(outputPath, targets.get(packageId));
|
|
91
|
+
return {
|
|
92
|
+
original: pair
|
|
93
|
+
? { foundationPath: foundation.locator, productPath: product.locator }
|
|
94
|
+
: { packagePath: product.locator },
|
|
95
|
+
output: pair
|
|
96
|
+
? {
|
|
97
|
+
foundationPath: finalPath(foundation.manifest.packageId),
|
|
98
|
+
productPath: finalPath(product.manifest.packageId),
|
|
99
|
+
workspacePath: outputPath,
|
|
100
|
+
}
|
|
101
|
+
: { packagePath: outputPath },
|
|
102
|
+
packages: [opened.foundation, opened.product]
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.map((snapshot) => ({
|
|
105
|
+
packageId: snapshot.manifest.packageId,
|
|
106
|
+
revision: snapshot.revision,
|
|
107
|
+
})),
|
|
108
|
+
status: "migrated",
|
|
109
|
+
themes: result.themes,
|
|
110
|
+
warnings: result.warnings,
|
|
111
|
+
};
|
|
112
|
+
} catch (error) {
|
|
113
|
+
await rm(transactionPath, { force: true, recursive: true });
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|