@persona/layout-editor-remote-module 0.0.0-bootstrap.0 → 0.5.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/README.md +147 -0
- package/dist/bridge-frame.d.mts +45 -0
- package/dist/bridge-frame.mjs +350 -0
- package/dist/bridge-host.d.mts +61 -0
- package/dist/bridge-host.mjs +217 -0
- package/dist/define-Dt8sbrVO.d.mts +236 -0
- package/dist/index.d.mts +98 -0
- package/dist/index.mjs +58 -0
- package/dist/protocol-DMsysRFd.mjs +211 -0
- package/dist/protocol-edoRdXpF.d.mts +30 -0
- package/dist/schema-BeI8GdXS.mjs +105 -0
- package/dist/vite-plugin.d.mts +43 -0
- package/dist/vite-plugin.mjs +435 -0
- package/package.json +117 -4
- package/skills/getting-started/SKILL.md +371 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { n as DEFAULT_MANIFEST_PATH, r as defineLayoutModule, t as remoteModuleRegistrationManifestV1Schema } from "./schema-BeI8GdXS.mjs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { build } from "esbuild";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { build as build$1 } from "vite";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
|
|
10
|
+
//#region src/remote-shims.ts
|
|
11
|
+
/**
|
|
12
|
+
* Specifiers a remote module bundle must NOT carry its own copy of — each is
|
|
13
|
+
* redirected (via esbuild/Vite `alias`) to a tiny CJS shim that reads the
|
|
14
|
+
* host's own instance off `globalThis.__PERSONA_REMOTE_MODULE_DEPS__` instead
|
|
15
|
+
* of bundling the real package.
|
|
16
|
+
*
|
|
17
|
+
* Why this exists: hooks require the calling code's React module instance to
|
|
18
|
+
* be the exact one actively rendering the tree. A remote module built with
|
|
19
|
+
* its own bundled `react` (the default, before this) creates a SECOND,
|
|
20
|
+
* unrelated React instance — any hook call from code using that instance
|
|
21
|
+
* (including styled-components' own `useContext` inside `ThemeProvider`, and
|
|
22
|
+
* anything in `@persona/design-system`, which itself just imports `react`)
|
|
23
|
+
* reads a dispatcher that was never set, producing exactly
|
|
24
|
+
* "Cannot read properties of null (reading 'useContext')". Sharing the
|
|
25
|
+
* instance via a runtime global — rather than bundling it — fixes this for
|
|
26
|
+
* every remote module, not just one that happens to avoid hooks.
|
|
27
|
+
*
|
|
28
|
+
* `styled-components` needs the same treatment for a subtler reason: even
|
|
29
|
+
* with a shared React, a SEPARATE `styled-components` package instance would
|
|
30
|
+
* call `React.createContext()` itself and produce a DIFFERENT Context object
|
|
31
|
+
* than the host's — a `<ThemeProvider>` from one instance is invisible to
|
|
32
|
+
* `styled.div` definitions from the other, no matter how the React inside
|
|
33
|
+
* them is shared.
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* Order matters: Vite's alias resolution (via its wrapped
|
|
37
|
+
* `@rollup/plugin-alias`) treats a string `find` as a PREFIX match, not just
|
|
38
|
+
* exact — `importee === find || importee.startsWith(find + '/')` — and takes
|
|
39
|
+
* the FIRST entry (in object key / array order) that matches. Since
|
|
40
|
+
* `'react/jsx-runtime'.startsWith('react/')` is true, listing `'react'`
|
|
41
|
+
* before `'react/jsx-runtime'` would make every `react/jsx-runtime` import
|
|
42
|
+
* resolve against the 'react' shim's path (e.g.
|
|
43
|
+
* `.../react.cjs/jsx-runtime`, ENOTDIR) instead of its own shim. The more
|
|
44
|
+
* specific specifier must come first.
|
|
45
|
+
*/
|
|
46
|
+
const SHIMMABLE_SPECIFIERS = [
|
|
47
|
+
"react/jsx-runtime",
|
|
48
|
+
"react/jsx-dev-runtime",
|
|
49
|
+
"react",
|
|
50
|
+
"react-dom",
|
|
51
|
+
"styled-components"
|
|
52
|
+
];
|
|
53
|
+
const HOST_DEPS_GLOBAL = "__PERSONA_REMOTE_MODULE_DEPS__";
|
|
54
|
+
/**
|
|
55
|
+
* `module.exports = ...` (not a static ESM re-export) deliberately — an ESM
|
|
56
|
+
* `export` list has to be known at author time, but the shimmed value's
|
|
57
|
+
* actual shape lives on a runtime global. esbuild's own CJS interop
|
|
58
|
+
* (`__toESM`) forwards named-import access to a CJS module's properties
|
|
59
|
+
* dynamically, at RUNTIME, regardless of what the bundler could see
|
|
60
|
+
* statically — the same mechanism that already makes `import { useState }
|
|
61
|
+
* from 'react'` work today, since React itself ships as CJS.
|
|
62
|
+
*
|
|
63
|
+
* Consuming apps with `esModuleInterop` off in tsconfig (allowed by TS —
|
|
64
|
+
* `allowSyntheticDefaultImports` permits the import *syntax* without it) get
|
|
65
|
+
* esbuild's "Node mode" CJS interop for a DEFAULT import of an aliased CJS
|
|
66
|
+
* file: `target.default = mod` (the whole `module.exports`), unconditionally
|
|
67
|
+
* — `mod.__esModule` is NOT consulted in that mode, so it can't be used to
|
|
68
|
+
* steer this. That's a correct model for a module whose "the module" and "the
|
|
69
|
+
* default export" are the same thing (true for `react`/`react-dom`, which
|
|
70
|
+
* have no own `.default`). It's wrong for `styled-components`: its
|
|
71
|
+
* module.exports has BOTH a whole-module shape (ThemeProvider, css, ... as
|
|
72
|
+
* plain properties) AND a separate own `.default` (the callable `styled` tag
|
|
73
|
+
* function) — Node mode's `target.default = mod` picks the whole-module
|
|
74
|
+
* shape, so `styled.div`/`.attrs(...)` throws on `undefined`.
|
|
75
|
+
*
|
|
76
|
+
* The fix: make OUR module.exports itself resolve the ambiguity — if the
|
|
77
|
+
* host's real module has its own `.default` distinct from itself, export
|
|
78
|
+
* THAT (with the rest of the module's named properties copied onto it, so
|
|
79
|
+
* `styled.ThemeProvider`-style access — not used by styled-components itself,
|
|
80
|
+
* but keeps this general — and named imports both still resolve). Mutating
|
|
81
|
+
* the host's real default value (rather than building a fresh wrapper) is
|
|
82
|
+
* deliberate: styled-components' internals sometimes compare a component
|
|
83
|
+
* against the exact `styled` function reference (e.g. `isStyledComponent`);
|
|
84
|
+
* a copy would break that. Ordinary functions/objects aren't frozen, so this
|
|
85
|
+
* is safe for every specifier actually on this list.
|
|
86
|
+
*/
|
|
87
|
+
function shimModuleSource(specifier) {
|
|
88
|
+
const globalAccess = `globalThis.${HOST_DEPS_GLOBAL} && globalThis.${HOST_DEPS_GLOBAL}[${JSON.stringify(specifier)}]`;
|
|
89
|
+
const missingMessage = `[persona-layout-modules] the host did not expose "${specifier}" via globalThis.${HOST_DEPS_GLOBAL} before this module loaded`;
|
|
90
|
+
return [
|
|
91
|
+
`var __persona_dep = ${globalAccess} || (() => { throw new Error(${JSON.stringify(missingMessage)}); })();`,
|
|
92
|
+
`var __persona_base = (__persona_dep && 'default' in __persona_dep && __persona_dep.default !== __persona_dep) ? __persona_dep.default : __persona_dep;`,
|
|
93
|
+
`module.exports = Object.assign(__persona_base, __persona_dep, { __esModule: true, default: __persona_base });`,
|
|
94
|
+
""
|
|
95
|
+
].join("\n");
|
|
96
|
+
}
|
|
97
|
+
let cachedShimPaths;
|
|
98
|
+
/**
|
|
99
|
+
* Writes one `.cjs` shim file per specifier to a temp dir (once per process
|
|
100
|
+
* — the content is static, not per-request) and returns a specifier -> path
|
|
101
|
+
* map suitable for esbuild's/Vite's `alias` option. `.cjs` is deliberate: it
|
|
102
|
+
* forces CommonJS treatment regardless of any ambient `package.json#type`.
|
|
103
|
+
*/
|
|
104
|
+
function ensureRemoteShimFiles() {
|
|
105
|
+
if (!cachedShimPaths) cachedShimPaths = (async () => {
|
|
106
|
+
const dir = await mkdtemp(path.join(tmpdir(), "persona-layout-module-shims-"));
|
|
107
|
+
const entries = await Promise.all(SHIMMABLE_SPECIFIERS.map(async (specifier) => {
|
|
108
|
+
const fileName = `${specifier.replace(/[\\/]/g, "_")}.cjs`;
|
|
109
|
+
const filePath = path.join(dir, fileName);
|
|
110
|
+
await writeFile(filePath, shimModuleSource(specifier), "utf8");
|
|
111
|
+
return [specifier, filePath];
|
|
112
|
+
}));
|
|
113
|
+
return Object.fromEntries(entries);
|
|
114
|
+
})();
|
|
115
|
+
return cachedShimPaths;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/vite-plugin.ts
|
|
120
|
+
/**
|
|
121
|
+
* Vite plugin that takes a statically-declared list of layout modules
|
|
122
|
+
* (`options.modules`, authored via `defineLayoutModule` — see ./define.ts),
|
|
123
|
+
* wires every `code`-delivered entry into the build as an addressable ESM
|
|
124
|
+
* chunk, ensures every `document`-delivered (`.html`) entry is actually
|
|
125
|
+
* built/served, and emits a `RemoteModuleRegistrationManifestV1[]` document
|
|
126
|
+
* persona-web can intake.
|
|
127
|
+
*
|
|
128
|
+
* Modules are configured directly here, not discovered by globbing the
|
|
129
|
+
* source tree — a module IS whatever's in `options.modules`, full stop.
|
|
130
|
+
*/
|
|
131
|
+
function personaLayoutModules(options) {
|
|
132
|
+
const manifestPath = (options.manifestPath ?? DEFAULT_MANIFEST_PATH).replace(/^\/+/, "");
|
|
133
|
+
let root = process.cwd();
|
|
134
|
+
let resolvedConfig;
|
|
135
|
+
let definitions = [];
|
|
136
|
+
return {
|
|
137
|
+
async closeBundle() {
|
|
138
|
+
if (definitions.length === 0 || resolvedConfig.command !== "build") return;
|
|
139
|
+
const outDir = path.isAbsolute(resolvedConfig.build.outDir) ? resolvedConfig.build.outDir : path.resolve(root, resolvedConfig.build.outDir);
|
|
140
|
+
const input = {};
|
|
141
|
+
for (const definition of definitions) if (definition.render.kind === "code") input[entryNameFor(definition)] = definition.render.absolutePath;
|
|
142
|
+
let emitted = [];
|
|
143
|
+
if (Object.keys(input).length > 0) {
|
|
144
|
+
const shimAlias = await ensureRemoteShimFiles();
|
|
145
|
+
const result = await build$1({
|
|
146
|
+
base: resolvedConfig.base,
|
|
147
|
+
build: {
|
|
148
|
+
emptyOutDir: false,
|
|
149
|
+
outDir,
|
|
150
|
+
rollupOptions: {
|
|
151
|
+
input,
|
|
152
|
+
preserveEntrySignatures: "exports-only"
|
|
153
|
+
},
|
|
154
|
+
write: true
|
|
155
|
+
},
|
|
156
|
+
configFile: false,
|
|
157
|
+
esbuild: { jsx: "automatic" },
|
|
158
|
+
logLevel: "warn",
|
|
159
|
+
mode: "production",
|
|
160
|
+
plugins: options.modulePlugins?.() ?? [],
|
|
161
|
+
resolve: { alias: {
|
|
162
|
+
...resolveAliasForEsbuild(resolvedConfig),
|
|
163
|
+
...shimAlias
|
|
164
|
+
} },
|
|
165
|
+
root
|
|
166
|
+
});
|
|
167
|
+
emitted = (Array.isArray(result) ? result : [result]).flatMap((output) => "output" in output ? output.output : []);
|
|
168
|
+
}
|
|
169
|
+
const base = resolvedConfig.base ?? "/";
|
|
170
|
+
const manifests = [];
|
|
171
|
+
for (const definition of definitions) {
|
|
172
|
+
const render = resolveManifestDelivery(definition, definition.render, emitted, root, base);
|
|
173
|
+
const css = /* @__PURE__ */ new Set();
|
|
174
|
+
if (render.chunk) collectCss(render.chunk, css);
|
|
175
|
+
const manifest = {
|
|
176
|
+
...buildManifestBase(definition),
|
|
177
|
+
remote: {
|
|
178
|
+
css: css.size > 0 ? [...css].map((fileName) => joinUrl(base, fileName)) : void 0,
|
|
179
|
+
render: render.value
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const result2 = remoteModuleRegistrationManifestV1Schema.safeParse(manifest);
|
|
183
|
+
if (!result2.success) throw new Error(`[persona-layout-modules] ${definition.definitionFilePath}: invalid manifest for "${definition.input.key}":\n${z.prettifyError(result2.error)}`);
|
|
184
|
+
manifests.push(manifest);
|
|
185
|
+
}
|
|
186
|
+
assertUniqueKeys(manifests);
|
|
187
|
+
const manifestFilePath = path.join(outDir, manifestPath);
|
|
188
|
+
await mkdir(path.dirname(manifestFilePath), { recursive: true });
|
|
189
|
+
await writeFile(manifestFilePath, JSON.stringify({ modules: manifests }, null, 2), "utf8");
|
|
190
|
+
},
|
|
191
|
+
async config(config, env) {
|
|
192
|
+
root = config.root ? path.resolve(config.root) : process.cwd();
|
|
193
|
+
definitions = options.modules.map((input) => ({
|
|
194
|
+
definitionFilePath: input.key,
|
|
195
|
+
input,
|
|
196
|
+
render: resolveEntry(root, input.render)
|
|
197
|
+
}));
|
|
198
|
+
if (env.command !== "build") return;
|
|
199
|
+
const existingInputs = absoluteInputPaths(config, root);
|
|
200
|
+
const toInject = {};
|
|
201
|
+
for (const definition of definitions) {
|
|
202
|
+
const delivery = definition.render;
|
|
203
|
+
if (delivery.kind === "document" && !existingInputs.has(delivery.absolutePath)) {
|
|
204
|
+
toInject[entryNameFor(definition)] = delivery.absolutePath;
|
|
205
|
+
existingInputs.add(delivery.absolutePath);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (Object.keys(toInject).length > 0) {
|
|
209
|
+
config.build ??= {};
|
|
210
|
+
config.build.rollupOptions ??= {};
|
|
211
|
+
config.build.rollupOptions.input = {
|
|
212
|
+
...toInputRecord(config.build.rollupOptions.input, root),
|
|
213
|
+
...toInject
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
configResolved(config) {
|
|
218
|
+
resolvedConfig = config;
|
|
219
|
+
for (const definition of definitions) checkDeliveryExists(definition, definition.render);
|
|
220
|
+
},
|
|
221
|
+
configureServer(server) {
|
|
222
|
+
server.middlewares.use(async (req, res, next) => {
|
|
223
|
+
if (req.url === `/${manifestPath}`) {
|
|
224
|
+
const base = resolvedConfig.base ?? "/";
|
|
225
|
+
const devManifests = definitions.map((definition$1) => ({
|
|
226
|
+
...buildManifestBase(definition$1),
|
|
227
|
+
remote: { render: devManifestDelivery(root, base, definition$1, definition$1.render) }
|
|
228
|
+
}));
|
|
229
|
+
res.setHeader("Content-Type", "application/json");
|
|
230
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
231
|
+
res.end(JSON.stringify({ modules: devManifests }, null, 2));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const bundleTarget = matchDevBundleRequest(req.url, definitions);
|
|
235
|
+
if (!bundleTarget) {
|
|
236
|
+
next();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const { absolutePath, definition } = bundleTarget;
|
|
240
|
+
try {
|
|
241
|
+
const code = await bundleForDev(absolutePath, root, resolvedConfig);
|
|
242
|
+
res.setHeader("Content-Type", "application/javascript; charset=utf-8");
|
|
243
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
244
|
+
res.setHeader("Cache-Control", "no-store");
|
|
245
|
+
res.end(code);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
server.config.logger.error(`[persona-layout-modules] failed to dev-bundle ${definition.definitionFilePath}: ${String(error)}`);
|
|
248
|
+
res.statusCode = 500;
|
|
249
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
250
|
+
res.end(`[persona-layout-modules] failed to bundle: ${String(error)}`);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
},
|
|
254
|
+
name: "persona-layout-modules"
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
const DEV_BUNDLE_PREFIX = "/@persona-layout-module";
|
|
258
|
+
/**
|
|
259
|
+
* Every absolute path Vite is ALREADY going to build, per whatever the app's
|
|
260
|
+
* own `build.rollupOptions.input` says (or the implicit default — Vite
|
|
261
|
+
* treats `<root>/index.html` as the one entry when nothing is configured).
|
|
262
|
+
* Used to decide whether a `document` entry needs injecting at all.
|
|
263
|
+
*/
|
|
264
|
+
function absoluteInputPaths(config, root) {
|
|
265
|
+
const input = config.build?.rollupOptions?.input;
|
|
266
|
+
if (input == null) return new Set([path.join(root, "index.html")]);
|
|
267
|
+
if (typeof input === "string") return new Set([path.resolve(root, input)]);
|
|
268
|
+
if (Array.isArray(input)) return new Set(input.map((entry) => path.resolve(root, entry)));
|
|
269
|
+
return new Set(Object.values(input).map((entry) => path.resolve(root, entry)));
|
|
270
|
+
}
|
|
271
|
+
function assertUniqueKeys(manifests) {
|
|
272
|
+
const seen = /* @__PURE__ */ new Set();
|
|
273
|
+
for (const manifest of manifests) {
|
|
274
|
+
if (seen.has(manifest.key)) throw new Error(`[persona-layout-modules] duplicate module key "${manifest.key}" — keys must be unique within an app`);
|
|
275
|
+
seen.add(manifest.key);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/** Fields shared between the prod and dev manifest — everything but `remote`. */
|
|
279
|
+
function buildManifestBase(definition) {
|
|
280
|
+
const { input } = definition;
|
|
281
|
+
return {
|
|
282
|
+
allowAddingToLayout: input.allowAddingToLayout,
|
|
283
|
+
allowDynamicHeight: input.allowDynamicHeight,
|
|
284
|
+
badge: input.badge,
|
|
285
|
+
category: input.category,
|
|
286
|
+
configSchema: input.configSchema ? z.toJSONSchema(input.configSchema) : void 0,
|
|
287
|
+
defaultSize: input.defaultSize,
|
|
288
|
+
descriptionShort: input.descriptionShort,
|
|
289
|
+
displayName: input.displayName,
|
|
290
|
+
icon: input.icon,
|
|
291
|
+
internalUsersOnly: input.internalUsersOnly,
|
|
292
|
+
key: input.key,
|
|
293
|
+
layouts: input.layouts ?? [],
|
|
294
|
+
manifestVersion: "v1",
|
|
295
|
+
uiConfigSchema: input.uiConfigSchema
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Bundles a single `code` entry (with its dependencies inlined) for dev-mode
|
|
300
|
+
* cross-origin serving. Deliberately re-run on every request rather than
|
|
301
|
+
* cached — these are small files and esbuild is fast enough that freshness
|
|
302
|
+
* on every reload matters more than shaving milliseconds.
|
|
303
|
+
*/
|
|
304
|
+
async function bundleForDev(absolutePath, root, resolvedConfig) {
|
|
305
|
+
const shimAlias = await ensureRemoteShimFiles();
|
|
306
|
+
const [output] = (await build({
|
|
307
|
+
absWorkingDir: root,
|
|
308
|
+
alias: {
|
|
309
|
+
...resolveAliasForEsbuild(resolvedConfig),
|
|
310
|
+
...shimAlias
|
|
311
|
+
},
|
|
312
|
+
bundle: true,
|
|
313
|
+
entryPoints: [absolutePath],
|
|
314
|
+
format: "esm",
|
|
315
|
+
jsx: "automatic",
|
|
316
|
+
sourcemap: "inline",
|
|
317
|
+
write: false
|
|
318
|
+
})).outputFiles;
|
|
319
|
+
if (!output) throw new Error("esbuild produced no output");
|
|
320
|
+
return output.text;
|
|
321
|
+
}
|
|
322
|
+
function checkDeliveryExists(definition, delivery) {
|
|
323
|
+
if (!existsSync(delivery.absolutePath)) throw new Error(`[persona-layout-modules] ${definition.definitionFilePath}: render "${delivery.sourcePath}" does not resolve to a file (${delivery.absolutePath})`);
|
|
324
|
+
}
|
|
325
|
+
function collectCss(chunk, into) {
|
|
326
|
+
for (const fileName of chunk.viteMetadata?.importedCss ?? []) into.add(fileName);
|
|
327
|
+
}
|
|
328
|
+
function devBundleUrl(definition) {
|
|
329
|
+
return `${DEV_BUNDLE_PREFIX}/${entryNameFor(definition)}.js`;
|
|
330
|
+
}
|
|
331
|
+
function devManifestDelivery(root, base, definition, delivery) {
|
|
332
|
+
if (delivery.kind === "document") return { document: documentUrlFor(root, base, delivery) };
|
|
333
|
+
return { code: devBundleUrl(definition) };
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* A `document` delivery's final PRODUCTION URL: the author's `path` override
|
|
337
|
+
* when given (a logical SPA route, not a file location — used as-is, never
|
|
338
|
+
* joined with `base`, matching how a subapp's own manifest URLs work),
|
|
339
|
+
* otherwise the html file's own path relative to root (which Vite preserves
|
|
340
|
+
* in its build output for multi-page entries) joined with `base`, since
|
|
341
|
+
* that IS a real build-output location.
|
|
342
|
+
*/
|
|
343
|
+
function documentUrlFor(root, base, delivery) {
|
|
344
|
+
if (delivery.path) return delivery.path;
|
|
345
|
+
return joinUrl(base, path.relative(root, delivery.absolutePath).split(path.sep).join("/"));
|
|
346
|
+
}
|
|
347
|
+
function entryNameFor(definition) {
|
|
348
|
+
return `layout-module--${definition.input.key}`;
|
|
349
|
+
}
|
|
350
|
+
function findChunkByFacadeModule(emitted, facadeModuleId) {
|
|
351
|
+
for (const file of emitted) if (file.type === "chunk" && file.facadeModuleId === facadeModuleId) return file;
|
|
352
|
+
}
|
|
353
|
+
function joinUrl(base, fileName) {
|
|
354
|
+
return `${base.endsWith("/") ? base : `${base}/`}${fileName}`.replace(/\/{2,}/g, "/");
|
|
355
|
+
}
|
|
356
|
+
function matchDevBundleRequest(url, definitions) {
|
|
357
|
+
if (!url?.startsWith(`${DEV_BUNDLE_PREFIX}/`) || !url.endsWith(".js")) return;
|
|
358
|
+
const name = url.slice(24, -3);
|
|
359
|
+
for (const definition of definitions) if (definition.render.kind === "code" && name === entryNameFor(definition)) return {
|
|
360
|
+
absolutePath: definition.render.absolutePath,
|
|
361
|
+
definition
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Vite normalizes `resolve.alias` (however the user wrote it — object or
|
|
366
|
+
* array) into an array of `{ find, replacement }`. esbuild's own `alias`
|
|
367
|
+
* option only accepts exact string keys (no RegExp, no partial-path
|
|
368
|
+
* matching) — a real limitation this doesn't try to hide, just the common
|
|
369
|
+
* case: a plain string-to-string alias map, which is what every `sa-*`
|
|
370
|
+
* subapp's `vite.config.ts` actually uses.
|
|
371
|
+
*/
|
|
372
|
+
function resolveAliasForEsbuild(resolvedConfig) {
|
|
373
|
+
const alias = {};
|
|
374
|
+
for (const entry of resolvedConfig.resolve?.alias ?? []) if (typeof entry.find === "string") alias[entry.find] = entry.replacement;
|
|
375
|
+
return alias;
|
|
376
|
+
}
|
|
377
|
+
/** `.html` → `document`; anything else (`.ts`/`.tsx`/`.js`/`.jsx`) → `code`. */
|
|
378
|
+
function resolveEntry(root, spec) {
|
|
379
|
+
const absolutePath = path.resolve(root, spec.entry);
|
|
380
|
+
if (absolutePath.endsWith(".html")) return {
|
|
381
|
+
absolutePath,
|
|
382
|
+
kind: "document",
|
|
383
|
+
path: spec.path,
|
|
384
|
+
sourcePath: spec.entry
|
|
385
|
+
};
|
|
386
|
+
return {
|
|
387
|
+
absolutePath,
|
|
388
|
+
kind: "code",
|
|
389
|
+
sourcePath: spec.entry
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Resolves the `render` delivery into its final manifest value for the
|
|
394
|
+
* PRODUCTION path. `document` uses `documentUrlFor` and touches nothing else —
|
|
395
|
+
* no chunk lookup, since it was never part of this plugin's own isolated
|
|
396
|
+
* build. `code` becomes `{ code: <hashed chunk URL> }`, throwing if the
|
|
397
|
+
* isolated build didn't emit a chunk for it. Also returns the chunk itself
|
|
398
|
+
* (when there is one) so the caller can collect its CSS without a second
|
|
399
|
+
* lookup.
|
|
400
|
+
*/
|
|
401
|
+
function resolveManifestDelivery(definition, delivery, emitted, root, base) {
|
|
402
|
+
if (delivery.kind === "document") return { value: { document: documentUrlFor(root, base, delivery) } };
|
|
403
|
+
const chunk = findChunkByFacadeModule(emitted, delivery.absolutePath);
|
|
404
|
+
if (!chunk) throw new Error(`[persona-layout-modules] ${definition.definitionFilePath}: could not locate the emitted chunk for render "${delivery.sourcePath}" in the isolated module build.`);
|
|
405
|
+
return {
|
|
406
|
+
chunk,
|
|
407
|
+
value: { code: joinUrl(base, chunk.fileName) }
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Normalizes whatever shape `build.rollupOptions.input` currently has (unset,
|
|
412
|
+
* a string, an array, or already an object) into the object form, so a newly
|
|
413
|
+
* injected `document` entry can be merged in as an additional key WITHOUT
|
|
414
|
+
* dropping whatever was already configured — including the implicit default
|
|
415
|
+
* `index.html` entry when nothing was configured at all, which would
|
|
416
|
+
* otherwise silently disappear the moment this plugin assigns an object here.
|
|
417
|
+
*/
|
|
418
|
+
function toInputRecord(input, root) {
|
|
419
|
+
if (input == null) return { index: path.join(root, "index.html") };
|
|
420
|
+
if (typeof input === "string") return { index: path.resolve(root, input) };
|
|
421
|
+
if (Array.isArray(input)) {
|
|
422
|
+
const record$1 = {};
|
|
423
|
+
for (const entry of input) {
|
|
424
|
+
const absolutePath = path.resolve(root, entry);
|
|
425
|
+
record$1[path.basename(absolutePath, path.extname(absolutePath))] = absolutePath;
|
|
426
|
+
}
|
|
427
|
+
return record$1;
|
|
428
|
+
}
|
|
429
|
+
const record = {};
|
|
430
|
+
for (const [name, entry] of Object.entries(input)) record[name] = path.resolve(root, entry);
|
|
431
|
+
return record;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
//#endregion
|
|
435
|
+
export { defineLayoutModule, personaLayoutModules };
|
package/package.json
CHANGED
|
@@ -1,10 +1,123 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@persona/layout-editor-remote-module",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Shared manifest format, Vite plugin, and host/iframe bridge for remotely-defined dashboard layout modules (app-builder Apps and sa-* subapps).",
|
|
5
5
|
"license": "SEE LICENSE",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"types": "./dist/index.d.mts",
|
|
8
|
+
"typesVersions": {
|
|
9
|
+
"*": {
|
|
10
|
+
"vite-plugin": [
|
|
11
|
+
"dist/vite-plugin.d.mts"
|
|
12
|
+
],
|
|
13
|
+
"bridge-host": [
|
|
14
|
+
"dist/bridge-host.d.mts"
|
|
15
|
+
],
|
|
16
|
+
"bridge-frame": [
|
|
17
|
+
"dist/bridge-frame.d.mts"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
23
|
+
},
|
|
6
24
|
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"skills",
|
|
7
27
|
"LICENSE"
|
|
8
28
|
],
|
|
9
|
-
"
|
|
10
|
-
|
|
29
|
+
"keywords": [
|
|
30
|
+
"tanstack-intent"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsdown",
|
|
34
|
+
"check": "yarn build && yarn skills:check && yarn test:run",
|
|
35
|
+
"skills:check": "intent validate",
|
|
36
|
+
"test": "vitest",
|
|
37
|
+
"test:run": "vitest run",
|
|
38
|
+
"release": "semantic-release"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"esbuild": ">=0.21.0 <1",
|
|
42
|
+
"react": "^17 || ^18 || ^19",
|
|
43
|
+
"react-dom": "^17 || ^18 || ^19",
|
|
44
|
+
"vite": "^5 || ^6 || ^7 || ^8",
|
|
45
|
+
"zod": "^4.3"
|
|
46
|
+
},
|
|
47
|
+
"peerDependenciesMeta": {
|
|
48
|
+
"react": {
|
|
49
|
+
"optional": true
|
|
50
|
+
},
|
|
51
|
+
"react-dom": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@semantic-release/exec": "^7.1.0",
|
|
57
|
+
"@tanstack/intent": "^0.3.6",
|
|
58
|
+
"@testing-library/dom": "^10.4.1",
|
|
59
|
+
"@testing-library/react": "^16.3.0",
|
|
60
|
+
"@types/node": "^22.10.0",
|
|
61
|
+
"@types/react": "^19.1.12",
|
|
62
|
+
"@types/react-dom": "^19.1.9",
|
|
63
|
+
"@typescript/native": "npm:typescript@^7.0.2",
|
|
64
|
+
"@vitest/browser": "~4.1.11",
|
|
65
|
+
"@vitest/browser-playwright": "~4.1.11",
|
|
66
|
+
"esbuild": "^0.28.0",
|
|
67
|
+
"jsdom": "^30.0.1",
|
|
68
|
+
"playwright": "1.62.1",
|
|
69
|
+
"react": "^18.3.1",
|
|
70
|
+
"react-dom": "^18.3.1",
|
|
71
|
+
"rollup": "^4.0.0",
|
|
72
|
+
"semantic-release": "^25.0.7",
|
|
73
|
+
"semantic-release-monorepo": "^8.0.2",
|
|
74
|
+
"styled-components": "^5.3.5",
|
|
75
|
+
"tsdown": "^0.18.1",
|
|
76
|
+
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
77
|
+
"vite": "^7.0.0",
|
|
78
|
+
"vitest": "~4.1.11",
|
|
79
|
+
"zod": "4.3.6"
|
|
80
|
+
},
|
|
81
|
+
"publishConfig": {
|
|
82
|
+
"registry": "https://registry.npmjs.org/",
|
|
83
|
+
"access": "public"
|
|
84
|
+
},
|
|
85
|
+
"release": {
|
|
86
|
+
"extends": "semantic-release-monorepo",
|
|
87
|
+
"branches": [
|
|
88
|
+
"main"
|
|
89
|
+
],
|
|
90
|
+
"tagFormat": "remote-module-v${version}",
|
|
91
|
+
"plugins": [
|
|
92
|
+
"@semantic-release/commit-analyzer",
|
|
93
|
+
"@semantic-release/release-notes-generator",
|
|
94
|
+
[
|
|
95
|
+
"@semantic-release/exec",
|
|
96
|
+
{
|
|
97
|
+
"prepareCmd": "yarn version ${nextRelease.version}",
|
|
98
|
+
"publishCmd": "yarn npm publish --staged --access public"
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
"@semantic-release/github"
|
|
102
|
+
]
|
|
103
|
+
},
|
|
104
|
+
"exports": {
|
|
105
|
+
".": {
|
|
106
|
+
"types": "./dist/index.d.mts",
|
|
107
|
+
"import": "./dist/index.mjs"
|
|
108
|
+
},
|
|
109
|
+
"./vite-plugin": {
|
|
110
|
+
"types": "./dist/vite-plugin.d.mts",
|
|
111
|
+
"import": "./dist/vite-plugin.mjs"
|
|
112
|
+
},
|
|
113
|
+
"./bridge-host": {
|
|
114
|
+
"types": "./dist/bridge-host.d.mts",
|
|
115
|
+
"import": "./dist/bridge-host.mjs"
|
|
116
|
+
},
|
|
117
|
+
"./bridge-frame": {
|
|
118
|
+
"types": "./dist/bridge-frame.d.mts",
|
|
119
|
+
"import": "./dist/bridge-frame.mjs"
|
|
120
|
+
},
|
|
121
|
+
"./package.json": "./package.json"
|
|
122
|
+
}
|
|
123
|
+
}
|