@smallpen/local-package 0.1.0-alpha.2 → 0.1.0-alpha.5
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 +2 -2
- 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 +6 -1
- package/src/initialize.mjs +1 -11
- 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 +532 -176
- package/src/local-workspace-session.mjs +223 -95
- package/src/remote-library.mjs +167 -28
- package/src/render.mjs +342 -94
- package/src/webp-decode.mjs +32 -0
- package/src/workspace.mjs +85 -15
|
@@ -3,7 +3,15 @@ import { dirname, join, resolve } from "node:path";
|
|
|
3
3
|
import { SmallPenError } from "@smallpen/core";
|
|
4
4
|
|
|
5
5
|
import { LocalPackageBackend } from "./local-package-backend.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
localLibraryLocator,
|
|
8
|
+
MAX_LIBRARY_DECLARATIONS,
|
|
9
|
+
resolveWorkspace,
|
|
10
|
+
} from "./workspace.mjs";
|
|
11
|
+
|
|
12
|
+
// A Package in Repair because a linked Foundation or Library is unavailable
|
|
13
|
+
// gets no event when that link is fixed, so its status is resolved again.
|
|
14
|
+
const LINK_REPAIR_RETRY_MS = 1000;
|
|
7
15
|
|
|
8
16
|
function collectStableIds(value, result) {
|
|
9
17
|
if (Array.isArray(value)) {
|
|
@@ -49,22 +57,6 @@ function defaultTokenLibrary() {
|
|
|
49
57
|
};
|
|
50
58
|
}
|
|
51
59
|
|
|
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
60
|
export function reconcilePackageViewState(snapshot, value = {}) {
|
|
69
61
|
const firstScreenEntry = snapshot.manifest.entries.screens[0];
|
|
70
62
|
const defaultScreen =
|
|
@@ -94,13 +86,21 @@ export function reconcilePackageViewState(snapshot, value = {}) {
|
|
|
94
86
|
}
|
|
95
87
|
|
|
96
88
|
export class LocalWorkspaceSession {
|
|
89
|
+
#fetchImpl;
|
|
97
90
|
#libraryCacheRoot;
|
|
98
91
|
#listeners = new Set();
|
|
92
|
+
// Opens still running: requests, dependency events and Repair retries.
|
|
93
|
+
#opening = new Set();
|
|
99
94
|
#packages = new Map();
|
|
95
|
+
// Remote Library snapshots verified by this session, keyed by manifest URL.
|
|
96
|
+
// Status updates after commits reuse them so a slow or unreachable remote
|
|
97
|
+
// never blocks a local write; open() and refresh() read them again.
|
|
98
|
+
#remoteLibraries = new Map();
|
|
100
99
|
|
|
101
100
|
activeLocator;
|
|
102
101
|
|
|
103
|
-
constructor({ libraryCacheRoot } = {}) {
|
|
102
|
+
constructor({ fetchImpl, libraryCacheRoot } = {}) {
|
|
103
|
+
this.#fetchImpl = fetchImpl;
|
|
104
104
|
this.#libraryCacheRoot = libraryCacheRoot;
|
|
105
105
|
}
|
|
106
106
|
|
|
@@ -120,40 +120,67 @@ export class LocalWorkspaceSession {
|
|
|
120
120
|
}));
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
open(locator, viewState = {}) {
|
|
124
|
+
const opening = this.#open(locator, viewState);
|
|
125
|
+
this.#opening.add(opening);
|
|
126
|
+
const settled = () => this.#opening.delete(opening);
|
|
127
|
+
opening.then(settled, settled);
|
|
128
|
+
return opening;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async #open(locator, viewState) {
|
|
124
132
|
const requested = resolve(locator);
|
|
125
133
|
const existing = this.#find(requested);
|
|
126
134
|
if (existing) {
|
|
127
135
|
this.activeLocator = existing.locator;
|
|
128
136
|
return this.describe(existing.locator);
|
|
129
137
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
138
|
+
// Opening never writes: a Package without Tokens is served with the
|
|
139
|
+
// default Theme projected in memory and persisted by its first commit.
|
|
140
|
+
const backend = new LocalPackageBackend({
|
|
141
|
+
defaultTokenLibrary: defaultTokenLibrary(),
|
|
142
|
+
});
|
|
143
|
+
let session;
|
|
144
|
+
let opened;
|
|
145
|
+
try {
|
|
146
|
+
const snapshot = await backend.open(requested);
|
|
147
|
+
// A symlinked or non-canonical spelling of an open Package, or a
|
|
148
|
+
// concurrent open of the same one, selects the session already open.
|
|
149
|
+
opened = this.#packages.get(snapshot.locator);
|
|
150
|
+
if (!opened) {
|
|
151
|
+
const duplicate = [...this.#packages.values()].find(
|
|
152
|
+
(candidate) =>
|
|
153
|
+
candidate.backend.snapshot.runtime.file === snapshot.runtime.file,
|
|
154
|
+
);
|
|
155
|
+
if (duplicate) {
|
|
156
|
+
throw new SmallPenError(
|
|
157
|
+
"duplicate_file_id",
|
|
158
|
+
"Another open Package has the same persistent file identity",
|
|
159
|
+
{
|
|
160
|
+
fileId: snapshot.runtime.file,
|
|
161
|
+
locator: snapshot.locator,
|
|
162
|
+
openLocator: duplicate.locator,
|
|
163
|
+
packageId: snapshot.manifest.packageId,
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
session = {
|
|
168
|
+
backend,
|
|
143
169
|
locator: snapshot.locator,
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
170
|
+
unsubscribe: undefined,
|
|
171
|
+
statusSequence: 0,
|
|
172
|
+
viewState: reconcilePackageViewState(snapshot, viewState),
|
|
173
|
+
workspaceStatus: structuredClone(backend.status),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
} finally {
|
|
177
|
+
if (!session) await backend.close();
|
|
148
178
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
viewState: reconcilePackageViewState(snapshot, viewState),
|
|
155
|
-
workspaceStatus: structuredClone(backend.status),
|
|
156
|
-
};
|
|
179
|
+
if (opened) {
|
|
180
|
+
this.activeLocator = opened.locator;
|
|
181
|
+
return this.describe(opened.locator);
|
|
182
|
+
}
|
|
183
|
+
const { snapshot } = backend;
|
|
157
184
|
session.unsubscribe = backend.subscribe((event) => {
|
|
158
185
|
if (event.snapshot) {
|
|
159
186
|
session.viewState = reconcilePackageViewState(
|
|
@@ -161,7 +188,11 @@ export class LocalWorkspaceSession {
|
|
|
161
188
|
session.viewState,
|
|
162
189
|
);
|
|
163
190
|
}
|
|
164
|
-
|
|
191
|
+
// An external edit can retarget the Foundation or a Library.
|
|
192
|
+
const linked = event.snapshot
|
|
193
|
+
? this.#openLinkedPackages(event.snapshot)
|
|
194
|
+
: Promise.resolve();
|
|
195
|
+
void linked.then(() => this.#updateWorkspaceStatus(session));
|
|
165
196
|
this.#emit({ ...event, locator: session.locator });
|
|
166
197
|
if (
|
|
167
198
|
event.type === "external-revision" ||
|
|
@@ -173,25 +204,8 @@ export class LocalWorkspaceSession {
|
|
|
173
204
|
});
|
|
174
205
|
this.#packages.set(session.locator, session);
|
|
175
206
|
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);
|
|
207
|
+
await this.#openLinkedPackages(snapshot);
|
|
208
|
+
await this.#updateWorkspaceStatus(session, { reuseRemoteLibraries: false });
|
|
195
209
|
this.#emit({ locator: session.locator, snapshot, type: "package-opened" });
|
|
196
210
|
return this.describe(session.locator);
|
|
197
211
|
}
|
|
@@ -219,7 +233,7 @@ export class LocalWorkspaceSession {
|
|
|
219
233
|
|
|
220
234
|
async refresh(locator) {
|
|
221
235
|
const session = this.#required(locator);
|
|
222
|
-
await this.#updateWorkspaceStatus(session);
|
|
236
|
+
await this.#updateWorkspaceStatus(session, { reuseRemoteLibraries: false });
|
|
223
237
|
return this.describe(session.locator);
|
|
224
238
|
}
|
|
225
239
|
|
|
@@ -245,6 +259,7 @@ export class LocalWorkspaceSession {
|
|
|
245
259
|
const session = this.#required(locator);
|
|
246
260
|
this.#assertWorkspaceWritable(session);
|
|
247
261
|
const result = await session.backend.commit(batch);
|
|
262
|
+
await this.#openLinkedPackages(session.backend.snapshot);
|
|
248
263
|
await this.#updateWorkspaceStatus(session);
|
|
249
264
|
session.viewState = reconcilePackageViewState(
|
|
250
265
|
session.backend.snapshot,
|
|
@@ -256,6 +271,7 @@ export class LocalWorkspaceSession {
|
|
|
256
271
|
result,
|
|
257
272
|
type: "package-committed",
|
|
258
273
|
});
|
|
274
|
+
this.#emitDependencyChanges(session, { type: "package-committed" });
|
|
259
275
|
return result;
|
|
260
276
|
}
|
|
261
277
|
|
|
@@ -269,6 +285,7 @@ export class LocalWorkspaceSession {
|
|
|
269
285
|
session.viewState,
|
|
270
286
|
);
|
|
271
287
|
this.#emit({ locator: session.locator, result, type: "package-undone" });
|
|
288
|
+
this.#emitDependencyChanges(session, { type: "package-undone" });
|
|
272
289
|
return result;
|
|
273
290
|
}
|
|
274
291
|
|
|
@@ -282,6 +299,7 @@ export class LocalWorkspaceSession {
|
|
|
282
299
|
session.viewState,
|
|
283
300
|
);
|
|
284
301
|
this.#emit({ locator: session.locator, result, type: "package-redone" });
|
|
302
|
+
this.#emitDependencyChanges(session, { type: "package-redone" });
|
|
285
303
|
return result;
|
|
286
304
|
}
|
|
287
305
|
|
|
@@ -330,6 +348,8 @@ export class LocalWorkspaceSession {
|
|
|
330
348
|
|
|
331
349
|
async close(locator) {
|
|
332
350
|
const session = this.#required(locator);
|
|
351
|
+
clearTimeout(session.repairRetry);
|
|
352
|
+
session.repairRetry = undefined;
|
|
333
353
|
session.unsubscribe?.();
|
|
334
354
|
await session.backend.close();
|
|
335
355
|
this.#packages.delete(session.locator);
|
|
@@ -339,8 +359,14 @@ export class LocalWorkspaceSession {
|
|
|
339
359
|
this.#emit({ locator: session.locator, type: "package-closed" });
|
|
340
360
|
}
|
|
341
361
|
|
|
362
|
+
// An open still in flight can register a Package (or its Foundation and
|
|
363
|
+
// Libraries) after the loop has passed; its watchers would keep the
|
|
364
|
+
// process alive, so close until no open is pending.
|
|
342
365
|
async closeAll() {
|
|
343
|
-
|
|
366
|
+
while (this.#packages.size > 0 || this.#opening.size > 0) {
|
|
367
|
+
for (const locator of [...this.#packages.keys()]) await this.close(locator);
|
|
368
|
+
await Promise.allSettled([...this.#opening]);
|
|
369
|
+
}
|
|
344
370
|
}
|
|
345
371
|
|
|
346
372
|
#find(locator) {
|
|
@@ -363,37 +389,134 @@ export class LocalWorkspaceSession {
|
|
|
363
389
|
return session;
|
|
364
390
|
}
|
|
365
391
|
|
|
392
|
+
// Opens the Foundation and local Libraries a Package links to, so their
|
|
393
|
+
// external edits reach it. Links that cannot be opened stay in Repair and
|
|
394
|
+
// are retried while the owner's status is not ready. The active Package
|
|
395
|
+
// never changes here.
|
|
396
|
+
async #openLinkedPackages(snapshot) {
|
|
397
|
+
const locators = [];
|
|
398
|
+
if (snapshot.manifest.role === "product") {
|
|
399
|
+
const dependency = snapshot.manifest.dependencies[0];
|
|
400
|
+
locators.push(join(dirname(snapshot.locator), dependency.path));
|
|
401
|
+
}
|
|
402
|
+
// Resolution never follows more declarations than this, so retrying
|
|
403
|
+
// the rest would only spend CPU on every Repair retry.
|
|
404
|
+
for (const library of (snapshot.manifest.libraries ?? []).slice(
|
|
405
|
+
0,
|
|
406
|
+
MAX_LIBRARY_DECLARATIONS,
|
|
407
|
+
)) {
|
|
408
|
+
if (library.source.type !== "local") continue;
|
|
409
|
+
try {
|
|
410
|
+
locators.push(localLibraryLocator(snapshot.locator, library.source));
|
|
411
|
+
} catch {
|
|
412
|
+
// Composite status exposes the invalid Library source for Repair.
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
for (const locator of locators) {
|
|
416
|
+
if (this.#find(locator)) continue;
|
|
417
|
+
const active = this.activeLocator;
|
|
418
|
+
try {
|
|
419
|
+
await this.open(locator);
|
|
420
|
+
} catch {
|
|
421
|
+
// Composite status retains the owner and exposes the Repair.
|
|
422
|
+
} finally {
|
|
423
|
+
this.activeLocator = active ?? this.activeLocator;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Every open Package whose composite Workspace includes the changed one,
|
|
429
|
+
// directly or through a Foundation or Library chain.
|
|
430
|
+
#dependentSessions(changed) {
|
|
431
|
+
const dependents = [];
|
|
432
|
+
const seen = new Set([changed]);
|
|
433
|
+
const pending = [changed];
|
|
434
|
+
while (pending.length > 0) {
|
|
435
|
+
const packageId = pending.shift().backend.snapshot?.manifest.packageId;
|
|
436
|
+
if (!packageId) continue;
|
|
437
|
+
for (const session of this.#packages.values()) {
|
|
438
|
+
if (seen.has(session)) continue;
|
|
439
|
+
const manifest = session.backend.snapshot?.manifest;
|
|
440
|
+
if (
|
|
441
|
+
manifest?.dependencies?.some(
|
|
442
|
+
(dependency) => dependency.packageId === packageId,
|
|
443
|
+
) ||
|
|
444
|
+
manifest?.libraries?.some((library) => library.packageId === packageId)
|
|
445
|
+
) {
|
|
446
|
+
seen.add(session);
|
|
447
|
+
dependents.push(session);
|
|
448
|
+
pending.push(session);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return dependents;
|
|
453
|
+
}
|
|
454
|
+
|
|
366
455
|
#emitDependencyChanges(changed, event) {
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
(
|
|
374
|
-
|
|
375
|
-
session.backend.snapshot.manifest.libraries?.some(
|
|
376
|
-
(library) => library.packageId === packageId,
|
|
377
|
-
)
|
|
378
|
-
) {
|
|
379
|
-
void this.#updateWorkspaceStatus(session).then(() => {
|
|
380
|
-
this.#emit({
|
|
381
|
-
dependencyEvent: event.type,
|
|
382
|
-
dependencyLocator: changed.locator,
|
|
383
|
-
locator: session.locator,
|
|
384
|
-
status: structuredClone(session.workspaceStatus),
|
|
385
|
-
type: "package-dependency-changed",
|
|
386
|
-
});
|
|
456
|
+
for (const session of this.#dependentSessions(changed)) {
|
|
457
|
+
void this.#updateWorkspaceStatus(session).then(() => {
|
|
458
|
+
this.#emit({
|
|
459
|
+
dependencyEvent: event.type,
|
|
460
|
+
dependencyLocator: changed.locator,
|
|
461
|
+
locator: session.locator,
|
|
462
|
+
status: structuredClone(session.workspaceStatus),
|
|
463
|
+
type: "package-dependency-changed",
|
|
387
464
|
});
|
|
388
|
-
}
|
|
465
|
+
});
|
|
389
466
|
}
|
|
390
467
|
}
|
|
391
468
|
|
|
392
|
-
|
|
469
|
+
#scheduleLinkRepairRetry(session) {
|
|
470
|
+
clearTimeout(session.repairRetry);
|
|
471
|
+
session.repairRetry = undefined;
|
|
472
|
+
// A Package whose own state is invalid recovers through its Backend.
|
|
473
|
+
if (
|
|
474
|
+
session.workspaceStatus.state !== "repair" ||
|
|
475
|
+
session.backend.status.state !== "ready"
|
|
476
|
+
) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
session.repairRetry = setTimeout(async () => {
|
|
480
|
+
session.repairRetry = undefined;
|
|
481
|
+
if (this.#packages.get(session.locator) !== session) return;
|
|
482
|
+
const before = JSON.stringify(session.workspaceStatus);
|
|
483
|
+
await this.#openLinkedPackages(session.backend.snapshot);
|
|
484
|
+
if (this.#packages.get(session.locator) !== session) return;
|
|
485
|
+
await this.#updateWorkspaceStatus(session, { emitUnchanged: false });
|
|
486
|
+
if (JSON.stringify(session.workspaceStatus) === before) return;
|
|
487
|
+
this.#emit({
|
|
488
|
+
dependencyEvent: "link-repair-retry",
|
|
489
|
+
locator: session.locator,
|
|
490
|
+
status: structuredClone(session.workspaceStatus),
|
|
491
|
+
type: "package-dependency-changed",
|
|
492
|
+
});
|
|
493
|
+
}, LINK_REPAIR_RETRY_MS);
|
|
494
|
+
session.repairRetry.unref?.();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async #updateWorkspaceStatus(
|
|
498
|
+
session,
|
|
499
|
+
{ emitUnchanged = true, reuseRemoteLibraries = true } = {},
|
|
500
|
+
) {
|
|
501
|
+
// Updates run concurrently (events do not await them); only the latest
|
|
502
|
+
// one started may set the status, so a slow older read never wins.
|
|
503
|
+
session.statusSequence += 1;
|
|
504
|
+
const sequence = session.statusSequence;
|
|
505
|
+
const before = JSON.stringify(session.workspaceStatus);
|
|
506
|
+
let resolution;
|
|
507
|
+
let failure;
|
|
393
508
|
try {
|
|
394
|
-
|
|
509
|
+
resolution = await resolveWorkspace(session.locator, {
|
|
510
|
+
fetchImpl: this.#fetchImpl,
|
|
395
511
|
libraryCacheRoot: this.#libraryCacheRoot,
|
|
512
|
+
remoteLibrarySnapshots: this.#remoteLibraries,
|
|
513
|
+
reuseRemoteLibrarySnapshots: reuseRemoteLibraries,
|
|
396
514
|
});
|
|
515
|
+
} catch (error) {
|
|
516
|
+
failure = error;
|
|
517
|
+
}
|
|
518
|
+
if (sequence !== session.statusSequence) return session.workspaceStatus;
|
|
519
|
+
if (resolution) {
|
|
397
520
|
if (resolution.status === "ready") {
|
|
398
521
|
session.lastValidWorkspace = resolution.workspace;
|
|
399
522
|
session.repairWorkspace = undefined;
|
|
@@ -429,7 +552,8 @@ export class LocalWorkspaceSession {
|
|
|
429
552
|
state: "repair",
|
|
430
553
|
};
|
|
431
554
|
}
|
|
432
|
-
}
|
|
555
|
+
} else {
|
|
556
|
+
const error = failure;
|
|
433
557
|
session.workspaceStatus = {
|
|
434
558
|
error: {
|
|
435
559
|
code: error?.code ?? "invalid_workspace_state",
|
|
@@ -444,11 +568,14 @@ export class LocalWorkspaceSession {
|
|
|
444
568
|
state: "repair",
|
|
445
569
|
};
|
|
446
570
|
}
|
|
447
|
-
this.#
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
571
|
+
this.#scheduleLinkRepairRetry(session);
|
|
572
|
+
if (emitUnchanged || JSON.stringify(session.workspaceStatus) !== before) {
|
|
573
|
+
this.#emit({
|
|
574
|
+
locator: session.locator,
|
|
575
|
+
status: structuredClone(session.workspaceStatus),
|
|
576
|
+
type: "package-workspace-status",
|
|
577
|
+
});
|
|
578
|
+
}
|
|
452
579
|
return session.workspaceStatus;
|
|
453
580
|
}
|
|
454
581
|
|
|
@@ -467,6 +594,7 @@ export class LocalWorkspaceSession {
|
|
|
467
594
|
type,
|
|
468
595
|
value,
|
|
469
596
|
});
|
|
597
|
+
this.#emitDependencyChanges(session, { type });
|
|
470
598
|
return value;
|
|
471
599
|
}
|
|
472
600
|
|