@mosetta/runtime 0.0.0-stage → 0.0.9
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/LICENSE +21 -0
- package/README.md +15 -2
- package/package.json +26 -3
- package/src/assetRuntime.js +13 -0
- package/src/bootstrap.ts +363 -0
- package/src/client/appContainer.ts +86 -0
- package/src/client/client.ts +12 -0
- package/src/client/getIsClient.ts +3 -0
- package/src/client/headWriter.ts +85 -0
- package/src/client/pageManager.ts +55 -0
- package/src/client/refs.ts +319 -0
- package/src/client/router.ts +453 -0
- package/src/client/serverRender.ts +12 -0
- package/src/client/tile.ts +7 -0
- package/src/client/tokens.ts +70 -0
- package/src/domEvents.ts +118 -0
- package/src/naming.d.ts +6 -0
- package/src/naming.js +25 -0
- package/src/resolver.d.ts +29 -0
- package/src/resolver.js +137 -0
- package/src/tileLoader.js +42 -0
package/src/naming.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Canonical name encoding + external-dependency identity.
|
|
2
|
+
// Pure & browser-safe (no imports) — single source of truth shared by the build (src/*) and
|
|
3
|
+
// the runtime resolver (runtime/resolver.js), so they can never disagree on how ids are formed.
|
|
4
|
+
|
|
5
|
+
export const normalizeSpecifier = (s) => s.replace(/^@/, 'a_').replace(/[\\/]/g, '_');
|
|
6
|
+
|
|
7
|
+
export const normalizeVersion = (v) => String(v).trim().replace(/[^a-zA-Z0-9]/g, '_');
|
|
8
|
+
|
|
9
|
+
// Version-qualified id of an external dependency artifact: identity is (specifier, version).
|
|
10
|
+
export const dependencyFullId = (specifier, version) =>
|
|
11
|
+
`_ns_package_t_package_n_${normalizeSpecifier(specifier)}_v_${normalizeVersion(version)}`;
|
|
12
|
+
|
|
13
|
+
const DEP_PREFIX = '_ns_package_t_package_n_';
|
|
14
|
+
|
|
15
|
+
// Split a dependency full-id back into its normalized parts (version = last `_v_` segment,
|
|
16
|
+
// which is unambiguous because a normalized version never itself contains `_v_`).
|
|
17
|
+
export function parseDependencyFullId(id) {
|
|
18
|
+
if (typeof id !== 'string' || !id.startsWith(DEP_PREFIX)) return null;
|
|
19
|
+
const body = id.slice(DEP_PREFIX.length);
|
|
20
|
+
const i = body.lastIndexOf('_v_');
|
|
21
|
+
if (i < 0) return null;
|
|
22
|
+
return { normSpecifier: body.slice(0, i), normVersion: body.slice(i + 3) };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const isDependencyFullId = (id) => parseDependencyFullId(id) !== null;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Types for the plain-JS resolver: this package ships JS on purpose (node imports it
|
|
2
|
+
// untranspiled), so its types live beside it — a consumer in another package must not have to
|
|
3
|
+
// declare them again. They used to be an ambient shim inside @mosetta/server, invisible to
|
|
4
|
+
// everyone else.
|
|
5
|
+
export interface IRuntimeResolver {
|
|
6
|
+
resolveUrl(id: string, parentUrl?: string): string | null;
|
|
7
|
+
setConfig(cfg: unknown): void;
|
|
8
|
+
readonly shared: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface IResolverOptions {
|
|
12
|
+
base?: string;
|
|
13
|
+
config?: unknown;
|
|
14
|
+
warn?: (message: string) => void;
|
|
15
|
+
/** the artifact's integrity when known — the url is then content-addressed (`?v=`) */
|
|
16
|
+
integrityOf?: (id: string) => string | undefined;
|
|
17
|
+
/** the base of the code source the catalog places the id with; undefined — the default rules */
|
|
18
|
+
baseOf?: (id: string) => string | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createResolver(opts?: IResolverOptions): IRuntimeResolver;
|
|
22
|
+
|
|
23
|
+
export function installResolver(system: unknown, opts?: IResolverOptions): IRuntimeResolver;
|
|
24
|
+
|
|
25
|
+
/** the url-safe version token of an integrity (`sha256-…`); null without one */
|
|
26
|
+
export function versionTokenOf(integrity: string | undefined | null): string | null;
|
|
27
|
+
|
|
28
|
+
/** `url` addressed by its content: `?v=<token>` appended when the integrity is known */
|
|
29
|
+
export function versionedUrl(url: string, integrity: string | undefined | null): string;
|
package/src/resolver.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Runtime dependency resolver. Replaces the enumerated import map: id→URL is a pure
|
|
2
|
+
// formula over the version-qualified full-id, so the only real data is policy (singleton/pin).
|
|
3
|
+
// The resolver is package-agnostic: it bakes in NO package names. Version-alignment policy is data,
|
|
4
|
+
// supplied by the caller — it lives in the app node's settings in the page tree (content-manager
|
|
5
|
+
// configured in the editor), not in this runtime.
|
|
6
|
+
import {
|
|
7
|
+
normalizeSpecifier,
|
|
8
|
+
normalizeVersion,
|
|
9
|
+
dependencyFullId,
|
|
10
|
+
parseDependencyFullId,
|
|
11
|
+
isDependencyFullId,
|
|
12
|
+
} from './naming.js';
|
|
13
|
+
|
|
14
|
+
// CONTENT-ADDRESSED ARTIFACT URLS. An artifact's address carries its integrity: `…/<id>.js?v=<hash>`.
|
|
15
|
+
// A versioned folder alone is not enough — a rebuild under an unchanged version (every deploy
|
|
16
|
+
// of a 0.0.1 distribution) replaces the bytes behind the same address, and a cache that still
|
|
17
|
+
// holds the old ones (the CDN edge, the browser) serves them against a page that pins the new
|
|
18
|
+
// digest: the browser refuses them. With the hash in the address new bytes are a new address.
|
|
19
|
+
// The version token is the integrity digest itself, url-safe and short — the SAME function builds
|
|
20
|
+
// the shell's preload links, its boot scripts and every url the browser loader resolves, so a
|
|
21
|
+
// preloaded script and the script that uses it are one address.
|
|
22
|
+
/** the url-safe version token of an integrity (`sha256-…`); null without one */
|
|
23
|
+
export function versionTokenOf(integrity) {
|
|
24
|
+
if (typeof integrity !== 'string' || !integrity) return null;
|
|
25
|
+
const digest = integrity.replace(/^sha\d+-/, '').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
26
|
+
return digest ? digest.slice(0, 16) : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** `url` addressed by its content: `?v=<token>` appended when the integrity is known */
|
|
30
|
+
export function versionedUrl(url, integrity) {
|
|
31
|
+
const token = versionTokenOf(integrity);
|
|
32
|
+
if (!token) return url;
|
|
33
|
+
return `${url}${url.includes('?') ? '&' : '?'}v=${token}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Distributive root of the importer: artifacts live at <root>/{modules,dependencies,runtime}/...
|
|
37
|
+
// Derived from parentUrl so an ISOLATED dep resolves against its importer's OWN provider
|
|
38
|
+
// (multi-provider co-location), falling back to a configured base for top-level imports (no parent).
|
|
39
|
+
function providerRootOf(parentUrl, fallbackBase) {
|
|
40
|
+
if (parentUrl) {
|
|
41
|
+
let best = -1;
|
|
42
|
+
for (const seg of ['/modules/', '/runtime/']) {
|
|
43
|
+
const i = parentUrl.lastIndexOf(seg);
|
|
44
|
+
if (i > best) best = i;
|
|
45
|
+
}
|
|
46
|
+
if (best >= 0) return parentUrl.slice(0, best + 1); // keep the leading slash → ends with '/'
|
|
47
|
+
}
|
|
48
|
+
return fallbackBase;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Apply version policy to an incoming dep full-id. Returns the canonical full-id to load plus flags:
|
|
52
|
+
// - singleton: must be ONE instance per page → resolve against the host origin (base), not parentUrl.
|
|
53
|
+
// - version remap (pin/version): every version of the pkg collapses to the canonical one.
|
|
54
|
+
// A pkg absent from `shared` is isolatable: identity + co-located origin (multiple versions coexist).
|
|
55
|
+
function applyPolicy(id, shared) {
|
|
56
|
+
const parsed = parseDependencyFullId(id);
|
|
57
|
+
if (!parsed) return { target: id, singleton: false };
|
|
58
|
+
for (const key of Object.keys(shared)) {
|
|
59
|
+
if (parsed.normSpecifier !== normalizeSpecifier(key)) continue;
|
|
60
|
+
const rule = shared[key] || {};
|
|
61
|
+
const ver = rule.pin || rule.version; // explicit canonical version, if declared
|
|
62
|
+
return {
|
|
63
|
+
target: ver ? dependencyFullId(key, ver) : id,
|
|
64
|
+
singleton: rule.singleton === true,
|
|
65
|
+
key,
|
|
66
|
+
requested: parsed.normVersion,
|
|
67
|
+
canonical: ver ? normalizeVersion(ver) : parsed.normVersion,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return { target: id, singleton: false };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create a resolver. `base` is the host distributive root (where the page/app module is served) —
|
|
75
|
+
* SINGLETONS resolve against it so every component, regardless of its own provider origin, shares
|
|
76
|
+
* one instance. `config` is `{ shared }`. `setConfig` lets the bootstrap refine policy before
|
|
77
|
+
* importing page modules.
|
|
78
|
+
*
|
|
79
|
+
* `integrityOf(id)` — the artifact's integrity when known: the url is then content-addressed.
|
|
80
|
+
*
|
|
81
|
+
* `baseOf(id)` — where the page's catalog says the entity lives (the base of its code source, when
|
|
82
|
+
* it is not the host's): an id the catalog places is read THERE, singleton or not — a site's code
|
|
83
|
+
* may come from several sources, and a package one source's blocks import may lie with another.
|
|
84
|
+
* An id it does not place falls back to the rules above.
|
|
85
|
+
*
|
|
86
|
+
* @param {{ base?: string, config?: { shared?: Record<string, unknown> }, warn?: (m: string) => void, integrityOf?: (id: string) => string | undefined, baseOf?: (id: string) => string | undefined }} [options]
|
|
87
|
+
*/
|
|
88
|
+
export function createResolver({ base, config, warn = (m) => console.warn(m), integrityOf, baseOf } = {}) {
|
|
89
|
+
let shared = { ...(config && config.shared) };
|
|
90
|
+
const warned = new Set();
|
|
91
|
+
|
|
92
|
+
function maybeWarn(p, parentUrl) {
|
|
93
|
+
if (p.requested === p.canonical) return;
|
|
94
|
+
const k = `${p.key}:${p.requested}->${p.canonical}`;
|
|
95
|
+
if (warned.has(k)) return;
|
|
96
|
+
warned.add(k);
|
|
97
|
+
const major = (v) => v.split('_')[0];
|
|
98
|
+
const sev = major(p.requested) !== major(p.canonical)
|
|
99
|
+
? 'major mismatch — likely incompatible'
|
|
100
|
+
: 'version substitution';
|
|
101
|
+
const dot = (v) => v.replace(/_/g, '.');
|
|
102
|
+
warn(`[mosetta] singleton ${p.key}: ${dot(p.requested)} → ${dot(p.canonical)} (${sev})`
|
|
103
|
+
+ (parentUrl ? ` for ${parentUrl}` : ''));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
setConfig(cfg) {
|
|
108
|
+
shared = { ...(cfg && cfg.shared) };
|
|
109
|
+
},
|
|
110
|
+
get shared() {
|
|
111
|
+
return shared;
|
|
112
|
+
},
|
|
113
|
+
// returns a resolved URL for a dep full-id, or null if `id` is not ours (caller falls back)
|
|
114
|
+
resolveUrl(id, parentUrl) {
|
|
115
|
+
if (!isDependencyFullId(id)) return null;
|
|
116
|
+
const p = applyPolicy(id, shared);
|
|
117
|
+
// where the catalog places the (canonical) id; else singleton → canonical HOST origin (base)
|
|
118
|
+
// so cross-provider components dedup to one instance; isolated → co-located with the importer
|
|
119
|
+
const root = (baseOf && baseOf(p.target)) || (p.singleton ? base : providerRootOf(parentUrl, base));
|
|
120
|
+
if (p.singleton) maybeWarn(p, parentUrl);
|
|
121
|
+
// content-addressed when the caller knows the artifact's integrity (the browser, from the
|
|
122
|
+
// page's catalog); the server links by address and passes none
|
|
123
|
+
return versionedUrl(new URL(`modules/${p.target}/${p.target}.js`, root).href, integrityOf ? integrityOf(p.target) : undefined);
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Install the resolver as a SystemJS `resolve` hook (instance-level, leaves other instances alone).
|
|
129
|
+
export function installResolver(System, opts = {}) {
|
|
130
|
+
const resolver = createResolver(opts);
|
|
131
|
+
const orig = System.resolve.bind(System);
|
|
132
|
+
System.resolve = (id, parentUrl) => {
|
|
133
|
+
const url = resolver.resolveUrl(id, parentUrl);
|
|
134
|
+
return url !== null ? url : orig(id, parentUrl);
|
|
135
|
+
};
|
|
136
|
+
return resolver;
|
|
137
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Safe, shared module loader. The ONE mechanism for loading a module —
|
|
2
|
+
// used both by the initial bootstrap preload and by later dynamic loads. `load()` ALWAYS resolves
|
|
3
|
+
// to an ILoadedTile: { reference, module } on success, { reference, module: undefined, error } on
|
|
4
|
+
// failure — it NEVER rejects. So Promise.all over many loads can't blow up the app, and the React
|
|
5
|
+
// layer consumes the very same ILoadedTile shape. Results are cached per reference (dedup + reuse).
|
|
6
|
+
export function createTileLoader(System, urlOf) {
|
|
7
|
+
const pending = new Map(); // ref -> Promise<ILoadedTile> (in flight)
|
|
8
|
+
const resolved = new Map(); // ref -> ILoadedTile (settled)
|
|
9
|
+
|
|
10
|
+
function load(ref, providerBase) {
|
|
11
|
+
const done = resolved.get(ref);
|
|
12
|
+
if (done) return Promise.resolve(done);
|
|
13
|
+
const inflight = pending.get(ref);
|
|
14
|
+
if (inflight) return inflight;
|
|
15
|
+
|
|
16
|
+
const p = Promise.resolve()
|
|
17
|
+
.then(() => System.import(urlOf(ref, providerBase)))
|
|
18
|
+
.then(
|
|
19
|
+
(ns) => ({ reference: ref, module: ns.default, error: undefined }),
|
|
20
|
+
(error) => ({ reference: ref, module: undefined, error }),
|
|
21
|
+
)
|
|
22
|
+
.then((im) => {
|
|
23
|
+
resolved.set(ref, im);
|
|
24
|
+
pending.delete(ref);
|
|
25
|
+
return im;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
pending.set(ref, p);
|
|
29
|
+
return p;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
load, // (ref, providerBase?) => Promise<ILoadedTile> — always resolves
|
|
34
|
+
get: (ref) => resolved.get(ref), // ILoadedTile | undefined — sync accessor for the holder
|
|
35
|
+
// a settled FAILURE is forgotten, so the next load tries again (a later page asking for a
|
|
36
|
+
// module the first one could not reach); a success stays
|
|
37
|
+
forget: (ref) => {
|
|
38
|
+
const done = resolved.get(ref);
|
|
39
|
+
if (done && done.error) resolved.delete(ref);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|