sh3-core 0.20.2 → 0.21.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/BrandSlot.svelte +2 -2
- package/dist/actions/ctx-actions.svelte.test.js +2 -2
- package/dist/api.d.ts +2 -2
- package/dist/api.js +1 -1
- package/dist/app/store/StoreView.svelte +26 -35
- package/dist/app/store/storeShard.svelte.js +35 -49
- package/dist/app/store/verbs.js +24 -55
- package/dist/artifact.d.ts +2 -0
- package/dist/boot/satellitePayload.d.ts +2 -0
- package/dist/boot/satellitePayload.test.js +19 -0
- package/dist/build.d.ts +7 -1
- package/dist/build.js +34 -9
- package/dist/build.test.js +27 -1
- package/dist/createShell.js +34 -9
- package/dist/documents/browse.d.ts +20 -0
- package/dist/documents/browse.js +35 -0
- package/dist/documents/browse.test.js +125 -0
- package/dist/documents/config.d.ts +0 -4
- package/dist/documents/config.js +0 -8
- package/dist/documents/http-backend.d.ts +5 -0
- package/dist/documents/http-backend.js +25 -0
- package/dist/documents/http-backend.test.js +66 -0
- package/dist/documents/index.d.ts +1 -1
- package/dist/documents/index.js +1 -1
- package/dist/documents/types.d.ts +11 -0
- package/dist/env/client.d.ts +6 -10
- package/dist/env/client.js +11 -21
- package/dist/env/index.d.ts +2 -1
- package/dist/env/index.js +1 -1
- package/dist/host-entry.d.ts +1 -1
- package/dist/host-entry.js +1 -1
- package/dist/host.d.ts +1 -1
- package/dist/host.js +1 -1
- package/dist/layout/slotHostPool.svelte.js +2 -2
- package/dist/overlays/FloatFrame.svelte +1 -0
- package/dist/projects/session-state.svelte.d.ts +3 -0
- package/dist/projects/session-state.svelte.js +25 -0
- package/dist/projects/session-state.test.js +43 -2
- package/dist/projects-shard/ProjectsSection.svelte +14 -18
- package/dist/registry/archive.d.ts +12 -0
- package/dist/registry/archive.js +80 -0
- package/dist/registry/archive.test.d.ts +1 -0
- package/dist/registry/archive.test.js +84 -0
- package/dist/registry/client.d.ts +9 -29
- package/dist/registry/client.js +14 -60
- package/dist/registry/client.test.js +31 -21
- package/dist/registry/index.d.ts +2 -2
- package/dist/registry/index.js +1 -1
- package/dist/registry/installer.d.ts +4 -4
- package/dist/registry/installer.js +74 -45
- package/dist/registry/schema.js +4 -27
- package/dist/registry/schema.test.d.ts +1 -0
- package/dist/registry/schema.test.js +41 -0
- package/dist/registry/types.d.ts +16 -41
- package/dist/runtime/runVerb-shell.test.js +2 -2
- package/dist/runtime/runVerb.test.js +2 -2
- package/dist/sh3core-shard/appActions.js +5 -2
- package/dist/shards/activate-browse.test.js +2 -2
- package/dist/shards/activate-contributions.test.js +2 -2
- package/dist/shards/activate-error-isolation.test.js +3 -3
- package/dist/shards/activate-on-key-revoked.test.js +2 -2
- package/dist/shards/activate-runtime.test.js +2 -2
- package/dist/shards/activate.svelte.js +4 -4
- package/dist/shards/ctx-fetch.test.js +4 -4
- package/dist/shell-shard/verbs/xfer.js +13 -27
- package/dist/shell-shard/verbs/xfer.test.js +36 -25
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
package/dist/BrandSlot.svelte
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { getLiveDispatcherState } from './actions/state.svelte';
|
|
14
14
|
import { launchApp, returnToHome } from './apps/lifecycle';
|
|
15
15
|
import { getBreadcrumbAppId, getRegisteredApp } from './apps/registry.svelte';
|
|
16
|
-
import { sessionState,
|
|
16
|
+
import { sessionState, switchProjectScope } from './projects/session-state.svelte';
|
|
17
17
|
import { projectsState } from './projects-shard/projectsShard.svelte';
|
|
18
18
|
|
|
19
19
|
const activeAppId = $derived(getLiveDispatcherState().activeAppId);
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
function exitProject() {
|
|
51
|
-
|
|
51
|
+
switchProjectScope(null);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
function reenterProjectHome() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import { MemoryDocumentBackend } from '../documents/backends';
|
|
3
|
-
import { __setDocumentBackend,
|
|
3
|
+
import { __setDocumentBackend, __setActiveScope } from '../documents/config';
|
|
4
4
|
import { registerShard, activateShard, __resetShardRegistryForTest, } from '../shards/activate.svelte';
|
|
5
5
|
import { __resetViewRegistryForTest } from '../shards/registry';
|
|
6
6
|
import { __resetActionsRegistryForTest } from './registry';
|
|
@@ -14,7 +14,7 @@ describe('ShardContext.listActions / runAction (integration)', () => {
|
|
|
14
14
|
__resetActionsRegistryForTest();
|
|
15
15
|
__resetDispatcherStateForTest();
|
|
16
16
|
__setDocumentBackend(new MemoryDocumentBackend());
|
|
17
|
-
|
|
17
|
+
__setActiveScope('tenant-test');
|
|
18
18
|
});
|
|
19
19
|
it('listActions enumerates actions registered by other shards', async () => {
|
|
20
20
|
registerShard({
|
package/dist/api.d.ts
CHANGED
|
@@ -40,9 +40,9 @@ export type { ColorPickOptions, ColorContribution, ColorApi, } from './color/api
|
|
|
40
40
|
export { COLOR_PICKER_POINT } from './color/api';
|
|
41
41
|
export { registeredShards, activeShards, erroredShards } from './shards/activate.svelte';
|
|
42
42
|
export type { ShardErrorEntry } from './shards/activate.svelte';
|
|
43
|
-
export type { RegistryIndex, PackageEntry, PackageVersion, RequiredDependency, InstalledPackage, InstallResult, PackageMeta, } from './registry/types';
|
|
43
|
+
export type { RegistryIndex, PackageEntry, PackageVersion, RequiredDependency, InstalledPackage, InstallResult, PackageMeta, RemoteInstallRequest, } from './registry/types';
|
|
44
44
|
export type { ResolvedPackage } from './registry/client';
|
|
45
|
-
export { fetchRegistries,
|
|
45
|
+
export { fetchRegistries, fetchArchive, buildPackageMeta } from './registry/client';
|
|
46
46
|
export { validateRegistryIndex } from './registry/schema';
|
|
47
47
|
export { PERMISSION_KEYS_MINT, ScopeEscalationError, ConsentDeniedError, type ShardContextKeys, type ApiKeyPublic, type MintOpts, } from './keys/types';
|
|
48
48
|
export { registerConsentListener, resolveConsent, type ConsentRequest } from './keys/consent.svelte';
|
package/dist/api.js
CHANGED
|
@@ -43,7 +43,7 @@ export { COLOR_PICKER_POINT } from './color/api';
|
|
|
43
43
|
// addition: diagnostic used to reach `activate.svelte` directly via $lib;
|
|
44
44
|
// the package boundary requires routing through the public surface.
|
|
45
45
|
export { registeredShards, activeShards, erroredShards } from './shards/activate.svelte';
|
|
46
|
-
export { fetchRegistries,
|
|
46
|
+
export { fetchRegistries, fetchArchive, buildPackageMeta } from './registry/client';
|
|
47
47
|
export { validateRegistryIndex } from './registry/schema';
|
|
48
48
|
// Key mint/revoke types — client shards that declare `keys:mint` get ctx.keys.
|
|
49
49
|
export { PERMISSION_KEYS_MINT, ScopeEscalationError, ConsentDeniedError, } from './keys/types';
|
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { storeContext } from './storeShard.svelte';
|
|
10
|
-
import {
|
|
10
|
+
import { fetchArchive, buildPackageMeta } from '../../registry/client';
|
|
11
|
+
import { readFileFromArchive, readManifestFromArchive } from '../../registry/archive';
|
|
11
12
|
import { installPackage } from '../../registry/installer';
|
|
12
13
|
import { loadBundleModule, type LoadedBundle } from '../../registry/loader';
|
|
13
14
|
import { extractBundlePermissions } from '../../registry/permission-descriptions';
|
|
14
|
-
import {
|
|
15
|
+
import { requestServerInstall } from '../../env/client';
|
|
15
16
|
import { checkBundleFetch } from '../../registry/checkFetch';
|
|
16
17
|
import { contract } from '../../contract';
|
|
17
18
|
import type { ResolvedPackage } from '../../registry/client';
|
|
@@ -36,9 +37,8 @@
|
|
|
36
37
|
pkg: ResolvedPackage;
|
|
37
38
|
permissions: string[];
|
|
38
39
|
loaded: LoadedBundle;
|
|
39
|
-
|
|
40
|
+
archiveBytes: Uint8Array;
|
|
40
41
|
meta: ReturnType<typeof buildPackageMeta>;
|
|
41
|
-
serverBundle: ArrayBuffer | undefined;
|
|
42
42
|
warnings: string[];
|
|
43
43
|
}>(null);
|
|
44
44
|
|
|
@@ -171,26 +171,20 @@
|
|
|
171
171
|
installError = null;
|
|
172
172
|
|
|
173
173
|
try {
|
|
174
|
-
// 1. Fetch and integrity-verify the
|
|
175
|
-
const
|
|
174
|
+
// 1. Fetch and integrity-verify the archive from the registry.
|
|
175
|
+
const archiveBytes = await fetchArchive(pkg.latest, pkg.sourceRegistry);
|
|
176
176
|
const meta = buildPackageMeta(pkg, pkg.latest);
|
|
177
177
|
|
|
178
|
-
// 2.
|
|
179
|
-
|
|
180
|
-
const loaded = await loadBundleModule(
|
|
181
|
-
const permissions = extractBundlePermissions(loaded);
|
|
178
|
+
// 2. Extract client.js, load the module to get permissions for the modal.
|
|
179
|
+
const clientJs = readFileFromArchive(archiveBytes, 'client.js');
|
|
180
|
+
const loaded = clientJs ? await loadBundleModule(clientJs) : null;
|
|
181
|
+
const permissions = loaded ? extractBundlePermissions(loaded) : [];
|
|
182
182
|
|
|
183
|
-
// 3.
|
|
184
|
-
let serverBundle: ArrayBuffer | undefined;
|
|
185
|
-
if (pkg.latest.serverBundleUrl) {
|
|
186
|
-
serverBundle = await fetchServerBundle(pkg.latest, pkg.sourceRegistry);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// 4. Show the confirmation modal. The actual install happens in
|
|
183
|
+
// 3. Show the confirmation modal. The actual install happens in
|
|
190
184
|
// confirmInstall() once the user clicks Install.
|
|
191
|
-
const bundleText = new TextDecoder().decode(new Uint8Array(
|
|
185
|
+
const bundleText = clientJs ? new TextDecoder().decode(new Uint8Array(clientJs)) : '';
|
|
192
186
|
const warnings = checkBundleFetch(bundleText);
|
|
193
|
-
installModal = { pkg, permissions, loaded
|
|
187
|
+
installModal = { pkg, permissions, loaded: loaded!, archiveBytes, meta, warnings };
|
|
194
188
|
} catch (err) {
|
|
195
189
|
installError = err instanceof Error ? err.message : String(err);
|
|
196
190
|
const next = new Set(installingIds);
|
|
@@ -204,29 +198,26 @@
|
|
|
204
198
|
if (!ctxModal) return;
|
|
205
199
|
installModal = null;
|
|
206
200
|
|
|
207
|
-
const { pkg, loaded,
|
|
201
|
+
const { pkg, loaded, archiveBytes, meta } = ctxModal;
|
|
208
202
|
const id = pkg.entry.id;
|
|
209
203
|
|
|
210
204
|
try {
|
|
211
|
-
const
|
|
212
|
-
id: meta.id,
|
|
213
|
-
type: meta.type,
|
|
214
|
-
label: pkg.entry.label,
|
|
215
|
-
version: meta.version,
|
|
216
|
-
contractVersion: meta.contractVersion,
|
|
217
|
-
sourceRegistry: meta.sourceRegistry,
|
|
218
|
-
requiredShards: pkg.latest.requires?.map((r) => r.id) ?? [],
|
|
219
|
-
installedAt: new Date().toISOString(),
|
|
220
|
-
};
|
|
221
|
-
const serverResult = await serverInstallPackage(manifest, bundle, serverBundle);
|
|
205
|
+
const serverResult = await requestServerInstall(pkg.sourceRegistry, pkg.entry.id, meta.version);
|
|
222
206
|
if (!serverResult.ok) {
|
|
223
|
-
|
|
207
|
+
let errMsg = serverResult.error ?? 'Server install failed';
|
|
208
|
+
if (serverResult.code === 'missing-shards' && serverResult.missing) {
|
|
209
|
+
errMsg = `missing required shard(s): ${serverResult.missing.map((m: { id: string }) => m.id).join(', ')}`;
|
|
210
|
+
}
|
|
211
|
+
installError = errMsg;
|
|
224
212
|
return;
|
|
225
213
|
}
|
|
226
214
|
|
|
227
|
-
const
|
|
228
|
-
if (
|
|
229
|
-
|
|
215
|
+
const clientJs = readFileFromArchive(archiveBytes, 'client.js');
|
|
216
|
+
if (clientJs) {
|
|
217
|
+
const result = await installPackage(clientJs, meta, { loaded });
|
|
218
|
+
if (!result.success) {
|
|
219
|
+
console.warn(`[sh3-store] Server install ok but local hot-load failed: ${result.error}`);
|
|
220
|
+
}
|
|
230
221
|
}
|
|
231
222
|
|
|
232
223
|
await ctx.refreshInstalled();
|
|
@@ -17,13 +17,14 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { mount, unmount } from 'svelte';
|
|
19
19
|
import StoreView from './StoreView.svelte';
|
|
20
|
-
import { fetchRegistries,
|
|
20
|
+
import { fetchRegistries, fetchArchive, buildPackageMeta } from '../../registry/client';
|
|
21
21
|
import { installPackage, listInstalledPackages } from '../../registry/installer';
|
|
22
22
|
import { uninstallPackage as installerUninstallPackage } from '../../registry/installer';
|
|
23
23
|
import { loadBundle, loadMeta, savePackage } from '../../registry/storage';
|
|
24
24
|
import { loadBundleModule } from '../../registry/loader';
|
|
25
25
|
import { extractBundlePermissions } from '../../registry/permission-descriptions';
|
|
26
|
-
import {
|
|
26
|
+
import { readFileFromArchive } from '../../registry/archive';
|
|
27
|
+
import { requestServerInstall, fetchServerPackages, serverUninstallPackage } from '../../env/client';
|
|
27
28
|
import { VERSION } from '../../version';
|
|
28
29
|
import { installVerb, uninstallVerb, appinfoVerb, updateVerb } from './verbs';
|
|
29
30
|
import { isNewerVersion } from './version';
|
|
@@ -147,7 +148,7 @@ export const storeShard = {
|
|
|
147
148
|
await ctx.envUpdate({ registries });
|
|
148
149
|
}
|
|
149
150
|
async function updatePackage(id, confirmPermissionChange, version) {
|
|
150
|
-
var _a, _b
|
|
151
|
+
var _a, _b;
|
|
151
152
|
// Source the catalog entry. Without an explicit version we use the
|
|
152
153
|
// updatable map (which encodes the "newer than installed" check); with a
|
|
153
154
|
// version we look up the package in the full catalog so downgrades and
|
|
@@ -165,77 +166,62 @@ export const storeShard = {
|
|
|
165
166
|
if (!installedRecord)
|
|
166
167
|
return;
|
|
167
168
|
const picked = pickVersion(catalogEntry, version);
|
|
168
|
-
// 1. Fetch
|
|
169
|
-
const
|
|
170
|
-
let serverBundle;
|
|
171
|
-
if (picked.serverBundleUrl) {
|
|
172
|
-
serverBundle = await fetchServerBundle(picked, catalogEntry.sourceRegistry);
|
|
173
|
-
}
|
|
169
|
+
// 1. Fetch archive (verified against SRI).
|
|
170
|
+
const archiveBytes = await fetchArchive(picked, catalogEntry.sourceRegistry);
|
|
174
171
|
const meta = buildPackageMeta(catalogEntry, picked);
|
|
175
|
-
// 2.
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
172
|
+
// 2. Extract client.js for permission check and local install.
|
|
173
|
+
const clientJs = readFileFromArchive(archiveBytes, 'client.js');
|
|
174
|
+
// 3. Load module for permission extraction (if client bundle present).
|
|
175
|
+
let loaded;
|
|
176
|
+
let newPerms = [];
|
|
177
|
+
if (clientJs) {
|
|
178
|
+
loaded = await loadBundleModule(clientJs);
|
|
179
|
+
newPerms = extractBundlePermissions(loaded);
|
|
180
|
+
}
|
|
181
|
+
// 4. Look up the locally persisted old permissions.
|
|
181
182
|
let oldPerms = [];
|
|
182
183
|
try {
|
|
183
184
|
const localMeta = await loadMeta(id);
|
|
184
185
|
if (localMeta === null || localMeta === void 0 ? void 0 : localMeta.permissions)
|
|
185
186
|
oldPerms = localMeta.permissions;
|
|
186
187
|
}
|
|
187
|
-
catch (
|
|
188
|
-
// No local record
|
|
189
|
-
// empty. The diff will show all new permissions as additions.
|
|
188
|
+
catch (_c) {
|
|
189
|
+
// No local record; treat as empty.
|
|
190
190
|
}
|
|
191
|
-
//
|
|
192
|
-
// provided, await the user's decision before touching the server.
|
|
191
|
+
// 5. If the permission set changed, await the user's decision.
|
|
193
192
|
const { added, removed } = diffPermissions(oldPerms, newPerms);
|
|
194
193
|
if ((added.length > 0 || removed.length > 0) && confirmPermissionChange) {
|
|
195
194
|
const ok = await confirmPermissionChange(added, removed);
|
|
196
195
|
if (!ok)
|
|
197
196
|
return;
|
|
198
197
|
}
|
|
199
|
-
//
|
|
200
|
-
// permissions so the rollback write still satisfies the InstalledPackage
|
|
201
|
-
// contract (installedRecord came from the server-sourced list which
|
|
202
|
-
// lacks permissions).
|
|
198
|
+
// 6. Snapshot current state for rollback.
|
|
203
199
|
const oldBundle = await loadBundle(id);
|
|
204
200
|
const oldRecord = Object.assign(Object.assign({}, installedRecord), { permissions: oldPerms });
|
|
205
|
-
//
|
|
206
|
-
const
|
|
207
|
-
id: meta.id,
|
|
208
|
-
type: meta.type,
|
|
209
|
-
label: catalogEntry.entry.label,
|
|
210
|
-
version: meta.version,
|
|
211
|
-
contractVersion: meta.contractVersion,
|
|
212
|
-
sourceRegistry: meta.sourceRegistry,
|
|
213
|
-
installedAt: new Date().toISOString(),
|
|
214
|
-
requiredShards: (_b = (_a = picked.requires) === null || _a === void 0 ? void 0 : _a.map((r) => r.id)) !== null && _b !== void 0 ? _b : [],
|
|
215
|
-
};
|
|
216
|
-
const serverResult = await serverInstallPackage(manifest, bundle, serverBundle);
|
|
201
|
+
// 7. Tell server to install autonomously from the registry.
|
|
202
|
+
const serverResult = await requestServerInstall(catalogEntry.sourceRegistry, id, picked.version);
|
|
217
203
|
if (!serverResult.ok) {
|
|
218
|
-
let message = (
|
|
204
|
+
let message = (_a = serverResult.error) !== null && _a !== void 0 ? _a : 'Server update failed';
|
|
219
205
|
if (serverResult.code === 'missing-shards' && serverResult.missing) {
|
|
220
206
|
const ids = serverResult.missing.map((m) => m.id).join(', ');
|
|
221
207
|
message = `missing required shard(s): ${ids}`;
|
|
222
208
|
}
|
|
223
209
|
throw new Error(message);
|
|
224
210
|
}
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
211
|
+
// 8. Install locally from the already-fetched archive.
|
|
212
|
+
if (clientJs) {
|
|
213
|
+
const result = await installPackage(clientJs, meta, { loaded });
|
|
214
|
+
if (!result.success) {
|
|
215
|
+
if (oldBundle) {
|
|
216
|
+
try {
|
|
217
|
+
await savePackage(id, oldBundle, oldRecord);
|
|
218
|
+
}
|
|
219
|
+
catch (rollbackErr) {
|
|
220
|
+
console.warn(`[sh3-store] Rollback failed for "${id}":`, rollbackErr instanceof Error ? rollbackErr.message : rollbackErr);
|
|
221
|
+
}
|
|
236
222
|
}
|
|
223
|
+
throw new Error((_b = result.error) !== null && _b !== void 0 ? _b : 'Local install failed during update');
|
|
237
224
|
}
|
|
238
|
-
throw new Error((_d = result.error) !== null && _d !== void 0 ? _d : 'Local install failed during update');
|
|
239
225
|
}
|
|
240
226
|
await refreshInstalled();
|
|
241
227
|
}
|
package/dist/app/store/verbs.js
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* Auto-prefixed to sh3-store:install, sh3-store:uninstall, sh3-store:appinfo.
|
|
6
6
|
*/
|
|
7
7
|
import { storeContext } from './storeShard.svelte';
|
|
8
|
-
import {
|
|
8
|
+
import { fetchArchive, buildPackageMeta } from '../../registry/client';
|
|
9
|
+
import { readManifestFromArchive, readFileFromArchive } from '../../registry/archive';
|
|
10
|
+
import { requestServerInstall } from '../../env/client';
|
|
9
11
|
import { installPackage } from '../../registry/installer';
|
|
10
|
-
import { serverInstallPackage } from '../../env/client';
|
|
11
12
|
function findInCatalog(id) {
|
|
12
13
|
return storeContext.state.ephemeral.catalog.find((p) => p.entry.id === id);
|
|
13
14
|
}
|
|
@@ -19,7 +20,7 @@ export const installVerb = {
|
|
|
19
20
|
summary: 'Install a package by id from the catalog.',
|
|
20
21
|
programmatic: true,
|
|
21
22
|
async run(ctx, args) {
|
|
22
|
-
var _a
|
|
23
|
+
var _a;
|
|
23
24
|
const id = args[0];
|
|
24
25
|
if (!id) {
|
|
25
26
|
ctx.scrollback.push({
|
|
@@ -50,68 +51,36 @@ export const installVerb = {
|
|
|
50
51
|
});
|
|
51
52
|
return;
|
|
52
53
|
}
|
|
53
|
-
ctx.scrollback.push({
|
|
54
|
-
kind: 'status',
|
|
55
|
-
text: `installing ${id} v${pkg.latest.version}...`,
|
|
56
|
-
level: 'info',
|
|
57
|
-
ts: Date.now(),
|
|
58
|
-
});
|
|
54
|
+
ctx.scrollback.push({ kind: 'status', text: `installing ${id} v${pkg.latest.version}...`, level: 'info', ts: Date.now() });
|
|
59
55
|
try {
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const manifest = {
|
|
67
|
-
id: meta.id,
|
|
68
|
-
type: meta.type,
|
|
69
|
-
label: pkg.entry.label,
|
|
70
|
-
version: meta.version,
|
|
71
|
-
contractVersion: meta.contractVersion,
|
|
72
|
-
sourceRegistry: meta.sourceRegistry,
|
|
73
|
-
installedAt: new Date().toISOString(),
|
|
74
|
-
requiredShards: (_b = (_a = pkg.latest.requires) === null || _a === void 0 ? void 0 : _a.map((r) => r.id)) !== null && _b !== void 0 ? _b : [],
|
|
75
|
-
};
|
|
76
|
-
const serverResult = await serverInstallPackage(manifest, bundle, serverBundle);
|
|
56
|
+
// 1. Download archive for permission modal (future) and client local install
|
|
57
|
+
const archiveBytes = await fetchArchive(pkg.latest, pkg.sourceRegistry);
|
|
58
|
+
// 2. Extract manifest (permission modal reads from it)
|
|
59
|
+
readManifestFromArchive(archiveBytes);
|
|
60
|
+
// 3. Tell server to install autonomously
|
|
61
|
+
const serverResult = await requestServerInstall(pkg.sourceRegistry, id, pkg.latest.version);
|
|
77
62
|
if (!serverResult.ok) {
|
|
78
|
-
let text = `install failed: ${(
|
|
63
|
+
let text = `install failed: ${(_a = serverResult.error) !== null && _a !== void 0 ? _a : 'server error'}`;
|
|
79
64
|
if (serverResult.code === 'missing-shards' && serverResult.missing) {
|
|
80
|
-
|
|
81
|
-
text = `install failed: missing required shard(s): ${ids}`;
|
|
65
|
+
text = `install failed: missing required shard(s): ${serverResult.missing.map((m) => m.id).join(', ')}`;
|
|
82
66
|
}
|
|
83
|
-
ctx.scrollback.push({
|
|
84
|
-
kind: 'status',
|
|
85
|
-
text,
|
|
86
|
-
level: 'error',
|
|
87
|
-
ts: Date.now(),
|
|
88
|
-
});
|
|
67
|
+
ctx.scrollback.push({ kind: 'status', text, level: 'error', ts: Date.now() });
|
|
89
68
|
return;
|
|
90
69
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
ts: Date.now()
|
|
98
|
-
}
|
|
70
|
+
// 4. Extract client.js from already-fetched archive and install locally
|
|
71
|
+
const clientJs = readFileFromArchive(archiveBytes, 'client.js');
|
|
72
|
+
if (clientJs) {
|
|
73
|
+
const meta = buildPackageMeta(pkg, pkg.latest);
|
|
74
|
+
const result = await installPackage(clientJs, meta);
|
|
75
|
+
if (!result.success) {
|
|
76
|
+
ctx.scrollback.push({ kind: 'status', text: `server ok but local hot-load failed: ${result.error}`, level: 'warn', ts: Date.now() });
|
|
77
|
+
}
|
|
99
78
|
}
|
|
100
79
|
await storeContext.refreshInstalled();
|
|
101
|
-
ctx.scrollback.push({
|
|
102
|
-
kind: 'status',
|
|
103
|
-
text: `installed ${id} v${pkg.latest.version}`,
|
|
104
|
-
level: 'info',
|
|
105
|
-
ts: Date.now(),
|
|
106
|
-
});
|
|
80
|
+
ctx.scrollback.push({ kind: 'status', text: `installed ${id} v${pkg.latest.version}`, level: 'info', ts: Date.now() });
|
|
107
81
|
}
|
|
108
82
|
catch (err) {
|
|
109
|
-
ctx.scrollback.push({
|
|
110
|
-
kind: 'status',
|
|
111
|
-
text: `install failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
112
|
-
level: 'error',
|
|
113
|
-
ts: Date.now(),
|
|
114
|
-
});
|
|
83
|
+
ctx.scrollback.push({ kind: 'status', text: `install failed: ${err instanceof Error ? err.message : String(err)}`, level: 'error', ts: Date.now() });
|
|
115
84
|
}
|
|
116
85
|
},
|
|
117
86
|
};
|
package/dist/artifact.d.ts
CHANGED
|
@@ -8,10 +8,12 @@ export type SatellitePayload = {
|
|
|
8
8
|
h: number;
|
|
9
9
|
};
|
|
10
10
|
activateShards: string[];
|
|
11
|
+
projectId?: string;
|
|
11
12
|
} | {
|
|
12
13
|
kind: 'app';
|
|
13
14
|
appId: string;
|
|
14
15
|
activateShards: string[];
|
|
16
|
+
projectId?: string;
|
|
15
17
|
};
|
|
16
18
|
export declare function encodePayload(payload: SatellitePayload): string;
|
|
17
19
|
export declare function decodePayload(encoded: string): SatellitePayload;
|
|
@@ -36,6 +36,25 @@ describe('satellite payload encode/decode', () => {
|
|
|
36
36
|
const bad = btoa(JSON.stringify({ kind: 'mystery' }));
|
|
37
37
|
expect(() => decodePayload(bad)).toThrow();
|
|
38
38
|
});
|
|
39
|
+
it('round-trips a float payload with projectId', () => {
|
|
40
|
+
const payload = {
|
|
41
|
+
kind: 'float',
|
|
42
|
+
content: { type: 'slot', slotId: 's:1', viewId: 'foo:bar' },
|
|
43
|
+
size: { w: 800, h: 600 },
|
|
44
|
+
activateShards: ['foo'],
|
|
45
|
+
projectId: 'proj-abc',
|
|
46
|
+
};
|
|
47
|
+
expect(decodePayload(encodePayload(payload))).toEqual(payload);
|
|
48
|
+
});
|
|
49
|
+
it('round-trips an app payload with projectId', () => {
|
|
50
|
+
const payload = {
|
|
51
|
+
kind: 'app',
|
|
52
|
+
appId: 'sh3-store',
|
|
53
|
+
activateShards: ['sh3-core'],
|
|
54
|
+
projectId: 'proj-xyz',
|
|
55
|
+
};
|
|
56
|
+
expect(decodePayload(encodePayload(payload))).toEqual(payload);
|
|
57
|
+
});
|
|
39
58
|
it('produces a URL-safe encoding (no + / =)', () => {
|
|
40
59
|
// A long-ish payload increases the chance the underlying base64
|
|
41
60
|
// would contain non-URL-safe characters from the standard alphabet.
|
package/dist/build.d.ts
CHANGED
|
@@ -70,10 +70,16 @@ export declare function composeArtifactVersion(pkgVersion: string, suffix: strin
|
|
|
70
70
|
* After Vite's lib build completes, this plugin:
|
|
71
71
|
* 1. Finds the entry chunk and renames it to client.js
|
|
72
72
|
* 2. Optionally copies a pre-built server bundle as server.js
|
|
73
|
-
* 3. Extracts manifest fields (id, label, type) from the source
|
|
73
|
+
* 3. Extracts manifest fields (id, label, type, requiredShards) from the source
|
|
74
74
|
* 4. Reads `version` from `package.json.version` — the authoritative
|
|
75
75
|
* source per ADR-013. A literal `version:` in a source manifest is
|
|
76
76
|
* ignored; only `package.json.version` matters for the artifact.
|
|
77
77
|
* 5. Writes manifest.json alongside the bundles
|
|
78
78
|
*/
|
|
79
|
+
/**
|
|
80
|
+
* Extract the requiredShards array from a bundled source string.
|
|
81
|
+
* Finds the first `requiredShards: [...]` array literal and parses its string IDs.
|
|
82
|
+
* Exported for testing; used internally by sh3Artifact.
|
|
83
|
+
*/
|
|
84
|
+
export declare function extractRequiredShardsFromBundle(bundleSource: string): string[];
|
|
79
85
|
export declare function sh3Artifact(options?: Sh3ArtifactOptions): Plugin;
|
package/dist/build.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { readFileSync, writeFileSync, unlinkSync, readdirSync, copyFileSync, existsSync } from 'node:fs';
|
|
20
20
|
import { execSync } from 'node:child_process';
|
|
21
21
|
import { join } from 'node:path';
|
|
22
|
+
import { createArchive } from './registry/archive.js';
|
|
22
23
|
/**
|
|
23
24
|
* Vite plugin that inlines extracted CSS into the JS bundle.
|
|
24
25
|
*
|
|
@@ -136,12 +137,28 @@ function resolveAutoBuildSuffix() {
|
|
|
136
137
|
* After Vite's lib build completes, this plugin:
|
|
137
138
|
* 1. Finds the entry chunk and renames it to client.js
|
|
138
139
|
* 2. Optionally copies a pre-built server bundle as server.js
|
|
139
|
-
* 3. Extracts manifest fields (id, label, type) from the source
|
|
140
|
+
* 3. Extracts manifest fields (id, label, type, requiredShards) from the source
|
|
140
141
|
* 4. Reads `version` from `package.json.version` — the authoritative
|
|
141
142
|
* source per ADR-013. A literal `version:` in a source manifest is
|
|
142
143
|
* ignored; only `package.json.version` matters for the artifact.
|
|
143
144
|
* 5. Writes manifest.json alongside the bundles
|
|
144
145
|
*/
|
|
146
|
+
/**
|
|
147
|
+
* Extract the requiredShards array from a bundled source string.
|
|
148
|
+
* Finds the first `requiredShards: [...]` array literal and parses its string IDs.
|
|
149
|
+
* Exported for testing; used internally by sh3Artifact.
|
|
150
|
+
*/
|
|
151
|
+
export function extractRequiredShardsFromBundle(bundleSource) {
|
|
152
|
+
const arrayMatch = bundleSource.match(/\brequiredShards\s*:\s*\[([^\]]*)\]/);
|
|
153
|
+
if (!arrayMatch)
|
|
154
|
+
return [];
|
|
155
|
+
const ids = [];
|
|
156
|
+
const idRe = /["']([^"']+)["']/g;
|
|
157
|
+
let m;
|
|
158
|
+
while ((m = idRe.exec(arrayMatch[1])) !== null)
|
|
159
|
+
ids.push(m[1]);
|
|
160
|
+
return ids;
|
|
161
|
+
}
|
|
145
162
|
export function sh3Artifact(options = {}) {
|
|
146
163
|
let outDir = '';
|
|
147
164
|
let entryFileName = '';
|
|
@@ -238,14 +255,17 @@ export function sh3Artifact(options = {}) {
|
|
|
238
255
|
const m = block.match(pattern);
|
|
239
256
|
return m ? m[1] : '';
|
|
240
257
|
};
|
|
258
|
+
// Extract the requiredShards string-id array from the app manifest block.
|
|
259
|
+
const requiredShards = extractRequiredShardsFromBundle(block);
|
|
241
260
|
return {
|
|
242
261
|
id: get(/\bid\s*:\s*["']([^"']+)["']/),
|
|
243
262
|
label: get(/\blabel\s*:\s*["']([^"']+)["']/),
|
|
263
|
+
requiredShards,
|
|
244
264
|
};
|
|
245
265
|
}
|
|
246
266
|
// App first, then Shard.
|
|
247
267
|
const extracted = (_a = extractFromBlock(/\brequiredShards\s*:\s*\[/)) !== null && _a !== void 0 ? _a : extractFromBlock(/\bviews\s*:\s*\[/);
|
|
248
|
-
const { id, label } = extracted;
|
|
268
|
+
const { id, label, requiredShards } = extracted;
|
|
249
269
|
// --- Optional server bundle ---
|
|
250
270
|
let hasServer = false;
|
|
251
271
|
if (options.serverEntry && existsSync(options.serverEntry)) {
|
|
@@ -292,14 +312,19 @@ export function sh3Artifact(options = {}) {
|
|
|
292
312
|
if (!finalAuthor) {
|
|
293
313
|
throw new Error('[sh3-artifact] Missing "author". Add it to package.json or pass it via sh3Artifact({ manifest: { author } }).');
|
|
294
314
|
}
|
|
295
|
-
const manifest = Object.assign(Object.assign(Object.assign({ id: id || 'unknown', type, label: label || id || 'unknown', version: artifactVersion, contractVersion: 1
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const
|
|
315
|
+
const manifest = Object.assign(Object.assign(Object.assign(Object.assign({ id: id || 'unknown', type, label: label || id || 'unknown', version: artifactVersion, contractVersion: 1 }, (hasServer ? { server: 'server.js' } : {})), { description: finalDescription, author: finalAuthor }), ((type === 'app' || type === 'combo') ? { requiredShards } : {})), overrides);
|
|
316
|
+
// Read the emitted JS files as bytes for the archive
|
|
317
|
+
const clientBytes = readFileSync(join(outDir, 'client.js'));
|
|
318
|
+
const serverBytes = hasServer ? readFileSync(join(outDir, 'server.js')) : undefined;
|
|
319
|
+
// Create the .sh3pkg archive
|
|
320
|
+
const archive = createArchive(Object.assign({ manifest, client: new Uint8Array(clientBytes.buffer, clientBytes.byteOffset, clientBytes.byteLength) }, (serverBytes ? { server: new Uint8Array(serverBytes.buffer, serverBytes.byteOffset, serverBytes.byteLength) } : {})));
|
|
321
|
+
const archiveName = `${manifest.id}-${manifest.version}.sh3pkg`;
|
|
322
|
+
writeFileSync(join(outDir, archiveName), archive);
|
|
323
|
+
// Remove the loose files — the archive is the deliverable
|
|
324
|
+
unlinkSync(join(outDir, 'client.js'));
|
|
299
325
|
if (hasServer)
|
|
300
|
-
|
|
301
|
-
console.log(`[sh3-artifact] ${manifest.id}@${manifest.version} (${manifest.type}) → ${outDir}`);
|
|
302
|
-
console.log(`[sh3-artifact] files: ${files.join(', ')}`);
|
|
326
|
+
unlinkSync(join(outDir, 'server.js'));
|
|
327
|
+
console.log(`[sh3-artifact] ${manifest.id}@${manifest.version} (${manifest.type}) → ${outDir}/${archiveName}`);
|
|
303
328
|
},
|
|
304
329
|
};
|
|
305
330
|
}
|
package/dist/build.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { composeArtifactVersion } from './build';
|
|
2
|
+
import { composeArtifactVersion, extractRequiredShardsFromBundle } from './build';
|
|
3
3
|
describe('composeArtifactVersion', () => {
|
|
4
4
|
it('returns pkgVersion unchanged when suffix is undefined', () => {
|
|
5
5
|
expect(composeArtifactVersion('1.2.3', undefined)).toBe('1.2.3');
|
|
@@ -29,3 +29,29 @@ describe('composeArtifactVersion', () => {
|
|
|
29
29
|
expect(() => composeArtifactVersion('1.2.3', '+42')).toThrow(/not a valid semver build-metadata identifier/);
|
|
30
30
|
});
|
|
31
31
|
});
|
|
32
|
+
describe('extractRequiredShardsFromBundle', () => {
|
|
33
|
+
it('extracts a single shard id', () => {
|
|
34
|
+
const src = `const App = { manifest: { id: "my-app", requiredShards: ["guml.core"] } };`;
|
|
35
|
+
expect(extractRequiredShardsFromBundle(src)).toEqual(['guml.core']);
|
|
36
|
+
});
|
|
37
|
+
it('extracts multiple shard ids', () => {
|
|
38
|
+
const src = `requiredShards: ["guml.core", "sh3-file-explorer", "some-other"]`;
|
|
39
|
+
expect(extractRequiredShardsFromBundle(src)).toEqual(['guml.core', 'sh3-file-explorer', 'some-other']);
|
|
40
|
+
});
|
|
41
|
+
it('handles single-quoted ids', () => {
|
|
42
|
+
const src = `requiredShards: ['guml.core', 'sh3-files']`;
|
|
43
|
+
expect(extractRequiredShardsFromBundle(src)).toEqual(['guml.core', 'sh3-files']);
|
|
44
|
+
});
|
|
45
|
+
it('returns empty array when requiredShards is not present', () => {
|
|
46
|
+
const src = `const Shard = { manifest: { id: "my-shard", views: [] } };`;
|
|
47
|
+
expect(extractRequiredShardsFromBundle(src)).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
it('returns empty array when requiredShards is empty', () => {
|
|
50
|
+
const src = `requiredShards: []`;
|
|
51
|
+
expect(extractRequiredShardsFromBundle(src)).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
it('handles minified format without spaces', () => {
|
|
54
|
+
const src = `{id:"my-app",requiredShards:["guml.core","other-shard"]}`;
|
|
55
|
+
expect(extractRequiredShardsFromBundle(src)).toEqual(['guml.core', 'other-shard']);
|
|
56
|
+
});
|
|
57
|
+
});
|