@rmc-toolkit/core 0.1.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.
@@ -0,0 +1,4 @@
1
+ export declare const namedExport = "named-value";
2
+ declare const _default: "default-value";
3
+ export default _default;
4
+ //# sourceMappingURL=dummy-module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dummy-module.d.ts","sourceRoot":"","sources":["../../src/__fixtures__/dummy-module.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,gBAAgB,CAAC;wBAC1B,eAAe;AAA9B,wBAA+B"}
@@ -0,0 +1,2 @@
1
+ export const namedExport = "named-value";
2
+ export default "default-value";
@@ -0,0 +1,4 @@
1
+ import type { RuntimeCompositionManifest } from "./types.js";
2
+ export declare const listExternalSpecifiers: (manifest: RuntimeCompositionManifest) => string[];
3
+ export declare const createExternalMatcher: (manifest: RuntimeCompositionManifest) => ((source: string) => boolean);
4
+ //# sourceMappingURL=externals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"externals.d.ts","sourceRoot":"","sources":["../src/externals.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,0BAA0B,EAE3B,MAAM,YAAY,CAAC;AAKpB,eAAO,MAAM,sBAAsB,GACjC,UAAU,0BAA0B,KACnC,MAAM,EAiBR,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,UAAU,0BAA0B,KACnC,CAAC,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAW9B,CAAC"}
@@ -0,0 +1,26 @@
1
+ const isSharedExternal = (config) => typeof config === "string" ? true : config.external !== false;
2
+ export const listExternalSpecifiers = (manifest) => {
3
+ const prefixes = [
4
+ `${manifest.namespace}/`,
5
+ manifest.externalDepsOrigin
6
+ ? (manifest.externalDepsPrefix ?? "@esm.sh/")
7
+ : null,
8
+ ].filter((value) => Boolean(value));
9
+ const exactImportSpecifiers = Object.entries(manifest.exactImports ?? {})
10
+ .filter(([, config]) => isSharedExternal(config))
11
+ .map(([specifier]) => specifier);
12
+ const sliceOverrideSpecifiers = Object.values(manifest.sliceOverrides ?? {})
13
+ .filter((slice) => slice.external !== false)
14
+ .map((slice) => slice.specifier);
15
+ return [...prefixes, ...exactImportSpecifiers, ...sliceOverrideSpecifiers];
16
+ };
17
+ export const createExternalMatcher = (manifest) => {
18
+ const exactSpecifiers = new Set(listExternalSpecifiers(manifest));
19
+ const namespacePrefix = `${manifest.namespace}/`;
20
+ const externalDepsPrefix = manifest.externalDepsOrigin
21
+ ? (manifest.externalDepsPrefix ?? "@esm.sh/")
22
+ : null;
23
+ return (source) => exactSpecifiers.has(source) ||
24
+ source.startsWith(namespacePrefix) ||
25
+ (externalDepsPrefix ? source.startsWith(externalDepsPrefix) : false);
26
+ };
package/dist/host.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import type { DynamicImporter, RuntimeCompositionManifest } from "./types.js";
2
+ export type RuntimeHostOptions = {
3
+ manifest: RuntimeCompositionManifest;
4
+ target: Element;
5
+ onLoading?: (path: string) => void;
6
+ onError?: (error: unknown, path: string) => void;
7
+ importer?: DynamicImporter;
8
+ };
9
+ export type RuntimeHost = {
10
+ resolveAndMount(path: string): Promise<void>;
11
+ destroy(): Promise<void>;
12
+ };
13
+ export declare const createRuntimeHost: (options: RuntimeHostOptions) => RuntimeHost;
14
+ export declare const notifyInternalNavigation: (path: string) => void;
15
+ //# sourceMappingURL=host.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,eAAe,EACf,0BAA0B,EAE3B,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,0BAA0B,CAAC;IACrC,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,SAAS,kBAAkB,KAAG,WA2G/D,CAAC;AAEF,eAAO,MAAM,wBAAwB,GAAI,MAAM,MAAM,KAAG,IAGvD,CAAC"}
package/dist/host.js ADDED
@@ -0,0 +1,100 @@
1
+ import { importModule, unwrapDefault } from "./loader.js";
2
+ import { resolveRoute } from "./routes.js";
3
+ export const createRuntimeHost = (options) => {
4
+ const { manifest, target, importer } = options;
5
+ const onLoading = options.onLoading ?? (() => { });
6
+ // Reuse one generic message for both "no route matched" and "import/mount
7
+ // failed" — the default UI's job is "something's wrong", not a diagnostic
8
+ // surface; pass a custom onError to differentiate them.
9
+ const onError = options.onError ??
10
+ ((error, path) => {
11
+ console.error(`Failed to load slice for ${path}:`, error);
12
+ target.textContent = `Error: failed to load slice for ${path}`;
13
+ });
14
+ let latestToken = 0;
15
+ let currentSpecifier = null;
16
+ let currentModule = null;
17
+ const resetAndReportError = (error, path) => {
18
+ currentModule = null;
19
+ currentSpecifier = null;
20
+ onError(error, path);
21
+ };
22
+ const resolveAndMount = async (path) => {
23
+ const token = ++latestToken;
24
+ const match = resolveRoute(manifest, path);
25
+ if (!match) {
26
+ try {
27
+ if (currentModule) {
28
+ await currentModule.unmount?.();
29
+ }
30
+ }
31
+ catch (error) {
32
+ if (token === latestToken) {
33
+ resetAndReportError(error, path);
34
+ }
35
+ return;
36
+ }
37
+ if (token === latestToken) {
38
+ resetAndReportError(new Error(`No slice matches ${path}`), path);
39
+ }
40
+ return;
41
+ }
42
+ // Same module already mounted: everything past this point is the
43
+ // module's own responsibility, not the host's (e.g. its own internal
44
+ // sub-routing) — so there is nothing further to do here.
45
+ if (match.specifier === currentSpecifier) {
46
+ return;
47
+ }
48
+ onLoading(path);
49
+ try {
50
+ const runtimeModule = unwrapDefault(await importModule(match.specifier, importer));
51
+ if (token !== latestToken) {
52
+ return;
53
+ }
54
+ if (currentModule) {
55
+ await currentModule.unmount?.();
56
+ }
57
+ // Re-check after unmount: it's an arbitrary, user-authored async
58
+ // function that can take any amount of time, so a newer call may have
59
+ // become latest while this one was awaiting it. Without this check, a
60
+ // slow unmount could let this call commit stale state on top of a
61
+ // newer call's already-mounted module.
62
+ if (token !== latestToken) {
63
+ return;
64
+ }
65
+ currentSpecifier = match.specifier;
66
+ currentModule = runtimeModule;
67
+ // Accepted, narrower limitation: once mount() is actually called,
68
+ // there's no clean way to cancel it if a still-newer call starts and
69
+ // reaches its own mount() before this one finishes. Closing that would
70
+ // require serializing all resolveAndMount() calls behind a queue/lock,
71
+ // which is out of scope for the specific bug this token fix closes
72
+ // (the earlier, easily-hit case of an older call clobbering a newer
73
+ // call's already-*settled* mount).
74
+ await runtimeModule.mount(target, { route: match, manifest });
75
+ }
76
+ catch (error) {
77
+ if (token === latestToken) {
78
+ resetAndReportError(error, path);
79
+ }
80
+ }
81
+ };
82
+ const destroy = async () => {
83
+ latestToken += 1;
84
+ if (currentModule) {
85
+ await currentModule.unmount?.();
86
+ }
87
+ currentModule = null;
88
+ currentSpecifier = null;
89
+ // Same accepted, narrower limitation as the resolveAndMount-vs-
90
+ // resolveAndMount case above: if a resolveAndMount() call is already
91
+ // inside its own (uninterruptible) mount() when destroy() runs, that
92
+ // mount can still finish after this promise resolves, leaving something
93
+ // mounted that this host's own bookkeeping no longer reflects.
94
+ };
95
+ return { resolveAndMount, destroy };
96
+ };
97
+ export const notifyInternalNavigation = (path) => {
98
+ window.history.pushState({}, "", path);
99
+ window.dispatchEvent(new PopStateEvent("popstate"));
100
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=host.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host.test.d.ts","sourceRoot":"","sources":["../src/host.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,232 @@
1
+ // @vitest-environment happy-dom
2
+ import { describe, expect, test, vi } from "vitest";
3
+ import { defineManifest } from "./manifest.js";
4
+ import { createRuntimeHost, notifyInternalNavigation } from "./host.js";
5
+ const manifest = defineManifest({
6
+ namespace: "@acme",
7
+ assetsOrigin: "https://assets.example.com",
8
+ sliceOverrides: {
9
+ search: { route: "/search/*", specifier: "@acme/search/index.mjs", entry: "index.mjs" },
10
+ cart: { route: "/cart/*", specifier: "@acme/cart/index.mjs", entry: "index.mjs" },
11
+ wishlist: { route: "/wishlist/*", specifier: "@acme/wishlist/index.mjs", entry: "index.mjs" },
12
+ },
13
+ });
14
+ const createMockModule = () => ({
15
+ mount: vi.fn(async () => { }),
16
+ unmount: vi.fn(async () => { }),
17
+ });
18
+ describe("notifyInternalNavigation", () => {
19
+ test("pushes the given path and dispatches a popstate event", () => {
20
+ const pushStateSpy = vi.spyOn(window.history, "pushState");
21
+ const popstateListener = vi.fn();
22
+ window.addEventListener("popstate", popstateListener);
23
+ notifyInternalNavigation("/search/detail");
24
+ expect(pushStateSpy).toHaveBeenCalledWith({}, "", "/search/detail");
25
+ expect(popstateListener).toHaveBeenCalledTimes(1);
26
+ window.removeEventListener("popstate", popstateListener);
27
+ pushStateSpy.mockRestore();
28
+ });
29
+ });
30
+ describe("createRuntimeHost", () => {
31
+ test("resolves a route, imports the module, and mounts it with route + manifest context", async () => {
32
+ const searchModule = createMockModule();
33
+ const importer = vi.fn(async () => ({ default: searchModule }));
34
+ const target = document.createElement("div");
35
+ const host = createRuntimeHost({ manifest, target, importer });
36
+ await host.resolveAndMount("/search");
37
+ expect(importer).toHaveBeenCalledWith("@acme/search/index.mjs");
38
+ expect(searchModule.mount).toHaveBeenCalledTimes(1);
39
+ const [mountTarget, mountContext] = searchModule.mount.mock.calls[0];
40
+ expect(mountTarget).toBe(target);
41
+ expect(mountContext.manifest).toBe(manifest);
42
+ expect(mountContext.route.specifier).toBe("@acme/search/index.mjs");
43
+ });
44
+ test("unmounts the previous module before mounting a module for a different specifier", async () => {
45
+ const searchModule = createMockModule();
46
+ const cartModule = createMockModule();
47
+ const importer = vi.fn(async (specifier) => ({
48
+ default: specifier === "@acme/search/index.mjs" ? searchModule : cartModule,
49
+ }));
50
+ const target = document.createElement("div");
51
+ const host = createRuntimeHost({ manifest, target, importer });
52
+ await host.resolveAndMount("/search");
53
+ await host.resolveAndMount("/cart");
54
+ expect(searchModule.unmount).toHaveBeenCalledTimes(1);
55
+ expect(cartModule.mount).toHaveBeenCalledTimes(1);
56
+ });
57
+ test("navigating to the same specifier again is a no-op", async () => {
58
+ const searchModule = createMockModule();
59
+ const importer = vi.fn(async () => ({ default: searchModule }));
60
+ const target = document.createElement("div");
61
+ const host = createRuntimeHost({ manifest, target, importer });
62
+ await host.resolveAndMount("/search");
63
+ await host.resolveAndMount("/search/results");
64
+ expect(searchModule.mount).toHaveBeenCalledTimes(1);
65
+ expect(searchModule.unmount).not.toHaveBeenCalled();
66
+ });
67
+ test("calls onError with a descriptive message when no route matches", async () => {
68
+ const onError = vi.fn();
69
+ const target = document.createElement("div");
70
+ const importer = vi.fn();
71
+ const host = createRuntimeHost({ manifest, target, importer, onError });
72
+ await host.resolveAndMount("/");
73
+ expect(importer).not.toHaveBeenCalled();
74
+ expect(onError).toHaveBeenCalledTimes(1);
75
+ const [error, path] = onError.mock.calls[0];
76
+ expect(error).toBeInstanceOf(Error);
77
+ expect(error.message).toBe("No slice matches /");
78
+ expect(path).toBe("/");
79
+ });
80
+ test("default onError logs to console.error and writes a message into target", async () => {
81
+ const target = document.createElement("div");
82
+ const importer = vi.fn();
83
+ const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => { });
84
+ const host = createRuntimeHost({ manifest, target, importer });
85
+ await host.resolveAndMount("/");
86
+ expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
87
+ expect(target.textContent).toBe("Error: failed to load slice for /");
88
+ consoleErrorSpy.mockRestore();
89
+ });
90
+ test("calls onError when the importer rejects, and resets state so a later navigation isn't blocked", async () => {
91
+ const onError = vi.fn();
92
+ const target = document.createElement("div");
93
+ const importer = vi.fn(async () => {
94
+ throw new Error("network failure");
95
+ });
96
+ const host = createRuntimeHost({ manifest, target, importer, onError });
97
+ await host.resolveAndMount("/search");
98
+ expect(onError).toHaveBeenCalledWith(new Error("network failure"), "/search");
99
+ const searchModule = createMockModule();
100
+ importer.mockImplementation(async () => ({ default: searchModule }));
101
+ await host.resolveAndMount("/search");
102
+ expect(searchModule.mount).toHaveBeenCalledTimes(1);
103
+ });
104
+ test("calls onError when mount() throws", async () => {
105
+ const onError = vi.fn();
106
+ const target = document.createElement("div");
107
+ const failingModule = {
108
+ mount: vi.fn(async () => {
109
+ throw new Error("mount blew up");
110
+ }),
111
+ };
112
+ const importer = vi.fn(async () => ({ default: failingModule }));
113
+ const host = createRuntimeHost({ manifest, target, importer, onError });
114
+ await host.resolveAndMount("/search");
115
+ expect(onError).toHaveBeenCalledWith(new Error("mount blew up"), "/search");
116
+ });
117
+ test("calls onLoading with the path before importing a new module, but not for a same-specifier no-op", async () => {
118
+ const onLoading = vi.fn();
119
+ const searchModule = createMockModule();
120
+ const importer = vi.fn(async () => ({ default: searchModule }));
121
+ const target = document.createElement("div");
122
+ const host = createRuntimeHost({ manifest, target, importer, onLoading });
123
+ await host.resolveAndMount("/search");
124
+ await host.resolveAndMount("/search/results");
125
+ expect(onLoading).toHaveBeenCalledTimes(1);
126
+ expect(onLoading).toHaveBeenCalledWith("/search");
127
+ });
128
+ test("calls onError (not the 'no slice matches' message) when unmounting during a no-match navigation itself throws", async () => {
129
+ const onError = vi.fn();
130
+ const target = document.createElement("div");
131
+ const searchModule = {
132
+ mount: vi.fn(async () => { }),
133
+ unmount: vi.fn(async () => {
134
+ throw new Error("unmount blew up");
135
+ }),
136
+ };
137
+ const importer = vi.fn(async () => ({ default: searchModule }));
138
+ const host = createRuntimeHost({ manifest, target, importer, onError });
139
+ await host.resolveAndMount("/search");
140
+ await host.resolveAndMount("/");
141
+ expect(onError).toHaveBeenCalledTimes(1);
142
+ expect(onError).toHaveBeenCalledWith(new Error("unmount blew up"), "/");
143
+ });
144
+ test("discards a stale import: only the last-requested module ends up mounted", async () => {
145
+ const searchModule = createMockModule();
146
+ const cartModule = createMockModule();
147
+ const target = document.createElement("div");
148
+ let resolveSearchImport;
149
+ const searchImportPromise = new Promise((resolve) => {
150
+ resolveSearchImport = resolve;
151
+ });
152
+ const importer = vi.fn(async (specifier) => {
153
+ if (specifier === "@acme/search/index.mjs") {
154
+ return searchImportPromise;
155
+ }
156
+ return { default: cartModule };
157
+ });
158
+ const host = createRuntimeHost({ manifest, target, importer });
159
+ const firstCall = host.resolveAndMount("/search");
160
+ const secondCall = host.resolveAndMount("/cart");
161
+ await secondCall;
162
+ expect(cartModule.mount).toHaveBeenCalledTimes(1);
163
+ resolveSearchImport({ default: searchModule });
164
+ await firstCall;
165
+ expect(searchModule.mount).not.toHaveBeenCalled();
166
+ expect(searchModule.unmount).not.toHaveBeenCalled();
167
+ expect(cartModule.unmount).not.toHaveBeenCalled();
168
+ });
169
+ test("discards a stale call even when a newer call starts while this call is still awaiting its own unmount()", async () => {
170
+ const target = document.createElement("div");
171
+ const initialModule = createMockModule();
172
+ const staleModule = createMockModule();
173
+ const winningModule = createMockModule();
174
+ let resolveUnmount;
175
+ const unmountPromise = new Promise((resolve) => {
176
+ resolveUnmount = resolve;
177
+ });
178
+ initialModule.unmount.mockImplementation(() => unmountPromise);
179
+ const importer = vi.fn(async (specifier) => {
180
+ if (specifier === "@acme/search/index.mjs") {
181
+ return { default: initialModule };
182
+ }
183
+ if (specifier === "@acme/cart/index.mjs") {
184
+ return { default: staleModule };
185
+ }
186
+ return { default: winningModule };
187
+ });
188
+ const host = createRuntimeHost({ manifest, target, importer });
189
+ await host.resolveAndMount("/search");
190
+ // Stale call: its own import (cart) resolves immediately, so it passes
191
+ // the post-import staleness check while it's still the latest call, then
192
+ // blocks on unmounting `initialModule` (the shared, manually-controlled
193
+ // promise below).
194
+ const staleCall = host.resolveAndMount("/cart");
195
+ await vi.waitFor(() => {
196
+ expect(initialModule.unmount).toHaveBeenCalled();
197
+ });
198
+ // A newer call starts and targets a third slice while the stale call is
199
+ // still awaiting that same unmount().
200
+ const winningCall = host.resolveAndMount("/wishlist");
201
+ resolveUnmount();
202
+ await Promise.all([staleCall, winningCall]);
203
+ expect(staleModule.mount).not.toHaveBeenCalled();
204
+ expect(winningModule.mount).toHaveBeenCalledTimes(1);
205
+ });
206
+ test("destroy() unmounts the current module and resets state", async () => {
207
+ const searchModule = createMockModule();
208
+ const importer = vi.fn(async () => ({ default: searchModule }));
209
+ const target = document.createElement("div");
210
+ const host = createRuntimeHost({ manifest, target, importer });
211
+ await host.resolveAndMount("/search");
212
+ await host.destroy();
213
+ expect(searchModule.unmount).toHaveBeenCalledTimes(1);
214
+ await host.resolveAndMount("/search");
215
+ expect(searchModule.mount).toHaveBeenCalledTimes(2);
216
+ });
217
+ test("destroy() invalidates an in-flight resolveAndMount call", async () => {
218
+ const searchModule = createMockModule();
219
+ const target = document.createElement("div");
220
+ let resolveImport;
221
+ const importPromise = new Promise((resolve) => {
222
+ resolveImport = resolve;
223
+ });
224
+ const importer = vi.fn(async () => importPromise);
225
+ const host = createRuntimeHost({ manifest, target, importer });
226
+ const pendingCall = host.resolveAndMount("/search");
227
+ await host.destroy();
228
+ resolveImport({ default: searchModule });
229
+ await pendingCall;
230
+ expect(searchModule.mount).not.toHaveBeenCalled();
231
+ });
232
+ });
@@ -0,0 +1,8 @@
1
+ import type { ImportMap, RuntimeCompositionManifest, RuntimeEnvironment } from "./types.js";
2
+ export type CreateImportMapOptions = {
3
+ environment?: RuntimeEnvironment;
4
+ devDeps?: boolean;
5
+ };
6
+ export declare const createImportMap: (manifest: RuntimeCompositionManifest, options?: CreateImportMapOptions) => ImportMap;
7
+ export declare const createImportMapBootstrapScript: (manifest: RuntimeCompositionManifest, options?: CreateImportMapOptions) => string;
8
+ //# sourceMappingURL=import-map.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"import-map.d.ts","sourceRoot":"","sources":["../src/import-map.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EACT,0BAA0B,EAC1B,kBAAkB,EAGnB,MAAM,YAAY,CAAC;AA4BpB,MAAM,MAAM,sBAAsB,GAAG;IACnC,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AA6BF,eAAO,MAAM,eAAe,GAC1B,UAAU,0BAA0B,EACpC,UAAS,sBAA2B,KACnC,SAiDF,CAAC;AAIF,eAAO,MAAM,8BAA8B,GACzC,UAAU,0BAA0B,EACpC,UAAS,sBAA2B,KACnC,MAwCF,CAAC"}
@@ -0,0 +1,106 @@
1
+ import { joinUrl } from "./manifest.js";
2
+ const resolveSharedDependencyUrl = (config, environment) => {
3
+ if (typeof config === "string") {
4
+ return config;
5
+ }
6
+ return config.environments?.[environment] ?? config.url;
7
+ };
8
+ const resolveSliceUrl = (manifest, slice, environment) => {
9
+ const entry = slice.environments?.[environment] ?? slice.entry;
10
+ if (/^https?:\/\//.test(entry)) {
11
+ return entry;
12
+ }
13
+ return joinUrl(manifest.assetsOrigin, entry);
14
+ };
15
+ const ensurePrefix = (value) => value.endsWith("/") ? value : `${value}/`;
16
+ const buildDepsQuery = (peerDeps) => Object.entries(peerDeps)
17
+ .map(([name, version]) => `${name}@${version}`)
18
+ .join(",");
19
+ const applyDevFlag = (value, externalDepsOrigin) => {
20
+ if (!value.startsWith(externalDepsOrigin)) {
21
+ return value;
22
+ }
23
+ return value.includes("?") ? `${value}&dev` : `${value}?dev`;
24
+ };
25
+ const resolveAssetsOrigin = (manifest, environment) => manifest.environments?.[environment]?.assetsOrigin ?? manifest.assetsOrigin;
26
+ const resolveExternalDepsOrigin = (manifest, environment) => manifest.environments?.[environment]?.externalDepsOrigin ??
27
+ manifest.externalDepsOrigin;
28
+ export const createImportMap = (manifest, options = {}) => {
29
+ const environment = options.environment ?? "production";
30
+ const imports = {};
31
+ const namespacePrefix = ensurePrefix(manifest.namespace);
32
+ imports[namespacePrefix] = ensurePrefix(resolveAssetsOrigin(manifest, environment));
33
+ const externalDepsOrigin = resolveExternalDepsOrigin(manifest, environment);
34
+ const externalDepsPrefix = ensurePrefix(manifest.externalDepsPrefix ?? "@esm.sh/");
35
+ if (externalDepsOrigin) {
36
+ imports[externalDepsPrefix] = ensurePrefix(externalDepsOrigin);
37
+ for (const entry of manifest.externalDeps ?? []) {
38
+ const name = typeof entry === "string" ? entry : entry.name;
39
+ const peerDeps = typeof entry === "string" ? manifest.defaultPeerDeps : entry.peerDeps;
40
+ const specifier = `${externalDepsPrefix}${name}`;
41
+ const baseUrl = joinUrl(externalDepsOrigin, name);
42
+ imports[specifier] = peerDeps
43
+ ? `${baseUrl}?deps=${buildDepsQuery(peerDeps)}`
44
+ : baseUrl;
45
+ }
46
+ }
47
+ for (const [specifier, config] of Object.entries(manifest.exactImports ?? {})) {
48
+ imports[specifier] = resolveSharedDependencyUrl(config, environment);
49
+ }
50
+ for (const slice of Object.values(manifest.sliceOverrides ?? {})) {
51
+ imports[slice.specifier] = resolveSliceUrl(manifest, slice, environment);
52
+ }
53
+ for (const [sliceName, sliceOrigin] of Object.entries(manifest.environments?.[environment]?.sliceOrigins ?? {})) {
54
+ imports[`${namespacePrefix}${sliceName}/`] = ensurePrefix(sliceOrigin);
55
+ }
56
+ if (options.devDeps && externalDepsOrigin) {
57
+ for (const [specifier, url] of Object.entries(imports)) {
58
+ if (!specifier.endsWith("/") && url.startsWith(externalDepsOrigin)) {
59
+ imports[specifier] = applyDevFlag(url, externalDepsOrigin);
60
+ }
61
+ }
62
+ }
63
+ return { imports };
64
+ };
65
+ // Note: options.devDeps has no effect here — dev-mode is always resolved at
66
+ // runtime via the generated script's own `?dev` detection, not baked in here.
67
+ export const createImportMapBootstrapScript = (manifest, options = {}) => {
68
+ const environment = options.environment ?? "production";
69
+ const { imports } = createImportMap(manifest, { environment });
70
+ const externalDepsOrigin = resolveExternalDepsOrigin(manifest, environment) ?? "";
71
+ return `(function () {
72
+ var importMap = ${JSON.stringify({ imports })};
73
+ var externalDepsOrigin = ${JSON.stringify(externalDepsOrigin)};
74
+
75
+ var currentScript = document.currentScript;
76
+ var isDev = false;
77
+ if (currentScript) {
78
+ var scriptUrl = new URL(currentScript.src, window.location.origin);
79
+ isDev = scriptUrl.searchParams.has("dev");
80
+ }
81
+
82
+ var addDevFlag = function (specifier, value) {
83
+ if (
84
+ !isDev ||
85
+ specifier.slice(-1) === "/" ||
86
+ !externalDepsOrigin ||
87
+ typeof value !== "string" ||
88
+ value.indexOf(externalDepsOrigin) !== 0
89
+ ) {
90
+ return value;
91
+ }
92
+ return value.indexOf("?") !== -1 ? value + "&dev" : value + "?dev";
93
+ };
94
+
95
+ var adjustedImports = {};
96
+ Object.keys(importMap.imports).forEach(function (specifier) {
97
+ adjustedImports[specifier] = addDevFlag(specifier, importMap.imports[specifier]);
98
+ });
99
+
100
+ var script = document.createElement("script");
101
+ script.type = "importmap";
102
+ script.textContent = JSON.stringify({ imports: adjustedImports });
103
+ document.head.appendChild(script);
104
+ })();
105
+ `;
106
+ };
@@ -0,0 +1,9 @@
1
+ export * from "./externals.js";
2
+ export * from "./host.js";
3
+ export * from "./import-map.js";
4
+ export * from "./loader.js";
5
+ export * from "./manifest.js";
6
+ export * from "./routes.js";
7
+ export * from "./types.js";
8
+ export * from "./validation.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export * from "./externals.js";
2
+ export * from "./host.js";
3
+ export * from "./import-map.js";
4
+ export * from "./loader.js";
5
+ export * from "./manifest.js";
6
+ export * from "./routes.js";
7
+ export * from "./types.js";
8
+ export * from "./validation.js";
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,240 @@
1
+ // @vitest-environment happy-dom
2
+ import { describe, expect, test, vi } from "vitest";
3
+ import { createExternalMatcher, createImportMap, createImportMapBootstrapScript, createRuntimeHost, defineManifest, notifyInternalNavigation, resolveRoute, validateManifest, } from "./index.js";
4
+ const manifest = defineManifest({
5
+ namespace: "@acme",
6
+ assetsOrigin: "https://assets.example.com",
7
+ externalDepsOrigin: "https://esm.sh",
8
+ });
9
+ describe("runtime composition core", () => {
10
+ test("creates an import map from namespace and external dependency origins", () => {
11
+ expect(createImportMap(manifest)).toEqual({
12
+ imports: {
13
+ "@acme/": "https://assets.example.com/",
14
+ "@esm.sh/": "https://esm.sh/",
15
+ },
16
+ });
17
+ });
18
+ test("uses environment-specific origins and slice origins", () => {
19
+ expect(createImportMap({
20
+ ...manifest,
21
+ environments: {
22
+ development: {
23
+ assetsOrigin: "http://localhost:5173/assets",
24
+ externalDepsOrigin: "https://esm.sh",
25
+ sliceOrigins: {
26
+ search: "http://localhost:5174",
27
+ },
28
+ },
29
+ },
30
+ }, { environment: "development" })).toEqual({
31
+ imports: {
32
+ "@acme/": "http://localhost:5173/assets/",
33
+ "@esm.sh/": "https://esm.sh/",
34
+ "@acme/search/": "http://localhost:5174/",
35
+ },
36
+ });
37
+ });
38
+ test("includes exactImports entries in the generated import map", () => {
39
+ expect(createImportMap({
40
+ ...manifest,
41
+ exactImports: {
42
+ react: "https://esm.sh/react@19.2.4",
43
+ },
44
+ })).toEqual({
45
+ imports: {
46
+ "@acme/": "https://assets.example.com/",
47
+ "@esm.sh/": "https://esm.sh/",
48
+ react: "https://esm.sh/react@19.2.4",
49
+ },
50
+ });
51
+ });
52
+ test("generates externalDeps entries with defaultPeerDeps applied to bare strings", () => {
53
+ expect(createImportMap({
54
+ ...manifest,
55
+ externalDeps: ["zustand"],
56
+ defaultPeerDeps: { react: "19.2.4" },
57
+ })).toEqual({
58
+ imports: {
59
+ "@acme/": "https://assets.example.com/",
60
+ "@esm.sh/": "https://esm.sh/",
61
+ "@esm.sh/zustand": "https://esm.sh/zustand?deps=react@19.2.4",
62
+ },
63
+ });
64
+ });
65
+ test("supports externalDeps entries that opt out of peerDeps", () => {
66
+ expect(createImportMap({
67
+ ...manifest,
68
+ externalDeps: [{ name: "date-fns", peerDeps: false }],
69
+ defaultPeerDeps: { react: "19.2.4" },
70
+ })).toEqual({
71
+ imports: {
72
+ "@acme/": "https://assets.example.com/",
73
+ "@esm.sh/": "https://esm.sh/",
74
+ "@esm.sh/date-fns": "https://esm.sh/date-fns",
75
+ },
76
+ });
77
+ });
78
+ test("supports externalDeps entries with a custom peerDeps override", () => {
79
+ expect(createImportMap({
80
+ ...manifest,
81
+ externalDeps: [
82
+ {
83
+ name: "@radix-ui/themes",
84
+ peerDeps: { react: "19.2.4", "react-dom": "19.2.4/client" },
85
+ },
86
+ ],
87
+ defaultPeerDeps: { react: "19.2.4" },
88
+ })).toEqual({
89
+ imports: {
90
+ "@acme/": "https://assets.example.com/",
91
+ "@esm.sh/": "https://esm.sh/",
92
+ "@esm.sh/@radix-ui/themes": "https://esm.sh/@radix-ui/themes?deps=react@19.2.4,react-dom@19.2.4/client",
93
+ },
94
+ });
95
+ });
96
+ test("devDeps appends ?dev to exact external entries but not the catch-all prefix", () => {
97
+ expect(createImportMap({
98
+ ...manifest,
99
+ externalDeps: ["zustand"],
100
+ defaultPeerDeps: { react: "19.2.4" },
101
+ }, { devDeps: true })).toEqual({
102
+ imports: {
103
+ "@acme/": "https://assets.example.com/",
104
+ "@esm.sh/": "https://esm.sh/",
105
+ "@esm.sh/zustand": "https://esm.sh/zustand?deps=react@19.2.4&dev",
106
+ },
107
+ });
108
+ });
109
+ test("bare-string externalDeps entries resolve with no ?deps= query when defaultPeerDeps is unset", () => {
110
+ expect(createImportMap({
111
+ ...manifest,
112
+ externalDeps: ["zustand"],
113
+ })).toEqual({
114
+ imports: {
115
+ "@acme/": "https://assets.example.com/",
116
+ "@esm.sh/": "https://esm.sh/",
117
+ "@esm.sh/zustand": "https://esm.sh/zustand",
118
+ },
119
+ });
120
+ });
121
+ test("resolves routes by convention without explicit slice config", () => {
122
+ expect(resolveRoute(manifest, "/search/routes")?.specifier).toBe("@acme/search/index.mjs");
123
+ });
124
+ test("returns null for the root route unless an override is configured", () => {
125
+ expect(resolveRoute(manifest, "/")).toBeNull();
126
+ });
127
+ test("supports explicit routeOverrides for exceptions", () => {
128
+ const match = resolveRoute({
129
+ ...manifest,
130
+ routeOverrides: {
131
+ "/": "@acme/home/index.mjs",
132
+ },
133
+ }, "/");
134
+ expect(match?.specifier).toBe("@acme/home/index.mjs");
135
+ });
136
+ test("supports sliceOverrides for slices outside the entryFile convention", () => {
137
+ const match = resolveRoute({
138
+ ...manifest,
139
+ sliceOverrides: {
140
+ legacy: {
141
+ route: "/legacy/*",
142
+ specifier: "@acme/legacy",
143
+ entry: "/legacy/index.mjs",
144
+ },
145
+ },
146
+ }, "/legacy/anything");
147
+ expect(match?.specifier).toBe("@acme/legacy");
148
+ });
149
+ test("creates an external matcher from the manifest", () => {
150
+ const isExternal = createExternalMatcher(manifest);
151
+ expect(isExternal("@esm.sh/react")).toBe(true);
152
+ expect(isExternal("@acme/search/index.mjs")).toBe(true);
153
+ expect(isExternal("@acme/anything")).toBe(true);
154
+ expect(isExternal("lodash")).toBe(false);
155
+ });
156
+ test("external matcher also matches exactImports and sliceOverrides specifiers", () => {
157
+ const isExternal = createExternalMatcher({
158
+ ...manifest,
159
+ exactImports: {
160
+ react: "https://esm.sh/react@19.2.4",
161
+ },
162
+ sliceOverrides: {
163
+ legacy: {
164
+ route: "/legacy/*",
165
+ specifier: "@vendor/legacy",
166
+ entry: "/legacy/index.mjs",
167
+ },
168
+ },
169
+ });
170
+ expect(isExternal("react")).toBe(true);
171
+ expect(isExternal("@vendor/legacy")).toBe(true);
172
+ expect(isExternal("lodash")).toBe(false);
173
+ });
174
+ test("validates the base manifest without errors", () => {
175
+ expect(validateManifest(manifest).filter((item) => item.level === "error")).toHaveLength(0);
176
+ });
177
+ test("createImportMapBootstrapScript generates valid, parseable JavaScript", () => {
178
+ const script = createImportMapBootstrapScript(manifest);
179
+ expect(() => new Function(script)).not.toThrow();
180
+ });
181
+ test("createImportMapBootstrapScript embeds the computed import map", () => {
182
+ const script = createImportMapBootstrapScript(manifest);
183
+ expect(script).toContain('"@acme/":"https://assets.example.com/"');
184
+ expect(script).toContain('"@esm.sh/":"https://esm.sh/"');
185
+ });
186
+ test("createImportMapBootstrapScript embeds the manifest's own external dependency configuration, not hardcoded esm.sh defaults", () => {
187
+ const script = createImportMapBootstrapScript({
188
+ ...manifest,
189
+ externalDepsOrigin: "https://cdn.example.org",
190
+ externalDepsPrefix: "@cdn/",
191
+ });
192
+ expect(script).toContain("https://cdn.example.org");
193
+ expect(script).not.toContain("esm.sh");
194
+ });
195
+ test("createImportMapBootstrapScript injects an importmap script via document.head.appendChild", () => {
196
+ const script = createImportMapBootstrapScript(manifest);
197
+ expect(script).toContain("document.head.appendChild(script)");
198
+ expect(script).toContain('script.type = "importmap"');
199
+ });
200
+ test("createImportMapBootstrapScript's embedded dev-flag logic matches createImportMap's own devDeps behavior", () => {
201
+ const manifestWithExternalDeps = {
202
+ ...manifest,
203
+ externalDeps: ["zustand"],
204
+ defaultPeerDeps: { react: "19.2.4" },
205
+ };
206
+ const expectedWithDevDeps = createImportMap(manifestWithExternalDeps, {
207
+ devDeps: true,
208
+ }).imports;
209
+ const script = createImportMapBootstrapScript(manifestWithExternalDeps);
210
+ // Simulate the generated script running with a `?dev` script src, using
211
+ // the same DOM-shape the real browser execution relies on.
212
+ const documentStub = {
213
+ currentScript: { src: "http://localhost/importmap.js?dev" },
214
+ createElement: () => ({}),
215
+ head: { appendChild: (el) => {
216
+ documentStub.result =
217
+ el.textContent;
218
+ } },
219
+ };
220
+ const windowStub = { location: { origin: "http://localhost" } };
221
+ const run = new Function("document", "window", "URL", script);
222
+ run(documentStub, windowStub, URL);
223
+ const producedImports = JSON.parse(documentStub.result).imports;
224
+ expect(producedImports["@esm.sh/zustand"]).toBe(expectedWithDevDeps["@esm.sh/zustand"]);
225
+ expect(producedImports["@esm.sh/"]).toBe(expectedWithDevDeps["@esm.sh/"]);
226
+ });
227
+ test("re-exports createRuntimeHost and notifyInternalNavigation from the public barrel", async () => {
228
+ const target = document.createElement("div");
229
+ const mountSpy = vi.fn(async () => { });
230
+ const importer = vi.fn(async () => ({ default: { mount: mountSpy } }));
231
+ const host = createRuntimeHost({
232
+ manifest: { namespace: "@acme", assetsOrigin: "https://assets.example.com" },
233
+ target,
234
+ importer,
235
+ });
236
+ await host.resolveAndMount("/search");
237
+ expect(mountSpy).toHaveBeenCalledTimes(1);
238
+ expect(typeof notifyInternalNavigation).toBe("function");
239
+ });
240
+ });
@@ -0,0 +1,4 @@
1
+ import type { DynamicImporter } from "./types.js";
2
+ export declare const importModule: (specifier: string, importer?: DynamicImporter) => Promise<unknown>;
3
+ export declare const unwrapDefault: (moduleNamespace: unknown) => unknown;
4
+ //# sourceMappingURL=loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,eAAO,MAAM,YAAY,GACvB,WAAW,MAAM,EACjB,WAAU,eAC4B,KACrC,OAAO,CAAC,OAAO,CAAwB,CAAC;AAE3C,eAAO,MAAM,aAAa,GAAI,iBAAiB,OAAO,KAAG,OAKpC,CAAC"}
package/dist/loader.js ADDED
@@ -0,0 +1,6 @@
1
+ export const importModule = (specifier, importer = (specifier) => import(/* @vite-ignore */ specifier)) => importer(specifier);
2
+ export const unwrapDefault = (moduleNamespace) => moduleNamespace &&
3
+ typeof moduleNamespace === "object" &&
4
+ "default" in moduleNamespace
5
+ ? moduleNamespace.default
6
+ : moduleNamespace;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=loader.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.test.d.ts","sourceRoot":"","sources":["../src/loader.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,31 @@
1
+ import { describe, expect, test, vi } from "vitest";
2
+ import { importModule, unwrapDefault } from "./loader.js";
3
+ describe("importModule", () => {
4
+ test("uses the default importer to call native dynamic import", async () => {
5
+ const namespace = (await importModule("./__fixtures__/dummy-module.js"));
6
+ expect(namespace.default).toBe("default-value");
7
+ expect(namespace.namedExport).toBe("named-value");
8
+ });
9
+ test("uses a custom importer when provided", async () => {
10
+ const customImporter = vi.fn(async (specifier) => ({
11
+ default: `loaded:${specifier}`,
12
+ }));
13
+ const result = await importModule("some-specifier", customImporter);
14
+ expect(customImporter).toHaveBeenCalledWith("some-specifier");
15
+ expect(result).toEqual({ default: "loaded:some-specifier" });
16
+ });
17
+ });
18
+ describe("unwrapDefault", () => {
19
+ test("returns the default export when present", () => {
20
+ expect(unwrapDefault({ default: "the-value" })).toBe("the-value");
21
+ });
22
+ test("returns the namespace itself when there is no default export", () => {
23
+ const namespace = { namedExport: "value" };
24
+ expect(unwrapDefault(namespace)).toBe(namespace);
25
+ });
26
+ test("returns non-object values unchanged", () => {
27
+ expect(unwrapDefault(null)).toBe(null);
28
+ expect(unwrapDefault(undefined)).toBe(undefined);
29
+ expect(unwrapDefault("plain-string")).toBe("plain-string");
30
+ });
31
+ });
@@ -0,0 +1,6 @@
1
+ import type { RuntimeCompositionManifest } from "./types.js";
2
+ export declare const defineManifest: <TManifest extends RuntimeCompositionManifest>(manifest: TManifest) => TManifest;
3
+ export declare const trimTrailingSlash: (value: string) => string;
4
+ export declare const trimLeadingSlash: (value: string) => string;
5
+ export declare const joinUrl: (origin: string, path: string) => string;
6
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AAE7D,eAAO,MAAM,cAAc,GAAI,SAAS,SAAS,0BAA0B,EACzE,UAAU,SAAS,KAClB,SAAqB,CAAC;AAEzB,eAAO,MAAM,iBAAiB,GAAI,OAAO,MAAM,KAAG,MACA,CAAC;AAEnD,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAAM,KAAG,MACD,CAAC;AAEjD,eAAO,MAAM,OAAO,GAAI,QAAQ,MAAM,EAAE,MAAM,MAAM,KAAG,MACG,CAAC"}
@@ -0,0 +1,4 @@
1
+ export const defineManifest = (manifest) => manifest;
2
+ export const trimTrailingSlash = (value) => value.endsWith("/") ? value.slice(0, -1) : value;
3
+ export const trimLeadingSlash = (value) => value.startsWith("/") ? value.slice(1) : value;
4
+ export const joinUrl = (origin, path) => `${trimTrailingSlash(origin)}/${trimLeadingSlash(path)}`;
@@ -0,0 +1,3 @@
1
+ import type { RuntimeCompositionManifest, RuntimeRouteMatch } from "./types.js";
2
+ export declare const resolveRoute: (manifest: RuntimeCompositionManifest, path: string) => RuntimeRouteMatch | null;
3
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,0BAA0B,EAC1B,iBAAiB,EAElB,MAAM,YAAY,CAAC;AA6HpB,eAAO,MAAM,YAAY,GACvB,UAAU,0BAA0B,EACpC,MAAM,MAAM,KACX,iBAAiB,GAAG,IAiBtB,CAAC"}
package/dist/routes.js ADDED
@@ -0,0 +1,105 @@
1
+ const normalizePath = (path) => {
2
+ const withoutHash = path.split("#")[0] ?? "/";
3
+ const withoutSearch = withoutHash.split("?")[0] ?? "/";
4
+ const normalized = withoutSearch.startsWith("/")
5
+ ? withoutSearch
6
+ : `/${withoutSearch}`;
7
+ return normalized.length > 1 && normalized.endsWith("/")
8
+ ? normalized.slice(0, -1)
9
+ : normalized;
10
+ };
11
+ const routePatternsFor = (slice) => Array.isArray(slice.route) ? slice.route : [slice.route];
12
+ const routePatternsForOverride = (route, override) => {
13
+ if (typeof override === "string" || !override.route) {
14
+ return [route];
15
+ }
16
+ return Array.isArray(override.route) ? override.route : [override.route];
17
+ };
18
+ const ensureNamespacePrefix = (namespace) => namespace.endsWith("/") ? namespace : `${namespace}/`;
19
+ const resolveConventionalSliceName = (path) => {
20
+ const [firstSegment] = normalizePath(path).split("/").filter(Boolean);
21
+ return firstSegment ?? null;
22
+ };
23
+ const buildConventionalSpecifier = (manifest, sliceName) => `${ensureNamespacePrefix(manifest.namespace)}${sliceName}/${manifest.entryFile ?? "index.mjs"}`;
24
+ const matchRoutePattern = (pattern, pathname) => {
25
+ const normalizedPattern = normalizePath(pattern);
26
+ const normalizedPathname = normalizePath(pathname);
27
+ if (normalizedPattern.endsWith("/*")) {
28
+ const prefix = normalizedPattern.slice(0, -2);
29
+ return {
30
+ matched: normalizedPathname === prefix ||
31
+ normalizedPathname.startsWith(`${prefix}/`),
32
+ params: {},
33
+ score: prefix.length,
34
+ };
35
+ }
36
+ if (normalizedPattern === normalizedPathname) {
37
+ return {
38
+ matched: true,
39
+ params: {},
40
+ score: normalizedPattern.length + 1000,
41
+ };
42
+ }
43
+ return {
44
+ matched: false,
45
+ params: {},
46
+ score: -1,
47
+ };
48
+ };
49
+ const resolveExplicitRoute = (manifest, path) => {
50
+ const matches = [];
51
+ for (const [route, override] of Object.entries(manifest.routeOverrides ?? {})) {
52
+ for (const pattern of routePatternsForOverride(route, override)) {
53
+ const match = matchRoutePattern(pattern, path);
54
+ if (match.matched) {
55
+ const specifier = typeof override === "string" ? override : override.specifier;
56
+ matches.push({
57
+ sliceName: specifier
58
+ .slice(ensureNamespacePrefix(manifest.namespace).length)
59
+ .split("/")[0] ?? "",
60
+ specifier,
61
+ route: pattern,
62
+ params: match.params,
63
+ score: match.score + 2000,
64
+ });
65
+ }
66
+ }
67
+ }
68
+ for (const [sliceName, slice] of Object.entries(manifest.sliceOverrides ?? {})) {
69
+ for (const route of routePatternsFor(slice)) {
70
+ const match = matchRoutePattern(route, path);
71
+ if (match.matched) {
72
+ matches.push({
73
+ sliceName,
74
+ slice,
75
+ specifier: slice.specifier,
76
+ route,
77
+ params: match.params,
78
+ score: match.score,
79
+ });
80
+ }
81
+ }
82
+ }
83
+ const [bestMatch] = matches.sort((a, b) => b.score - a.score);
84
+ if (!bestMatch) {
85
+ return null;
86
+ }
87
+ const { score: _score, ...routeMatch } = bestMatch;
88
+ return routeMatch;
89
+ };
90
+ export const resolveRoute = (manifest, path) => {
91
+ const explicitMatch = resolveExplicitRoute(manifest, path);
92
+ if (explicitMatch) {
93
+ return explicitMatch;
94
+ }
95
+ const sliceName = resolveConventionalSliceName(path);
96
+ if (!sliceName) {
97
+ return null;
98
+ }
99
+ return {
100
+ sliceName,
101
+ specifier: buildConventionalSpecifier(manifest, sliceName),
102
+ route: `/${sliceName}/*`,
103
+ params: {},
104
+ };
105
+ };
@@ -0,0 +1,67 @@
1
+ export type ImportMap = {
2
+ imports: Record<string, string>;
3
+ scopes?: Record<string, Record<string, string>>;
4
+ };
5
+ export type RuntimeEnvironment = "development" | "preview" | "production";
6
+ export type EnvironmentUrlMap = Partial<Record<RuntimeEnvironment, string>>;
7
+ export type SharedDependencyConfig = string | {
8
+ url: string;
9
+ environments?: EnvironmentUrlMap;
10
+ external?: boolean;
11
+ };
12
+ export type SliceConfig = {
13
+ route: string | string[];
14
+ specifier: string;
15
+ entry: string;
16
+ environments?: EnvironmentUrlMap;
17
+ external?: boolean;
18
+ };
19
+ export type RouteOverrideConfig = string | {
20
+ specifier: string;
21
+ route?: string | string[];
22
+ };
23
+ export type EnvironmentConfig = {
24
+ assetsOrigin?: string;
25
+ externalDepsOrigin?: string;
26
+ sliceOrigins?: Record<string, string>;
27
+ };
28
+ export type ExternalDepEntry = string | {
29
+ name: string;
30
+ peerDeps?: Record<string, string> | false;
31
+ };
32
+ export type RuntimeCompositionManifest = {
33
+ namespace: string;
34
+ assetsOrigin: string;
35
+ externalDepsOrigin?: string;
36
+ externalDepsPrefix?: string;
37
+ entryFile?: string;
38
+ environments?: Partial<Record<RuntimeEnvironment, EnvironmentConfig>>;
39
+ exactImports?: Record<string, SharedDependencyConfig>;
40
+ sliceOverrides?: Record<string, SliceConfig>;
41
+ routeOverrides?: Record<string, RouteOverrideConfig>;
42
+ externalDeps?: ExternalDepEntry[];
43
+ defaultPeerDeps?: Record<string, string>;
44
+ };
45
+ export type RuntimeRouteMatch = {
46
+ sliceName: string;
47
+ slice?: SliceConfig;
48
+ specifier: string;
49
+ route: string;
50
+ params: Record<string, string>;
51
+ };
52
+ export type RuntimeCompositionDiagnostic = {
53
+ level: "error" | "warning";
54
+ code: string;
55
+ message: string;
56
+ };
57
+ export type RuntimeModuleContext = {
58
+ route?: RuntimeRouteMatch;
59
+ manifest?: RuntimeCompositionManifest;
60
+ data?: unknown;
61
+ };
62
+ export type RuntimeModule = {
63
+ mount(target: Element, context?: RuntimeModuleContext): void | Promise<void>;
64
+ unmount?(): void | Promise<void>;
65
+ };
66
+ export type DynamicImporter = (specifier: string) => Promise<unknown>;
67
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { RuntimeCompositionDiagnostic, RuntimeCompositionManifest } from "./types.js";
2
+ export declare const validateManifest: (manifest: RuntimeCompositionManifest) => RuntimeCompositionDiagnostic[];
3
+ //# sourceMappingURL=validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,4BAA4B,EAC5B,0BAA0B,EAC3B,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,gBAAgB,GAC3B,UAAU,0BAA0B,KACnC,4BAA4B,EAgF9B,CAAC"}
@@ -0,0 +1,67 @@
1
+ export const validateManifest = (manifest) => {
2
+ const diagnostics = [];
3
+ if (!manifest.namespace.startsWith("@")) {
4
+ diagnostics.push({
5
+ level: "warning",
6
+ code: "namespace-format",
7
+ message: "Manifest namespace should be a bare package scope, e.g. @acme.",
8
+ });
9
+ }
10
+ if (!/^https?:\/\//.test(manifest.assetsOrigin)) {
11
+ diagnostics.push({
12
+ level: "error",
13
+ code: "assets-origin-url",
14
+ message: "assetsOrigin must be an absolute HTTP(S) URL.",
15
+ });
16
+ }
17
+ if (manifest.externalDepsOrigin &&
18
+ !/^https?:\/\//.test(manifest.externalDepsOrigin)) {
19
+ diagnostics.push({
20
+ level: "error",
21
+ code: "external-deps-origin-url",
22
+ message: "externalDepsOrigin must be an absolute HTTP(S) URL.",
23
+ });
24
+ }
25
+ if (manifest.externalDepsPrefix &&
26
+ !manifest.externalDepsPrefix.endsWith("/")) {
27
+ diagnostics.push({
28
+ level: "warning",
29
+ code: "external-deps-prefix-format",
30
+ message: "externalDepsPrefix should end with / for import-map prefix matching.",
31
+ });
32
+ }
33
+ for (const [sliceName, slice] of Object.entries(manifest.sliceOverrides ?? {})) {
34
+ if (!slice.specifier.startsWith(`${manifest.namespace}/`)) {
35
+ diagnostics.push({
36
+ level: "warning",
37
+ code: "slice-specifier-namespace",
38
+ message: `Slice "${sliceName}" specifier should start with ${manifest.namespace}/.`,
39
+ });
40
+ }
41
+ if (!slice.entry.endsWith(".mjs") && !/^https?:\/\//.test(slice.entry)) {
42
+ diagnostics.push({
43
+ level: "warning",
44
+ code: "slice-entry-extension",
45
+ message: `Slice "${sliceName}" entry should usually point to an ESM .mjs asset.`,
46
+ });
47
+ }
48
+ }
49
+ for (const [route, override] of Object.entries(manifest.routeOverrides ?? {})) {
50
+ const specifier = typeof override === "string" ? override : override.specifier;
51
+ if (!route.startsWith("/")) {
52
+ diagnostics.push({
53
+ level: "warning",
54
+ code: "route-override-format",
55
+ message: `Route override "${route}" should start with /.`,
56
+ });
57
+ }
58
+ if (!specifier.startsWith(`${manifest.namespace}/`)) {
59
+ diagnostics.push({
60
+ level: "warning",
61
+ code: "route-override-specifier-namespace",
62
+ message: `Route override "${route}" specifier should start with ${manifest.namespace}/.`,
63
+ });
64
+ }
65
+ }
66
+ return diagnostics;
67
+ };
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@rmc-toolkit/core",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "scripts": {
20
+ "build": "tsc -b",
21
+ "test": "vitest run"
22
+ }
23
+ }
24
+