sh3-core 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Sh3.svelte +2 -73
- package/dist/actions/ctx-actions.svelte.test.js +4 -4
- package/dist/api.d.ts +2 -0
- package/dist/api.js +1 -0
- package/dist/build.d.ts +27 -0
- package/dist/build.js +59 -1
- package/dist/build.test.d.ts +1 -0
- package/dist/build.test.js +31 -0
- package/dist/contributions/index.d.ts +1 -1
- package/dist/contributions/index.js +1 -1
- package/dist/contributions/registry.d.ts +17 -1
- package/dist/contributions/registry.js +50 -2
- package/dist/contributions/scope.test.d.ts +1 -0
- package/dist/contributions/scope.test.js +52 -0
- package/dist/contributions/types.d.ts +11 -3
- package/dist/createShell.js +7 -1
- package/dist/fields/address.d.ts +3 -0
- package/dist/fields/address.js +36 -0
- package/dist/fields/address.test.d.ts +1 -0
- package/dist/fields/address.test.js +34 -0
- package/dist/fields/decoration.d.ts +7 -0
- package/dist/fields/decoration.js +199 -0
- package/dist/fields/decoration.svelte.test.d.ts +1 -0
- package/dist/fields/decoration.svelte.test.js +177 -0
- package/dist/fields/dispatch.d.ts +22 -0
- package/dist/fields/dispatch.js +254 -0
- package/dist/fields/dispatch.test.d.ts +1 -0
- package/dist/fields/dispatch.test.js +175 -0
- package/dist/fields/types.d.ts +101 -0
- package/dist/fields/types.js +16 -0
- package/dist/fields/walker.svelte.test.d.ts +1 -0
- package/dist/fields/walker.svelte.test.js +138 -0
- package/dist/host.js +27 -2
- package/dist/host.svelte.test.d.ts +1 -0
- package/dist/host.svelte.test.js +92 -0
- package/dist/layout/slotHostPool.svelte.d.ts +8 -0
- package/dist/layout/slotHostPool.svelte.js +14 -1
- package/dist/overlays/OverlayRoots.svelte +86 -0
- package/dist/overlays/OverlayRoots.svelte.d.ts +3 -0
- package/dist/platform/tauri-backend.d.ts +3 -3
- package/dist/platform/tauri-backend.js +24 -3
- package/dist/projects/session-state.svelte.d.ts +3 -3
- package/dist/projects/session-state.svelte.js +5 -4
- package/dist/runtime/runVerb.js +2 -2
- package/dist/satellite/SatelliteShell.svelte +58 -11
- package/dist/satellite/SatelliteShell.svelte.test.d.ts +1 -0
- package/dist/satellite/SatelliteShell.svelte.test.js +61 -0
- package/dist/sh3Api/fields-walker.svelte.test.d.ts +1 -0
- package/dist/sh3Api/fields-walker.svelte.test.js +75 -0
- package/dist/sh3Api/headless.d.ts +9 -0
- package/dist/sh3Api/headless.js +163 -16
- package/dist/sh3Api/headless.svelte.test.js +9 -9
- package/dist/sh3core-shard/sh3coreShard.svelte.js +2 -2
- package/dist/shards/activate-fields.svelte.test.d.ts +1 -0
- package/dist/shards/activate-fields.svelte.test.js +121 -0
- package/dist/shards/activate-runtime.test.js +8 -8
- package/dist/shards/activate.svelte.js +29 -35
- package/dist/shards/types.d.ts +14 -75
- package/dist/shell-shard/ScrollbackView.svelte +55 -9
- package/dist/shell-shard/Terminal.svelte +1 -1
- package/dist/shell-shard/scrollback-stick.d.ts +9 -0
- package/dist/shell-shard/scrollback-stick.js +21 -0
- package/dist/shell-shard/scrollback-stick.test.d.ts +1 -0
- package/dist/shell-shard/scrollback-stick.test.js +25 -0
- package/dist/verbs/types.d.ts +56 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/Sh3.svelte
CHANGED
|
@@ -15,11 +15,7 @@
|
|
|
15
15
|
import './tokens.css';
|
|
16
16
|
import './primitives/base.css';
|
|
17
17
|
import LayoutRenderer from './layout/LayoutRenderer.svelte';
|
|
18
|
-
import
|
|
19
|
-
import FloatLayer from './overlays/FloatLayer.svelte';
|
|
20
|
-
import type { OverlayLayer } from './overlays/types';
|
|
21
|
-
import { registerLayerRoot, unregisterLayerRoot } from './overlays/roots';
|
|
22
|
-
import { bindFloatStore, unbindFloatStore } from './overlays/float';
|
|
18
|
+
import OverlayRoots from './overlays/OverlayRoots.svelte';
|
|
23
19
|
import { returnToHome, isAdmin } from './api';
|
|
24
20
|
import { getActiveRoot, layoutStore } from './layout/store.svelte';
|
|
25
21
|
import { syncMountedViewIdsFromLayout } from './actions/state.svelte';
|
|
@@ -39,41 +35,6 @@
|
|
|
39
35
|
// stays set) and only flips the layout store's activeRoot back to 'home'.
|
|
40
36
|
const onHome = $derived(getActiveRoot() === 'home');
|
|
41
37
|
|
|
42
|
-
// Layer metadata — order matches the stack in docs/design/layout.md.
|
|
43
|
-
// Index 0 here is layer 1 (floating panels); layer 0 is the content area.
|
|
44
|
-
const overlayLayers: { layer: number; name: OverlayLayer }[] = [
|
|
45
|
-
{ layer: 1, name: 'floating' },
|
|
46
|
-
{ layer: 2, name: 'drag-preview' },
|
|
47
|
-
{ layer: 3, name: 'popup' },
|
|
48
|
-
{ layer: 4, name: 'modal' },
|
|
49
|
-
{ layer: 5, name: 'toast' },
|
|
50
|
-
{ layer: 6, name: 'command' },
|
|
51
|
-
];
|
|
52
|
-
|
|
53
|
-
// Populated by bind:this during render; registered with the overlay
|
|
54
|
-
// module via $effect after mount so layer managers (sh3.modal,
|
|
55
|
-
// sh3.popup, sh3.toast) can find their target DOM roots.
|
|
56
|
-
const overlayRoots: Partial<Record<OverlayLayer, HTMLDivElement>> = $state({});
|
|
57
|
-
|
|
58
|
-
$effect(() => {
|
|
59
|
-
for (const { name } of overlayLayers) {
|
|
60
|
-
const el = overlayRoots[name];
|
|
61
|
-
if (el) registerLayerRoot(name, el);
|
|
62
|
-
}
|
|
63
|
-
return () => {
|
|
64
|
-
for (const { name } of overlayLayers) unregisterLayerRoot(name);
|
|
65
|
-
};
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
$effect(() => {
|
|
69
|
-
const tree = layoutStore.tree;
|
|
70
|
-
bindFloatStore(tree.floats, () => ({
|
|
71
|
-
w: window.innerWidth,
|
|
72
|
-
h: window.innerHeight,
|
|
73
|
-
}));
|
|
74
|
-
return () => unbindFloatStore();
|
|
75
|
-
});
|
|
76
|
-
|
|
77
38
|
// Keep the actions dispatcher's `mountedViewIds` set in sync with the
|
|
78
39
|
// live layout tree, so `view:<viewId>` scope checks (context menu,
|
|
79
40
|
// palette, keyboard) see currently-mounted views. Deep Svelte 5
|
|
@@ -144,28 +105,7 @@
|
|
|
144
105
|
<!-- alpha tag moved to Sh3Home title row -->
|
|
145
106
|
</footer>
|
|
146
107
|
|
|
147
|
-
|
|
148
|
-
Overlay roots. Each is absolutely positioned over the entire sh3 with
|
|
149
|
-
pointer-events: none by default; layer managers enable pointer events on
|
|
150
|
-
the specific surfaces they portal in.
|
|
151
|
-
-->
|
|
152
|
-
<div class="sh3-overlays" aria-hidden="true">
|
|
153
|
-
{#each overlayLayers as { layer, name } (layer)}
|
|
154
|
-
<div
|
|
155
|
-
class="sh3-overlay-root"
|
|
156
|
-
data-sh3-overlay={name}
|
|
157
|
-
data-sh3-layer={layer}
|
|
158
|
-
style="z-index: var(--sh3-z-layer-{layer});"
|
|
159
|
-
bind:this={overlayRoots[name]}
|
|
160
|
-
>
|
|
161
|
-
{#if name === 'floating'}
|
|
162
|
-
<FloatLayer />
|
|
163
|
-
{:else if name === 'drag-preview'}
|
|
164
|
-
<DragPreview />
|
|
165
|
-
{/if}
|
|
166
|
-
</div>
|
|
167
|
-
{/each}
|
|
168
|
-
</div>
|
|
108
|
+
<OverlayRoots />
|
|
169
109
|
|
|
170
110
|
<!--
|
|
171
111
|
Sh3-owned consent dialog for ctx.keys.mint().
|
|
@@ -217,17 +157,6 @@
|
|
|
217
157
|
user-select: none;
|
|
218
158
|
}
|
|
219
159
|
|
|
220
|
-
.sh3-overlays {
|
|
221
|
-
position: absolute;
|
|
222
|
-
inset: 0;
|
|
223
|
-
pointer-events: none;
|
|
224
|
-
}
|
|
225
|
-
.sh3-overlay-root {
|
|
226
|
-
position: absolute;
|
|
227
|
-
inset: 0;
|
|
228
|
-
pointer-events: none;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
160
|
.sh3-tabbar-home-button {
|
|
232
161
|
display: flex;
|
|
233
162
|
align-items: center;
|
|
@@ -35,7 +35,7 @@ describe('ShardContext.listActions / runAction (integration)', () => {
|
|
|
35
35
|
});
|
|
36
36
|
await activateShard('producer');
|
|
37
37
|
await activateShard('consumer');
|
|
38
|
-
const list = consumerCtx.listActions();
|
|
38
|
+
const list = consumerCtx.sh3.listActions();
|
|
39
39
|
const ids = list.map((d) => d.id);
|
|
40
40
|
expect(ids).toContain('producer.do');
|
|
41
41
|
const desc = list.find((d) => d.id === 'producer.do');
|
|
@@ -62,7 +62,7 @@ describe('ShardContext.listActions / runAction (integration)', () => {
|
|
|
62
62
|
});
|
|
63
63
|
await activateShard('producer');
|
|
64
64
|
await activateShard('consumer');
|
|
65
|
-
const snapshot = consumerCtx.listActions({ activeOnly: true });
|
|
65
|
+
const snapshot = consumerCtx.sh3.listActions({ activeOnly: true });
|
|
66
66
|
const ids = snapshot.map((d) => d.id);
|
|
67
67
|
expect(ids).toContain('home.go');
|
|
68
68
|
expect(ids).not.toContain('app.go');
|
|
@@ -87,7 +87,7 @@ describe('ShardContext.listActions / runAction (integration)', () => {
|
|
|
87
87
|
});
|
|
88
88
|
await activateShard('producer');
|
|
89
89
|
await activateShard('consumer');
|
|
90
|
-
await consumerCtx.runAction('producer.go');
|
|
90
|
+
await consumerCtx.sh3.runAction('producer.go');
|
|
91
91
|
expect(invokedVia).toBe('programmatic');
|
|
92
92
|
});
|
|
93
93
|
it('runAction rejects when the target action is inactive', async () => {
|
|
@@ -106,6 +106,6 @@ describe('ShardContext.listActions / runAction (integration)', () => {
|
|
|
106
106
|
});
|
|
107
107
|
await activateShard('producer');
|
|
108
108
|
await activateShard('consumer');
|
|
109
|
-
await expect(consumerCtx.runAction('gated.go')).rejects.toThrow(/not active/);
|
|
109
|
+
await expect(consumerCtx.sh3.runAction('gated.go')).rejects.toThrow(/not active/);
|
|
110
110
|
});
|
|
111
111
|
});
|
package/dist/api.d.ts
CHANGED
|
@@ -80,3 +80,5 @@ export { default as Select } from './primitives/widgets/Select.svelte';
|
|
|
80
80
|
export type { SelectOption } from './primitives/widgets/Select';
|
|
81
81
|
export { default as AppPicker } from './primitives/widgets/AppPicker.svelte';
|
|
82
82
|
export { default as UserPicker } from './primitives/widgets/UserPicker.svelte';
|
|
83
|
+
export type { FieldKind, FieldAddress, FieldView, ControllableFieldDescriptor, ImperativeFieldDescriptor, ElementRefFieldDescriptor, ReadonlyFieldDescriptor, FieldsApi, DecorationHandle, } from './fields/types';
|
|
84
|
+
export { fieldAddressToString, fieldAddressFromString } from './fields/address';
|
package/dist/api.js
CHANGED
|
@@ -97,3 +97,4 @@ export { default as FilePicker } from './primitives/widgets/FilePicker.svelte';
|
|
|
97
97
|
export { default as Select } from './primitives/widgets/Select.svelte';
|
|
98
98
|
export { default as AppPicker } from './primitives/widgets/AppPicker.svelte';
|
|
99
99
|
export { default as UserPicker } from './primitives/widgets/UserPicker.svelte';
|
|
100
|
+
export { fieldAddressToString, fieldAddressFromString } from './fields/address';
|
package/dist/build.d.ts
CHANGED
|
@@ -36,7 +36,34 @@ export interface Sh3ArtifactOptions {
|
|
|
36
36
|
serverEntry?: string;
|
|
37
37
|
/** Override manifest fields not extractable from code (description, author, requires). */
|
|
38
38
|
manifest?: Partial<Pick<ArtifactManifest, 'description' | 'author' | 'requires'>>;
|
|
39
|
+
/**
|
|
40
|
+
* Append a semver build-metadata suffix to the artifact `version` written
|
|
41
|
+
* into `manifest.json`. Per ADR-013 the package.json version remains the
|
|
42
|
+
* release-version source; this option layers an artifact-level counter on
|
|
43
|
+
* top so test iterations and hotfix variants of the same release can ship
|
|
44
|
+
* as distinct artifacts without bumping the release number.
|
|
45
|
+
*
|
|
46
|
+
* - omit / undefined: artifact version equals package.json.version (default).
|
|
47
|
+
* - 'auto': resolved from `git describe --tags --abbrev=0` →
|
|
48
|
+
* `git rev-list --count <tag>..HEAD`. Suffix is omitted when the
|
|
49
|
+
* count is 0 (canonical release) or when git is unavailable.
|
|
50
|
+
* - string: literal suffix. Empty string is treated as no suffix.
|
|
51
|
+
*
|
|
52
|
+
* Example: package.json says "1.2.3", git is 4 commits past `v1.2.3` →
|
|
53
|
+
* manifest.json says `"version": "1.2.3+4"`.
|
|
54
|
+
*/
|
|
55
|
+
buildSuffix?: 'auto' | string;
|
|
39
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Compose a release version (`pkgVersion`) with an optional semver
|
|
59
|
+
* build-metadata suffix. Pure function — no I/O — for testability.
|
|
60
|
+
*
|
|
61
|
+
* Returns `pkgVersion` unchanged when `suffix` is empty/undefined.
|
|
62
|
+
* Throws when `pkgVersion` already carries build metadata or when the
|
|
63
|
+
* suffix is not a valid semver build-metadata identifier
|
|
64
|
+
* (dot-separated `[0-9A-Za-z-]+`).
|
|
65
|
+
*/
|
|
66
|
+
export declare function composeArtifactVersion(pkgVersion: string, suffix: string | undefined): string;
|
|
40
67
|
/**
|
|
41
68
|
* Vite plugin that produces a distributable artifact directory after build.
|
|
42
69
|
*
|
package/dist/build.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* });
|
|
18
18
|
*/
|
|
19
19
|
import { readFileSync, writeFileSync, unlinkSync, readdirSync, copyFileSync, existsSync } from 'node:fs';
|
|
20
|
+
import { execSync } from 'node:child_process';
|
|
20
21
|
import { join } from 'node:path';
|
|
21
22
|
/**
|
|
22
23
|
* Vite plugin that inlines extracted CSS into the JS bundle.
|
|
@@ -83,6 +84,52 @@ export function sh3CssInline() {
|
|
|
83
84
|
},
|
|
84
85
|
};
|
|
85
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Compose a release version (`pkgVersion`) with an optional semver
|
|
89
|
+
* build-metadata suffix. Pure function — no I/O — for testability.
|
|
90
|
+
*
|
|
91
|
+
* Returns `pkgVersion` unchanged when `suffix` is empty/undefined.
|
|
92
|
+
* Throws when `pkgVersion` already carries build metadata or when the
|
|
93
|
+
* suffix is not a valid semver build-metadata identifier
|
|
94
|
+
* (dot-separated `[0-9A-Za-z-]+`).
|
|
95
|
+
*/
|
|
96
|
+
export function composeArtifactVersion(pkgVersion, suffix) {
|
|
97
|
+
if (!suffix)
|
|
98
|
+
return pkgVersion;
|
|
99
|
+
if (pkgVersion.includes('+')) {
|
|
100
|
+
throw new Error(`[sh3-artifact] package.json version "${pkgVersion}" already contains semver build metadata; cannot append additional suffix "${suffix}".`);
|
|
101
|
+
}
|
|
102
|
+
if (!/^[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*$/.test(suffix)) {
|
|
103
|
+
throw new Error(`[sh3-artifact] buildSuffix "${suffix}" is not a valid semver build-metadata identifier (expected dot-separated [0-9A-Za-z-]+).`);
|
|
104
|
+
}
|
|
105
|
+
return `${pkgVersion}+${suffix}`;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Resolve the auto build suffix from git: count of commits between the
|
|
109
|
+
* latest reachable tag and HEAD. Returns '' if git is unavailable, no
|
|
110
|
+
* tags exist, or HEAD is exactly at the tag (canonical release).
|
|
111
|
+
*/
|
|
112
|
+
function resolveAutoBuildSuffix() {
|
|
113
|
+
try {
|
|
114
|
+
const lastTag = execSync('git describe --tags --abbrev=0', {
|
|
115
|
+
encoding: 'utf-8',
|
|
116
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
117
|
+
}).trim();
|
|
118
|
+
if (!lastTag)
|
|
119
|
+
return '';
|
|
120
|
+
const count = execSync(`git rev-list --count ${lastTag}..HEAD`, {
|
|
121
|
+
encoding: 'utf-8',
|
|
122
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
123
|
+
}).trim();
|
|
124
|
+
const n = parseInt(count, 10);
|
|
125
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
126
|
+
return '';
|
|
127
|
+
return String(n);
|
|
128
|
+
}
|
|
129
|
+
catch (_a) {
|
|
130
|
+
return '';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
86
133
|
/**
|
|
87
134
|
* Vite plugin that produces a distributable artifact directory after build.
|
|
88
135
|
*
|
|
@@ -224,6 +271,17 @@ export function sh3Artifact(options = {}) {
|
|
|
224
271
|
throw new Error('[sh3-artifact] Missing "version" in package.json. Per ADR-013 the package '
|
|
225
272
|
+ 'version is read from package.json, not from the source manifest.');
|
|
226
273
|
}
|
|
274
|
+
// --- Resolve build suffix and compose the artifact version ---
|
|
275
|
+
// Per ADR-013 amendment: package.json holds the release version;
|
|
276
|
+
// sh3Artifact may layer a build-metadata suffix on top.
|
|
277
|
+
let resolvedSuffix = '';
|
|
278
|
+
if (options.buildSuffix === 'auto') {
|
|
279
|
+
resolvedSuffix = resolveAutoBuildSuffix();
|
|
280
|
+
}
|
|
281
|
+
else if (typeof options.buildSuffix === 'string') {
|
|
282
|
+
resolvedSuffix = options.buildSuffix;
|
|
283
|
+
}
|
|
284
|
+
const artifactVersion = composeArtifactVersion(pkgVersion, resolvedSuffix);
|
|
227
285
|
// --- Write manifest.json ---
|
|
228
286
|
const overrides = (_c = options.manifest) !== null && _c !== void 0 ? _c : {};
|
|
229
287
|
const finalDescription = (_d = overrides.description) !== null && _d !== void 0 ? _d : pkgDescription;
|
|
@@ -234,7 +292,7 @@ export function sh3Artifact(options = {}) {
|
|
|
234
292
|
if (!finalAuthor) {
|
|
235
293
|
throw new Error('[sh3-artifact] Missing "author". Add it to package.json or pass it via sh3Artifact({ manifest: { author } }).');
|
|
236
294
|
}
|
|
237
|
-
const manifest = Object.assign(Object.assign(Object.assign({ id: id || 'unknown', type, label: label || id || 'unknown', version:
|
|
295
|
+
const manifest = Object.assign(Object.assign(Object.assign({ id: id || 'unknown', type, label: label || id || 'unknown', version: artifactVersion, contractVersion: 1, client: 'client.js' }, (hasServer ? { server: 'server.js' } : {})), { description: finalDescription, author: finalAuthor }), overrides);
|
|
238
296
|
writeFileSync(join(outDir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
239
297
|
// --- Log summary ---
|
|
240
298
|
const files = ['manifest.json', 'client.js'];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { composeArtifactVersion } from './build';
|
|
3
|
+
describe('composeArtifactVersion', () => {
|
|
4
|
+
it('returns pkgVersion unchanged when suffix is undefined', () => {
|
|
5
|
+
expect(composeArtifactVersion('1.2.3', undefined)).toBe('1.2.3');
|
|
6
|
+
});
|
|
7
|
+
it('returns pkgVersion unchanged when suffix is empty string', () => {
|
|
8
|
+
expect(composeArtifactVersion('1.2.3', '')).toBe('1.2.3');
|
|
9
|
+
});
|
|
10
|
+
it('appends a numeric suffix as build metadata', () => {
|
|
11
|
+
expect(composeArtifactVersion('1.2.3', '42')).toBe('1.2.3+42');
|
|
12
|
+
});
|
|
13
|
+
it('appends dotted alphanumeric suffix as build metadata', () => {
|
|
14
|
+
expect(composeArtifactVersion('1.2.3', 'build.42')).toBe('1.2.3+build.42');
|
|
15
|
+
});
|
|
16
|
+
it('composes onto a prerelease version (semver allows -pre+meta)', () => {
|
|
17
|
+
expect(composeArtifactVersion('1.2.3-rc.1', '7')).toBe('1.2.3-rc.1+7');
|
|
18
|
+
});
|
|
19
|
+
it('throws when pkgVersion already has build metadata', () => {
|
|
20
|
+
expect(() => composeArtifactVersion('1.2.3+local', '42')).toThrow(/already contains semver build metadata/);
|
|
21
|
+
});
|
|
22
|
+
it('throws when suffix contains an invalid character', () => {
|
|
23
|
+
expect(() => composeArtifactVersion('1.2.3', 'build_42')).toThrow(/not a valid semver build-metadata identifier/);
|
|
24
|
+
});
|
|
25
|
+
it('throws when suffix has empty dot-separated identifier', () => {
|
|
26
|
+
expect(() => composeArtifactVersion('1.2.3', 'build..42')).toThrow(/not a valid semver build-metadata identifier/);
|
|
27
|
+
});
|
|
28
|
+
it('throws when suffix starts with a plus', () => {
|
|
29
|
+
expect(() => composeArtifactVersion('1.2.3', '+42')).toThrow(/not a valid semver build-metadata identifier/);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export type { ContributionsApi } from './types';
|
|
2
|
-
export { register, list, listPoints, onChange, onAnyChange, __resetContributionsForTest } from './registry';
|
|
2
|
+
export { register, list, listPoints, onChange, onAnyChange, __disposeSlotContributions, __resetContributionsForTest, } from './registry';
|
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
* file is internal-only, re-exporting the registry for activate.svelte.ts
|
|
6
6
|
* and for tests.
|
|
7
7
|
*/
|
|
8
|
-
export { register, list, listPoints, onChange, onAnyChange, __resetContributionsForTest } from './registry';
|
|
8
|
+
export { register, list, listPoints, onChange, onAnyChange, __disposeSlotContributions, __resetContributionsForTest, } from './registry';
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Register a descriptor under the given point. Returns an unregister
|
|
3
3
|
* function; calling it more than once is a safe no-op.
|
|
4
|
+
*
|
|
5
|
+
* Pass `opts.scope.slotId` to tie cleanup to a slot's lifecycle:
|
|
6
|
+
* `__disposeSlotContributions(slotId)` will fire the disposer on slot
|
|
7
|
+
* unmount. The disposer is idempotent — manual dispose detaches it from
|
|
8
|
+
* the slot bag, so a later slot-cleanup pass becomes a no-op for that
|
|
9
|
+
* entry.
|
|
4
10
|
*/
|
|
5
|
-
export declare function register<T = unknown>(pointId: string, descriptor: T
|
|
11
|
+
export declare function register<T = unknown>(pointId: string, descriptor: T, opts?: {
|
|
12
|
+
scope?: {
|
|
13
|
+
slotId?: string;
|
|
14
|
+
};
|
|
15
|
+
}): () => void;
|
|
6
16
|
/** Enumerate descriptors at the named point in registration order. */
|
|
7
17
|
export declare function list<T = unknown>(pointId: string): T[];
|
|
8
18
|
/** Enumerate every point id with at least one registration. */
|
|
@@ -21,6 +31,12 @@ export declare function onChange(pointId: string, cb: () => void): () => void;
|
|
|
21
31
|
* safe no-op. Symmetric with `onChange`, but global.
|
|
22
32
|
*/
|
|
23
33
|
export declare function onAnyChange(cb: (pointId: string) => void): () => void;
|
|
34
|
+
/**
|
|
35
|
+
* Drain every disposer registered with `scope.slotId === slotId`. Safe
|
|
36
|
+
* to call on unknown slot ids. Used by the layout module on slot unmount
|
|
37
|
+
* to release contributions tied to that slot's lifetime.
|
|
38
|
+
*/
|
|
39
|
+
export declare function __disposeSlotContributions(slotId: string): void;
|
|
24
40
|
/**
|
|
25
41
|
* Test-only reset. Not exported from the barrel; tests import it
|
|
26
42
|
* directly from this module.
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
const points = new Map();
|
|
14
14
|
const listeners = new Map();
|
|
15
15
|
const anyListeners = new Set();
|
|
16
|
+
const slotCleanup = new Map();
|
|
16
17
|
function emit(pointId) {
|
|
17
18
|
const set = listeners.get(pointId);
|
|
18
19
|
if (set) {
|
|
@@ -22,11 +23,34 @@ function emit(pointId) {
|
|
|
22
23
|
for (const cb of anyListeners)
|
|
23
24
|
cb(pointId);
|
|
24
25
|
}
|
|
26
|
+
function attachToSlot(slotId, dispose) {
|
|
27
|
+
let bag = slotCleanup.get(slotId);
|
|
28
|
+
if (!bag) {
|
|
29
|
+
bag = new Set();
|
|
30
|
+
slotCleanup.set(slotId, bag);
|
|
31
|
+
}
|
|
32
|
+
bag.add(dispose);
|
|
33
|
+
}
|
|
34
|
+
function detachFromSlot(slotId, dispose) {
|
|
35
|
+
const bag = slotCleanup.get(slotId);
|
|
36
|
+
if (!bag)
|
|
37
|
+
return;
|
|
38
|
+
bag.delete(dispose);
|
|
39
|
+
if (bag.size === 0)
|
|
40
|
+
slotCleanup.delete(slotId);
|
|
41
|
+
}
|
|
25
42
|
/**
|
|
26
43
|
* Register a descriptor under the given point. Returns an unregister
|
|
27
44
|
* function; calling it more than once is a safe no-op.
|
|
45
|
+
*
|
|
46
|
+
* Pass `opts.scope.slotId` to tie cleanup to a slot's lifecycle:
|
|
47
|
+
* `__disposeSlotContributions(slotId)` will fire the disposer on slot
|
|
48
|
+
* unmount. The disposer is idempotent — manual dispose detaches it from
|
|
49
|
+
* the slot bag, so a later slot-cleanup pass becomes a no-op for that
|
|
50
|
+
* entry.
|
|
28
51
|
*/
|
|
29
|
-
export function register(pointId, descriptor) {
|
|
52
|
+
export function register(pointId, descriptor, opts) {
|
|
53
|
+
var _a;
|
|
30
54
|
const handle = Symbol();
|
|
31
55
|
let map = points.get(pointId);
|
|
32
56
|
if (!map) {
|
|
@@ -35,11 +59,14 @@ export function register(pointId, descriptor) {
|
|
|
35
59
|
}
|
|
36
60
|
map.set(handle, descriptor);
|
|
37
61
|
emit(pointId);
|
|
62
|
+
const slotId = (_a = opts === null || opts === void 0 ? void 0 : opts.scope) === null || _a === void 0 ? void 0 : _a.slotId;
|
|
38
63
|
let disposed = false;
|
|
39
|
-
|
|
64
|
+
const dispose = () => {
|
|
40
65
|
if (disposed)
|
|
41
66
|
return;
|
|
42
67
|
disposed = true;
|
|
68
|
+
if (slotId)
|
|
69
|
+
detachFromSlot(slotId, dispose);
|
|
43
70
|
const m = points.get(pointId);
|
|
44
71
|
if (!m)
|
|
45
72
|
return;
|
|
@@ -49,6 +76,9 @@ export function register(pointId, descriptor) {
|
|
|
49
76
|
emit(pointId);
|
|
50
77
|
}
|
|
51
78
|
};
|
|
79
|
+
if (slotId)
|
|
80
|
+
attachToSlot(slotId, dispose);
|
|
81
|
+
return dispose;
|
|
52
82
|
}
|
|
53
83
|
/** Enumerate descriptors at the named point in registration order. */
|
|
54
84
|
export function list(pointId) {
|
|
@@ -98,6 +128,23 @@ export function onAnyChange(cb) {
|
|
|
98
128
|
anyListeners.delete(cb);
|
|
99
129
|
};
|
|
100
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Drain every disposer registered with `scope.slotId === slotId`. Safe
|
|
133
|
+
* to call on unknown slot ids. Used by the layout module on slot unmount
|
|
134
|
+
* to release contributions tied to that slot's lifetime.
|
|
135
|
+
*/
|
|
136
|
+
export function __disposeSlotContributions(slotId) {
|
|
137
|
+
const bag = slotCleanup.get(slotId);
|
|
138
|
+
if (!bag)
|
|
139
|
+
return;
|
|
140
|
+
// Snapshot before iterating: each dispose detaches itself from the bag
|
|
141
|
+
// via detachFromSlot, which mutates the live set.
|
|
142
|
+
const snapshot = Array.from(bag);
|
|
143
|
+
for (const dispose of snapshot)
|
|
144
|
+
dispose();
|
|
145
|
+
// detachFromSlot already removes empty bags, but be explicit.
|
|
146
|
+
slotCleanup.delete(slotId);
|
|
147
|
+
}
|
|
101
148
|
/**
|
|
102
149
|
* Test-only reset. Not exported from the barrel; tests import it
|
|
103
150
|
* directly from this module.
|
|
@@ -106,4 +153,5 @@ export function __resetContributionsForTest() {
|
|
|
106
153
|
points.clear();
|
|
107
154
|
listeners.clear();
|
|
108
155
|
anyListeners.clear();
|
|
156
|
+
slotCleanup.clear();
|
|
109
157
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { register, list, onChange, __disposeSlotContributions, __resetContributionsForTest, } from './registry';
|
|
3
|
+
describe('contributions slot scope', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
__resetContributionsForTest();
|
|
6
|
+
});
|
|
7
|
+
it('register without scope behaves exactly as before', () => {
|
|
8
|
+
const dispose = register('p', { id: 'a' });
|
|
9
|
+
expect(list('p')).toEqual([{ id: 'a' }]);
|
|
10
|
+
dispose();
|
|
11
|
+
expect(list('p')).toEqual([]);
|
|
12
|
+
});
|
|
13
|
+
it('register with slot scope is reachable like any contribution', () => {
|
|
14
|
+
register('p', { id: 'a' }, { scope: { slotId: 's1' } });
|
|
15
|
+
expect(list('p')).toEqual([{ id: 'a' }]);
|
|
16
|
+
});
|
|
17
|
+
it('__disposeSlotContributions drains only the targeted slot', () => {
|
|
18
|
+
register('p', { id: 'a-s1' }, { scope: { slotId: 's1' } });
|
|
19
|
+
register('p', { id: 'b-s1' }, { scope: { slotId: 's1' } });
|
|
20
|
+
register('p', { id: 'c-s2' }, { scope: { slotId: 's2' } });
|
|
21
|
+
register('p', { id: 'd-noscope' });
|
|
22
|
+
__disposeSlotContributions('s1');
|
|
23
|
+
const remaining = list('p').map((d) => d.id).sort();
|
|
24
|
+
expect(remaining).toEqual(['c-s2', 'd-noscope']);
|
|
25
|
+
});
|
|
26
|
+
it('__disposeSlotContributions on unknown slot is a no-op', () => {
|
|
27
|
+
register('p', { id: 'a' }, { scope: { slotId: 's1' } });
|
|
28
|
+
expect(() => __disposeSlotContributions('s999')).not.toThrow();
|
|
29
|
+
expect(list('p')).toEqual([{ id: 'a' }]);
|
|
30
|
+
});
|
|
31
|
+
it('manually calling the disposer first makes __disposeSlotContributions a no-op for that entry', () => {
|
|
32
|
+
const dispose = register('p', { id: 'a' }, { scope: { slotId: 's1' } });
|
|
33
|
+
dispose();
|
|
34
|
+
expect(() => __disposeSlotContributions('s1')).not.toThrow();
|
|
35
|
+
expect(list('p')).toEqual([]);
|
|
36
|
+
});
|
|
37
|
+
it('double-dispose is idempotent', () => {
|
|
38
|
+
const dispose = register('p', { id: 'a' }, { scope: { slotId: 's1' } });
|
|
39
|
+
dispose();
|
|
40
|
+
dispose();
|
|
41
|
+
expect(list('p')).toEqual([]);
|
|
42
|
+
});
|
|
43
|
+
it('slot cleanup fires onChange for the affected pointId', () => {
|
|
44
|
+
const cb = vi.fn();
|
|
45
|
+
onChange('p', cb);
|
|
46
|
+
register('p', { id: 'a' }, { scope: { slotId: 's1' } });
|
|
47
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
48
|
+
cb.mockClear();
|
|
49
|
+
__disposeSlotContributions('s1');
|
|
50
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -5,11 +5,19 @@ export interface ContributionsApi {
|
|
|
5
5
|
* for ergonomics — provider and contributor agree on the shape via
|
|
6
6
|
* a type-only import of the provider's public types.
|
|
7
7
|
*
|
|
8
|
+
* Pass `opts.scope.slotId` to tie cleanup to a slot's lifecycle: the
|
|
9
|
+
* disposer will fire on slot unmount in addition to shard deactivate.
|
|
10
|
+
* Whichever fires first wins; the disposer is idempotent.
|
|
11
|
+
*
|
|
8
12
|
* Returns an unregister function. Calling it is optional (the
|
|
9
|
-
* framework auto-unregisters on shard deactivate
|
|
10
|
-
* more than once.
|
|
13
|
+
* framework auto-unregisters on shard deactivate, and on slot unmount
|
|
14
|
+
* when scoped) and safe to call more than once.
|
|
11
15
|
*/
|
|
12
|
-
register<T = unknown>(pointId: string, descriptor: T
|
|
16
|
+
register<T = unknown>(pointId: string, descriptor: T, opts?: {
|
|
17
|
+
scope?: {
|
|
18
|
+
slotId?: string;
|
|
19
|
+
};
|
|
20
|
+
}): () => void;
|
|
13
21
|
/** Enumerate descriptors at `pointId` in registration order. */
|
|
14
22
|
list<T = unknown>(pointId: string): T[];
|
|
15
23
|
/** Enumerate every point id with at least one registration. */
|
package/dist/createShell.js
CHANGED
|
@@ -70,8 +70,14 @@ export async function createShell(config) {
|
|
|
70
70
|
// via /api/packages aren't in IndexedDB on the satellite's view of the
|
|
71
71
|
// world unless we fetch and register them, same as the main path.
|
|
72
72
|
await loadDiscoveredPackages(config === null || config === void 0 ? void 0 : config.discoveredPackages);
|
|
73
|
+
// For app payloads, defer required-shard activation to launchApp so it
|
|
74
|
+
// runs *after* attachApp() binds the preset manager + slot holds. The
|
|
75
|
+
// payload's activateShards (manifest.requiredShards) are still carried
|
|
76
|
+
// for diagnostics, but launchApp drives activation in the same order
|
|
77
|
+
// as the host bootstrap. Float payloads have no launchApp so the walked
|
|
78
|
+
// view-providing shards must still activate here.
|
|
73
79
|
await bootstrapSatellite({
|
|
74
|
-
activateShardIds: satellite.payload.activateShards,
|
|
80
|
+
activateShardIds: satellite.payload.kind === 'app' ? [] : satellite.payload.activateShards,
|
|
75
81
|
});
|
|
76
82
|
attachGlobalListeners();
|
|
77
83
|
mount(SatelliteShell, { target, props: { payload: satellite.payload } });
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const ID_RE = /^[a-zA-Z0-9.\-_]+$/;
|
|
2
|
+
const SLOT_RE = /^[a-zA-Z0-9.\-_]*$/; // slotId may be empty in the wire form
|
|
3
|
+
function validateIdPart(value, partName) {
|
|
4
|
+
if (value.length === 0)
|
|
5
|
+
throw new Error(`fieldAddress: ${partName} is empty`);
|
|
6
|
+
if (!ID_RE.test(value)) {
|
|
7
|
+
throw new Error(`fieldAddress: invalid ${partName} "${value}" (must match [a-zA-Z0-9.\\-_]+)`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function validateSlotPart(value) {
|
|
11
|
+
if (!SLOT_RE.test(value)) {
|
|
12
|
+
throw new Error(`fieldAddress: invalid slotId "${value}"`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function fieldAddressToString(a) {
|
|
16
|
+
var _a;
|
|
17
|
+
validateIdPart(a.shardId, 'shardId');
|
|
18
|
+
validateIdPart(a.fieldId, 'fieldId');
|
|
19
|
+
if (a.slotId !== undefined)
|
|
20
|
+
validateSlotPart(a.slotId);
|
|
21
|
+
return `${a.shardId}::${(_a = a.slotId) !== null && _a !== void 0 ? _a : ''}::${a.fieldId}`;
|
|
22
|
+
}
|
|
23
|
+
export function fieldAddressFromString(s) {
|
|
24
|
+
const parts = s.split('::');
|
|
25
|
+
if (parts.length !== 3) {
|
|
26
|
+
throw new Error(`fieldAddress: malformed "${s}" (expected three ::-separated parts)`);
|
|
27
|
+
}
|
|
28
|
+
const [shardId, slotPart, fieldId] = parts;
|
|
29
|
+
validateIdPart(shardId, 'shardId');
|
|
30
|
+
validateIdPart(fieldId, 'fieldId');
|
|
31
|
+
validateSlotPart(slotPart);
|
|
32
|
+
const out = { shardId, fieldId };
|
|
33
|
+
if (slotPart.length > 0)
|
|
34
|
+
out.slotId = slotPart;
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { fieldAddressToString, fieldAddressFromString } from './address';
|
|
3
|
+
describe('fieldAddress codec', () => {
|
|
4
|
+
it('roundtrips a slot-scoped address', () => {
|
|
5
|
+
const a = { shardId: 'editor', slotId: 'slot-1', fieldId: 'title' };
|
|
6
|
+
expect(fieldAddressFromString(fieldAddressToString(a))).toEqual(a);
|
|
7
|
+
});
|
|
8
|
+
it('roundtrips a shard-scoped address (slotId absent)', () => {
|
|
9
|
+
const a = { shardId: 'settings', fieldId: 'theme' };
|
|
10
|
+
expect(fieldAddressFromString(fieldAddressToString(a))).toEqual(a);
|
|
11
|
+
});
|
|
12
|
+
it('serializes shard-scoped as <shardId>::<empty>::<fieldId>', () => {
|
|
13
|
+
expect(fieldAddressToString({ shardId: 's', fieldId: 'f' })).toBe('s::::f');
|
|
14
|
+
});
|
|
15
|
+
it('serializes slot-scoped as <shardId>::<slotId>::<fieldId>', () => {
|
|
16
|
+
expect(fieldAddressToString({ shardId: 's', slotId: 'sl', fieldId: 'f' })).toBe('s::sl::f');
|
|
17
|
+
});
|
|
18
|
+
it('rejects malformed input — too few parts', () => {
|
|
19
|
+
expect(() => fieldAddressFromString('s::sl')).toThrow(/malformed/);
|
|
20
|
+
});
|
|
21
|
+
it('rejects malformed input — too many parts', () => {
|
|
22
|
+
expect(() => fieldAddressFromString('s::sl::f::extra')).toThrow(/malformed/);
|
|
23
|
+
});
|
|
24
|
+
it('rejects an empty shardId', () => {
|
|
25
|
+
expect(() => fieldAddressFromString('::sl::f')).toThrow(/shardId/);
|
|
26
|
+
});
|
|
27
|
+
it('rejects an empty fieldId', () => {
|
|
28
|
+
expect(() => fieldAddressFromString('s::sl::')).toThrow(/fieldId/);
|
|
29
|
+
});
|
|
30
|
+
it('rejects characters outside [a-zA-Z0-9.\\-_]', () => {
|
|
31
|
+
expect(() => fieldAddressToString({ shardId: 's space', fieldId: 'f' })).toThrow(/invalid/);
|
|
32
|
+
expect(() => fieldAddressToString({ shardId: 's', fieldId: 'a:b' })).toThrow(/invalid/);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FieldAddress, DecorationHandle } from './types';
|
|
2
|
+
export declare function attachDecoration(addr: FieldAddress, factory: (target: {
|
|
3
|
+
element: HTMLElement;
|
|
4
|
+
rect: DOMRect;
|
|
5
|
+
}) => HTMLElement | DecorationHandle): () => void;
|
|
6
|
+
/** Test-only: tear everything down and reset module state. */
|
|
7
|
+
export declare function __resetDecorationLayerForTest(): void;
|