@jay-framework/stack-server-build 0.24.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +664 -0
- package/dist/index.js +2293 -0
- package/package.json +49 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2293 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
import fs$1 from "node:fs/promises";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import path__default from "node:path";
|
|
7
|
+
import { parseJayFile, JAY_IMPORT_RESOLVER, injectHeadfullFSTemplates, discoverHeadlessInstances, assignCoordinatesToJayHtml, generateServerElementFile, parseAction } from "@jay-framework/compiler-jay-html";
|
|
8
|
+
import { createRequire } from "module";
|
|
9
|
+
import jsBeautify from "js-beautify";
|
|
10
|
+
import fs$2 from "fs";
|
|
11
|
+
import path$1 from "path";
|
|
12
|
+
import YAML from "yaml";
|
|
13
|
+
import { serializeHeadTags, mergeHeadTags, actionRegistry, scanPlugins, resolveServices, sortPluginsByDependencies, getServiceRegistry } from "@jay-framework/stack-server-runtime";
|
|
14
|
+
export * from "@jay-framework/stack-server-runtime";
|
|
15
|
+
import crypto from "node:crypto";
|
|
16
|
+
import { getLogger } from "@jay-framework/logger";
|
|
17
|
+
import * as fs from "node:fs";
|
|
18
|
+
import { createRequire as createRequire$1 } from "node:module";
|
|
19
|
+
import { isJayAction, isJayStreamAction, isJayCliCommand } from "@jay-framework/fullstack-component";
|
|
20
|
+
const require$3 = createRequire(import.meta.url);
|
|
21
|
+
async function loadPageParts(vite, route, pagesBase, projectBase, jayRollupConfig, options) {
|
|
22
|
+
const exists = await fs$1.access(route.compPath, fs$1.constants.F_OK).then(() => true).catch(() => false);
|
|
23
|
+
const parts = [];
|
|
24
|
+
const validations = [];
|
|
25
|
+
if (exists) {
|
|
26
|
+
const exportName = route.componentExport || "page";
|
|
27
|
+
const pageComponent = (await vite.ssrLoadModule(route.compPath))[exportName];
|
|
28
|
+
if (!pageComponent) {
|
|
29
|
+
validations.push(
|
|
30
|
+
`${route.compPath} does not export "${exportName}". The file exists but the expected export is missing — remove the file or add the export.`
|
|
31
|
+
);
|
|
32
|
+
} else {
|
|
33
|
+
const clientImportPath = route.componentExport ? route.compPath.replace(/index\.js$/, "index.client.js") : route.compPath;
|
|
34
|
+
parts.push({
|
|
35
|
+
compDefinition: pageComponent,
|
|
36
|
+
clientImport: `import {${exportName}} from '${clientImportPath}'`,
|
|
37
|
+
clientPart: `{comp: ${exportName}.comp, contextMarkers: ${exportName}.contexts || []}`
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const jayHtmlFilePath = options?.preRenderedPath ?? route.jayHtmlPath;
|
|
42
|
+
const jayHtmlSource = options?.preRenderedContent ?? (await fs$1.readFile(jayHtmlFilePath)).toString();
|
|
43
|
+
const fileName = path__default.basename(route.jayHtmlPath);
|
|
44
|
+
const dirName = path__default.dirname(route.jayHtmlPath);
|
|
45
|
+
const jayHtmlWithValidations = await parseJayFile(
|
|
46
|
+
jayHtmlSource,
|
|
47
|
+
fileName,
|
|
48
|
+
dirName,
|
|
49
|
+
{
|
|
50
|
+
relativePath: jayRollupConfig.tsConfigFilePath
|
|
51
|
+
},
|
|
52
|
+
JAY_IMPORT_RESOLVER,
|
|
53
|
+
projectBase
|
|
54
|
+
);
|
|
55
|
+
jayHtmlWithValidations.validations.push(...validations);
|
|
56
|
+
return jayHtmlWithValidations.mapAsync(async (jayHtml) => {
|
|
57
|
+
const usedPackages = /* @__PURE__ */ new Set();
|
|
58
|
+
const headlessInstanceComponents = [];
|
|
59
|
+
for await (const headlessImport of jayHtml.headlessImports) {
|
|
60
|
+
const module = headlessImport.codeLink.module;
|
|
61
|
+
const name = headlessImport.codeLink.names[0].name;
|
|
62
|
+
const isLocalModule = module[0] === "." || module[0] === "/";
|
|
63
|
+
let compDefinition;
|
|
64
|
+
let clientModuleImport;
|
|
65
|
+
let isNpmPackage;
|
|
66
|
+
if (headlessImport.structural) {
|
|
67
|
+
continue;
|
|
68
|
+
} else {
|
|
69
|
+
const modulePath = isLocalModule ? path__default.resolve(dirName, module) : require$3.resolve(module, { paths: require$3.resolve.paths(dirName) });
|
|
70
|
+
compDefinition = (await vite.ssrLoadModule(modulePath))[name];
|
|
71
|
+
const moduleImport = isLocalModule ? path__default.resolve(dirName, module) : module;
|
|
72
|
+
isNpmPackage = !isLocalModule;
|
|
73
|
+
clientModuleImport = isNpmPackage ? `${moduleImport}/client` : `${moduleImport}`;
|
|
74
|
+
}
|
|
75
|
+
if (isNpmPackage) {
|
|
76
|
+
const packageName = module.startsWith("@") ? module.split("/").slice(0, 2).join("/") : module.split("/")[0];
|
|
77
|
+
usedPackages.add(packageName);
|
|
78
|
+
}
|
|
79
|
+
if (headlessImport.key) {
|
|
80
|
+
const key = headlessImport.key;
|
|
81
|
+
const hasClientComp = !!compDefinition?.fastRender || !!compDefinition?.hasInteractive;
|
|
82
|
+
const part = {
|
|
83
|
+
key,
|
|
84
|
+
compDefinition,
|
|
85
|
+
clientImport: hasClientComp ? `import {${name}} from '${clientModuleImport}'` : "",
|
|
86
|
+
clientPart: hasClientComp ? `{comp: ${name}.comp, contextMarkers: ${name}.contexts || [], key: '${key}'}` : "",
|
|
87
|
+
contractInfo: headlessImport.contract ? {
|
|
88
|
+
contractName: headlessImport.contract.name,
|
|
89
|
+
metadata: headlessImport.metadata
|
|
90
|
+
} : void 0,
|
|
91
|
+
headlessProps: headlessImport.headlessProps
|
|
92
|
+
};
|
|
93
|
+
parts.push(part);
|
|
94
|
+
}
|
|
95
|
+
if (!headlessImport.key && headlessImport.contract) {
|
|
96
|
+
headlessInstanceComponents.push({
|
|
97
|
+
contractName: headlessImport.contractName,
|
|
98
|
+
compDefinition,
|
|
99
|
+
contract: headlessImport.contract
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const headlessContracts = jayHtml.headlessImports.filter((hi) => hi.contract !== void 0 && hi.key !== void 0).map((hi) => ({
|
|
104
|
+
key: hi.key,
|
|
105
|
+
contract: hi.contract,
|
|
106
|
+
contractPath: hi.contractPath
|
|
107
|
+
}));
|
|
108
|
+
const jayHtmlForDiscovery = injectHeadfullFSTemplates(
|
|
109
|
+
jayHtmlSource,
|
|
110
|
+
dirName,
|
|
111
|
+
JAY_IMPORT_RESOLVER
|
|
112
|
+
);
|
|
113
|
+
let discoveryResult;
|
|
114
|
+
if (headlessInstanceComponents.length > 0) {
|
|
115
|
+
const firstDiscovery = discoverHeadlessInstances(jayHtmlForDiscovery);
|
|
116
|
+
const headlessContractNameSet = new Set(
|
|
117
|
+
jayHtml.headlessImports.map((hi) => hi.contractName)
|
|
118
|
+
);
|
|
119
|
+
const jayHtmlWithCoords = assignCoordinatesToJayHtml(
|
|
120
|
+
firstDiscovery.preRenderedJayHtml,
|
|
121
|
+
headlessContractNameSet
|
|
122
|
+
);
|
|
123
|
+
discoveryResult = discoverHeadlessInstances(jayHtmlWithCoords);
|
|
124
|
+
} else {
|
|
125
|
+
discoveryResult = {
|
|
126
|
+
instances: [],
|
|
127
|
+
forEachInstances: [],
|
|
128
|
+
preRenderedJayHtml: jayHtmlSource
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
parts,
|
|
133
|
+
serverTrackByMap: jayHtml.serverTrackByMap,
|
|
134
|
+
clientTrackByMap: jayHtml.clientTrackByMap,
|
|
135
|
+
usedPackages,
|
|
136
|
+
headlessContracts,
|
|
137
|
+
headlessInstanceComponents,
|
|
138
|
+
discoveredInstances: discoveryResult.instances,
|
|
139
|
+
forEachInstances: discoveryResult.forEachInstances,
|
|
140
|
+
linkedCssFiles: jayHtml.linkedCssFiles ?? [],
|
|
141
|
+
linkedComponentFiles: jayHtml.linkedComponentFiles ?? [],
|
|
142
|
+
scripts: jayHtml.scripts
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
var __defProp2 = Object.defineProperty;
|
|
147
|
+
var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
148
|
+
var __publicField2 = (obj, key, value) => __defNormalProp2(obj, key + "", value);
|
|
149
|
+
class JayAtomicType {
|
|
150
|
+
constructor(name) {
|
|
151
|
+
__publicField2(this, "kind", 0);
|
|
152
|
+
this.name = name;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
new JayAtomicType("string");
|
|
156
|
+
new JayAtomicType("string");
|
|
157
|
+
new JayAtomicType("number");
|
|
158
|
+
new JayAtomicType("boolean");
|
|
159
|
+
new JayAtomicType("Date");
|
|
160
|
+
new JayAtomicType("file");
|
|
161
|
+
new JayAtomicType("Unknown");
|
|
162
|
+
class JayObjectType {
|
|
163
|
+
constructor(name, props) {
|
|
164
|
+
__publicField2(this, "kind", 8);
|
|
165
|
+
this.name = name;
|
|
166
|
+
this.props = props;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
new JayObjectType("Error", {
|
|
170
|
+
message: new JayAtomicType("string"),
|
|
171
|
+
name: new JayAtomicType("string"),
|
|
172
|
+
stack: new JayAtomicType("string")
|
|
173
|
+
});
|
|
174
|
+
function isOptionalType(aType) {
|
|
175
|
+
return aType.kind === 14;
|
|
176
|
+
}
|
|
177
|
+
function isAtomicType(aType) {
|
|
178
|
+
return aType.kind === 0;
|
|
179
|
+
}
|
|
180
|
+
function isEnumType(aType) {
|
|
181
|
+
return aType.kind === 2;
|
|
182
|
+
}
|
|
183
|
+
function isImportedType(aType) {
|
|
184
|
+
return aType.kind === 4;
|
|
185
|
+
}
|
|
186
|
+
function isObjectType(aType) {
|
|
187
|
+
return aType.kind === 8;
|
|
188
|
+
}
|
|
189
|
+
function isArrayType(aType) {
|
|
190
|
+
return aType.kind === 9;
|
|
191
|
+
}
|
|
192
|
+
function isRecordType(aType) {
|
|
193
|
+
return aType.kind === 11;
|
|
194
|
+
}
|
|
195
|
+
function jayTypeToJsonSchema(type) {
|
|
196
|
+
if (isOptionalType(type)) {
|
|
197
|
+
return jayTypeToJsonSchema(type.innerType);
|
|
198
|
+
}
|
|
199
|
+
if (isAtomicType(type)) {
|
|
200
|
+
const name = type.name.toLowerCase();
|
|
201
|
+
if (name === "string" || name === "number" || name === "boolean") {
|
|
202
|
+
return { type: name };
|
|
203
|
+
}
|
|
204
|
+
if (name === "file") {
|
|
205
|
+
return { type: "string", description: "Binary file upload (JayFile)" };
|
|
206
|
+
}
|
|
207
|
+
return { type: "string" };
|
|
208
|
+
}
|
|
209
|
+
if (isEnumType(type)) {
|
|
210
|
+
return { type: "string", enum: type.values };
|
|
211
|
+
}
|
|
212
|
+
if (isImportedType(type)) {
|
|
213
|
+
return { type: "object", description: `Contract: ${type.name}` };
|
|
214
|
+
}
|
|
215
|
+
if (isArrayType(type)) {
|
|
216
|
+
const itemSchema = jayTypeToJsonSchema(type.itemType);
|
|
217
|
+
if (itemSchema) {
|
|
218
|
+
return { type: "array", items: itemSchema };
|
|
219
|
+
}
|
|
220
|
+
return { type: "array" };
|
|
221
|
+
}
|
|
222
|
+
if (isRecordType(type)) {
|
|
223
|
+
const valueSchema = jayTypeToJsonSchema(type.itemType);
|
|
224
|
+
if (valueSchema) {
|
|
225
|
+
return { type: "object", additionalProperties: valueSchema };
|
|
226
|
+
}
|
|
227
|
+
return { type: "object" };
|
|
228
|
+
}
|
|
229
|
+
if (isObjectType(type)) {
|
|
230
|
+
const properties = {};
|
|
231
|
+
const required = [];
|
|
232
|
+
for (const [key, propType] of Object.entries(type.props)) {
|
|
233
|
+
const isOpt = isOptionalType(propType);
|
|
234
|
+
const schema = jayTypeToJsonSchema(propType);
|
|
235
|
+
if (schema) {
|
|
236
|
+
properties[key] = schema;
|
|
237
|
+
if (!isOpt) {
|
|
238
|
+
required.push(key);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
type: "object",
|
|
244
|
+
properties,
|
|
245
|
+
...required.length > 0 && { required }
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
var RuntimeMode = /* @__PURE__ */ ((RuntimeMode2) => {
|
|
251
|
+
RuntimeMode2["MainTrusted"] = "mainTrusted";
|
|
252
|
+
RuntimeMode2["MainSandbox"] = "mainSandbox";
|
|
253
|
+
RuntimeMode2["WorkerTrusted"] = "workerTrusted";
|
|
254
|
+
RuntimeMode2["WorkerSandbox"] = "workerSandbox";
|
|
255
|
+
return RuntimeMode2;
|
|
256
|
+
})(RuntimeMode || {});
|
|
257
|
+
const TS_EXTENSION = ".ts";
|
|
258
|
+
const JAY_QUERY_PREFIX = "?jay-";
|
|
259
|
+
const JAY_QUERY_HYDRATE = `${JAY_QUERY_PREFIX}hydrate`;
|
|
260
|
+
[
|
|
261
|
+
// Hydrate target
|
|
262
|
+
{
|
|
263
|
+
pattern: JAY_QUERY_HYDRATE,
|
|
264
|
+
buildEnv: "client",
|
|
265
|
+
isHydrate: true
|
|
266
|
+
},
|
|
267
|
+
// Build environments
|
|
268
|
+
{
|
|
269
|
+
pattern: `${JAY_QUERY_PREFIX}${"client"}`,
|
|
270
|
+
buildEnv: "client"
|
|
271
|
+
/* Client */
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
pattern: `${JAY_QUERY_PREFIX}${"server"}`,
|
|
275
|
+
buildEnv: "server"
|
|
276
|
+
/* Server */
|
|
277
|
+
},
|
|
278
|
+
// Runtime modes (with .ts suffix)
|
|
279
|
+
{
|
|
280
|
+
pattern: `${JAY_QUERY_PREFIX}${RuntimeMode.MainSandbox}${TS_EXTENSION}`,
|
|
281
|
+
runtimeMode: RuntimeMode.MainSandbox
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
pattern: `${JAY_QUERY_PREFIX}${RuntimeMode.WorkerTrusted}${TS_EXTENSION}`,
|
|
285
|
+
runtimeMode: RuntimeMode.WorkerTrusted
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
pattern: `${JAY_QUERY_PREFIX}${RuntimeMode.WorkerSandbox}${TS_EXTENSION}`,
|
|
289
|
+
runtimeMode: RuntimeMode.WorkerSandbox
|
|
290
|
+
},
|
|
291
|
+
// Runtime modes (without .ts suffix)
|
|
292
|
+
{
|
|
293
|
+
pattern: `${JAY_QUERY_PREFIX}${RuntimeMode.MainSandbox}`,
|
|
294
|
+
runtimeMode: RuntimeMode.MainSandbox
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
pattern: `${JAY_QUERY_PREFIX}${RuntimeMode.WorkerTrusted}`,
|
|
298
|
+
runtimeMode: RuntimeMode.WorkerTrusted
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
pattern: `${JAY_QUERY_PREFIX}${RuntimeMode.WorkerSandbox}`,
|
|
302
|
+
runtimeMode: RuntimeMode.WorkerSandbox
|
|
303
|
+
}
|
|
304
|
+
];
|
|
305
|
+
function checkValidationErrors(withValidations) {
|
|
306
|
+
const { validations } = withValidations;
|
|
307
|
+
if (validations.length > 0) {
|
|
308
|
+
throw new Error(validations.join("\n"));
|
|
309
|
+
}
|
|
310
|
+
return withValidations.val;
|
|
311
|
+
}
|
|
312
|
+
const { html: htmlBeautify } = jsBeautify;
|
|
313
|
+
createRequire(import.meta.url);
|
|
314
|
+
function normalizeActionEntry(entry) {
|
|
315
|
+
if (typeof entry === "string") {
|
|
316
|
+
return { name: entry };
|
|
317
|
+
}
|
|
318
|
+
return { name: entry.name, action: entry.action };
|
|
319
|
+
}
|
|
320
|
+
function loadPluginManifest(pluginDir) {
|
|
321
|
+
const pluginYamlPath = path$1.join(pluginDir, "plugin.yaml");
|
|
322
|
+
if (!fs$2.existsSync(pluginYamlPath)) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const yamlContent = fs$2.readFileSync(pluginYamlPath, "utf-8");
|
|
327
|
+
return YAML.parse(yamlContent);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const s = createRequire(import.meta.url), e = s("typescript");
|
|
333
|
+
new Proxy(e, {
|
|
334
|
+
get(t, r) {
|
|
335
|
+
return t[r];
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
function asyncSwapScript(id, html) {
|
|
339
|
+
const escapedHtml = html.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
340
|
+
return `<script>(function(){var t=document.querySelector('[jay-async="${id}:pending"]');if(t){var d=document.createElement('div');d.innerHTML='${escapedHtml}';t.replaceWith(d.firstChild);}window.__jay&&window.__jay.hydrateAsync&&window.__jay.hydrateAsync('${id}');})()<\/script>`;
|
|
341
|
+
}
|
|
342
|
+
function generatePromiseReconstruction(outcomes) {
|
|
343
|
+
if (outcomes.length === 0) return "";
|
|
344
|
+
return outcomes.map((outcome) => {
|
|
345
|
+
if (outcome.status === "resolved") {
|
|
346
|
+
return ` viewState[${JSON.stringify(outcome.id)}] = Promise.resolve(${JSON.stringify(outcome.value)});`;
|
|
347
|
+
} else {
|
|
348
|
+
const errMsg = outcome.value?.message ?? "Unknown error";
|
|
349
|
+
return ` viewState[${JSON.stringify(outcome.id)}] = Promise.reject(new Error(${JSON.stringify(errMsg)}));`;
|
|
350
|
+
}
|
|
351
|
+
}).join("\n") + "\n";
|
|
352
|
+
}
|
|
353
|
+
function buildScriptFragments(parts, clientInitData, projectInit, pluginInits, options) {
|
|
354
|
+
const { enableAutomation = true, slowViewState } = options;
|
|
355
|
+
const hasSlowViewState = slowViewState && Object.keys(slowViewState).length > 0;
|
|
356
|
+
const clientParts = parts.filter((part) => part.clientPart);
|
|
357
|
+
const partImports = clientParts.length > 0 ? clientParts.map((part) => part.clientImport).join("\n") + "\n" : "";
|
|
358
|
+
const compositeParts = clientParts.length > 0 ? `[
|
|
359
|
+
${clientParts.map((part) => " " + part.clientPart).join(",\n")}
|
|
360
|
+
]` : "[]";
|
|
361
|
+
const hasClientInit = projectInit || pluginInits.length > 0;
|
|
362
|
+
const pluginClientInitImports = pluginInits.map((plugin, idx) => {
|
|
363
|
+
return `import { ${plugin.initExport} as jayInit${idx} } from "${plugin.importPath}";`;
|
|
364
|
+
}).join("\n ");
|
|
365
|
+
const projectInitImport = projectInit ? `import { ${projectInit.initExport || "init"} as projectJayInit } from "${projectInit.importPath}";` : "";
|
|
366
|
+
const pluginClientInitCalls = pluginInits.map((plugin, idx) => {
|
|
367
|
+
const pluginData = clientInitData[plugin.name] || {};
|
|
368
|
+
return `if (typeof jayInit${idx}._clientInit === 'function') {
|
|
369
|
+
console.log('[DevServer] Running client init: ${plugin.name}');
|
|
370
|
+
await jayInit${idx}._clientInit(${JSON.stringify(pluginData)});
|
|
371
|
+
}`;
|
|
372
|
+
}).join("\n ");
|
|
373
|
+
const projectInitCall = projectInit ? `if (typeof projectJayInit._clientInit === 'function') {
|
|
374
|
+
console.log('[DevServer] Running client init: project');
|
|
375
|
+
const projectData = ${JSON.stringify(clientInitData["project"] || {})};
|
|
376
|
+
await projectJayInit._clientInit(projectData);
|
|
377
|
+
}` : "";
|
|
378
|
+
const clientInitExecution = hasClientInit ? `
|
|
379
|
+
// Plugin client initialization (in dependency order)
|
|
380
|
+
${pluginClientInitCalls}
|
|
381
|
+
|
|
382
|
+
// Project client initialization
|
|
383
|
+
${projectInitCall}
|
|
384
|
+
` : "";
|
|
385
|
+
const automationImport = enableAutomation ? hasSlowViewState ? `import { wrapWithAutomation, AUTOMATION_CONTEXT } from "@jay-framework/runtime-automation";
|
|
386
|
+
import { registerGlobalContext } from "@jay-framework/runtime";
|
|
387
|
+
import { deepMergeViewStates } from "@jay-framework/view-state-merge";` : `import { wrapWithAutomation, AUTOMATION_CONTEXT } from "@jay-framework/runtime-automation";
|
|
388
|
+
import { registerGlobalContext } from "@jay-framework/runtime";` : "";
|
|
389
|
+
const slowViewStateDecl = enableAutomation && hasSlowViewState ? `const slowViewState = ${JSON.stringify(slowViewState)};` : "";
|
|
390
|
+
return {
|
|
391
|
+
partImports,
|
|
392
|
+
compositeParts,
|
|
393
|
+
pluginClientInitImports,
|
|
394
|
+
projectInitImport,
|
|
395
|
+
clientInitExecution,
|
|
396
|
+
automationImport,
|
|
397
|
+
slowViewStateDecl
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function buildFreezeScript(routePattern) {
|
|
401
|
+
const routePatternLiteral = routePattern ? `'${routePattern}'` : "undefined";
|
|
402
|
+
return `
|
|
403
|
+
// Page Freeze (DL#127, DL#128 iframe addendum)
|
|
404
|
+
// Sticky embed mode: URL param sets a session cookie so in-iframe navigation preserves it
|
|
405
|
+
if (new URLSearchParams(window.location.search).has('_jay_embed')) {
|
|
406
|
+
document.cookie = '_jay_embed=1;path=/;samesite=lax';
|
|
407
|
+
}
|
|
408
|
+
const __jayEmbedMode = document.cookie.split(';').some(c => c.trim().startsWith('_jay_embed='));
|
|
409
|
+
|
|
410
|
+
async function __jayDoFreeze() {
|
|
411
|
+
const automation = window.__jay?.automation;
|
|
412
|
+
if (!automation) return;
|
|
413
|
+
|
|
414
|
+
// Visual feedback: white flash
|
|
415
|
+
const flash = document.createElement('div');
|
|
416
|
+
flash.style.cssText = 'position:fixed;inset:0;background:white;z-index:999999;opacity:0.8;pointer-events:none;transition:opacity 0.3s';
|
|
417
|
+
document.body.appendChild(flash);
|
|
418
|
+
requestAnimationFrame(() => { flash.style.opacity = '0'; });
|
|
419
|
+
setTimeout(() => flash.remove(), 400);
|
|
420
|
+
|
|
421
|
+
// Audio feedback: camera shutter
|
|
422
|
+
try {
|
|
423
|
+
const ctx = new AudioContext();
|
|
424
|
+
const buf = ctx.createBuffer(1, ctx.sampleRate * 0.15, ctx.sampleRate);
|
|
425
|
+
const data = buf.getChannelData(0);
|
|
426
|
+
for (let i = 0; i < data.length; i++) {
|
|
427
|
+
data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (ctx.sampleRate * 0.02));
|
|
428
|
+
}
|
|
429
|
+
const src = ctx.createBufferSource();
|
|
430
|
+
src.buffer = buf;
|
|
431
|
+
src.connect(ctx.destination);
|
|
432
|
+
src.start();
|
|
433
|
+
} catch {}
|
|
434
|
+
|
|
435
|
+
// Capture and save
|
|
436
|
+
try {
|
|
437
|
+
const state = automation.getPageState();
|
|
438
|
+
const route = window.location.pathname;
|
|
439
|
+
const routePattern = ${routePatternLiteral};
|
|
440
|
+
const resp = await fetch('/_jay/freeze', {
|
|
441
|
+
method: 'POST',
|
|
442
|
+
headers: { 'Content-Type': 'application/json' },
|
|
443
|
+
body: JSON.stringify({ route, routePattern, viewState: state.viewState }),
|
|
444
|
+
});
|
|
445
|
+
const { id } = await resp.json();
|
|
446
|
+
if (__jayEmbedMode) {
|
|
447
|
+
window.parent.postMessage({ type: 'jay:freeze', id, route }, '*');
|
|
448
|
+
} else {
|
|
449
|
+
window.open(route + '?_jay_freeze=' + id, '_blank');
|
|
450
|
+
}
|
|
451
|
+
} catch (err) {
|
|
452
|
+
console.error('[Freeze] Failed:', err);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (__jayEmbedMode) {
|
|
457
|
+
// Notify parent of the current route on load (DL#128 route addendum)
|
|
458
|
+
window.parent.postMessage({ type: 'jay:route', route: window.location.pathname, routePattern: ${routePatternLiteral} }, '*');
|
|
459
|
+
|
|
460
|
+
// Embed mode: parent triggers freeze via postMessage
|
|
461
|
+
window.addEventListener('message', (e) => {
|
|
462
|
+
if (e.data?.type === 'jay:requestFreeze') __jayDoFreeze();
|
|
463
|
+
});
|
|
464
|
+
} else {
|
|
465
|
+
// Standalone: Alt+S / Option+S keyboard shortcut
|
|
466
|
+
document.addEventListener('keydown', (e) => {
|
|
467
|
+
if (e.altKey && e.code === 'KeyS') {
|
|
468
|
+
e.preventDefault();
|
|
469
|
+
__jayDoFreeze();
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Targeted page reload: only reload if this page matches the changed route prefix
|
|
475
|
+
if (import.meta.hot) {
|
|
476
|
+
import.meta.hot.on('jay:page-reload', (data) => {
|
|
477
|
+
const prefix = data.routePrefix;
|
|
478
|
+
const pathname = window.location.pathname;
|
|
479
|
+
if (pathname === prefix || pathname.startsWith(prefix + '/')) {
|
|
480
|
+
window.location.reload();
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
}`;
|
|
484
|
+
}
|
|
485
|
+
function buildAutomationWrap(options, mode) {
|
|
486
|
+
const { enableAutomation = true, slowViewState } = options;
|
|
487
|
+
const hasSlowViewState = slowViewState && Object.keys(slowViewState).length > 0;
|
|
488
|
+
const appendDom = mode === "client";
|
|
489
|
+
if (!enableAutomation) {
|
|
490
|
+
return appendDom ? `
|
|
491
|
+
target.appendChild(instance.element.dom);` : "";
|
|
492
|
+
}
|
|
493
|
+
const appendLine = appendDom ? `
|
|
494
|
+
target.appendChild(wrapped.element.dom);` : "";
|
|
495
|
+
const freezeScript = buildFreezeScript(options.routePattern);
|
|
496
|
+
if (hasSlowViewState) {
|
|
497
|
+
return `
|
|
498
|
+
// Wrap with automation for dev tooling
|
|
499
|
+
// Deep merge slow+fast ViewState so automation can see full page state
|
|
500
|
+
const fullViewState = deepMergeViewStates(slowViewState, {...viewState, ...fastCarryForward}, trackByMap);
|
|
501
|
+
const wrapped = wrapWithAutomation(instance, { initialViewState: fullViewState, trackByMap });
|
|
502
|
+
registerGlobalContext(AUTOMATION_CONTEXT, wrapped.automation);
|
|
503
|
+
window.__jay = window.__jay || {};
|
|
504
|
+
window.__jay.automation = wrapped.automation;
|
|
505
|
+
window.dispatchEvent(new Event('jay:automation-ready'));${appendLine}
|
|
506
|
+
${freezeScript}`;
|
|
507
|
+
}
|
|
508
|
+
return `
|
|
509
|
+
// Wrap with automation for dev tooling
|
|
510
|
+
const wrapped = wrapWithAutomation(instance);
|
|
511
|
+
registerGlobalContext(AUTOMATION_CONTEXT, wrapped.automation);
|
|
512
|
+
window.__jay = window.__jay || {};
|
|
513
|
+
window.__jay.automation = wrapped.automation;
|
|
514
|
+
window.dispatchEvent(new Event('jay:automation-ready'));${appendLine}
|
|
515
|
+
${freezeScript}`;
|
|
516
|
+
}
|
|
517
|
+
async function resolveViewStatePromises(viewState) {
|
|
518
|
+
const entries = Object.entries(viewState);
|
|
519
|
+
const hasPromises = entries.some(([, v]) => v instanceof Promise);
|
|
520
|
+
if (!hasPromises) return { resolved: viewState, outcomes: [] };
|
|
521
|
+
const result = { ...viewState };
|
|
522
|
+
const outcomes = [];
|
|
523
|
+
for (const [key, value] of entries) {
|
|
524
|
+
if (value instanceof Promise) {
|
|
525
|
+
try {
|
|
526
|
+
const val = await value;
|
|
527
|
+
result[key] = val;
|
|
528
|
+
outcomes.push({ id: key, status: "resolved", value: val });
|
|
529
|
+
} catch (err) {
|
|
530
|
+
delete result[key];
|
|
531
|
+
outcomes.push({
|
|
532
|
+
id: key,
|
|
533
|
+
status: "rejected",
|
|
534
|
+
value: { message: err?.message ?? String(err) }
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return { resolved: result, outcomes };
|
|
540
|
+
}
|
|
541
|
+
async function generateClientScript(defaultViewState, fastCarryForward, parts, jayHtmlPath, trackByMap = {}, clientInitData = {}, projectInit, pluginInits = [], options = {}) {
|
|
542
|
+
const { resolved, outcomes } = await resolveViewStatePromises(defaultViewState);
|
|
543
|
+
defaultViewState = resolved;
|
|
544
|
+
const {
|
|
545
|
+
partImports,
|
|
546
|
+
compositeParts,
|
|
547
|
+
pluginClientInitImports,
|
|
548
|
+
projectInitImport,
|
|
549
|
+
clientInitExecution,
|
|
550
|
+
automationImport,
|
|
551
|
+
slowViewStateDecl
|
|
552
|
+
} = buildScriptFragments(parts, clientInitData, projectInit, pluginInits, options);
|
|
553
|
+
const automationWrap = buildAutomationWrap(options, "client");
|
|
554
|
+
return `<!doctype html>
|
|
555
|
+
<html lang="en">
|
|
556
|
+
<head>
|
|
557
|
+
<meta charset="UTF-8" />
|
|
558
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
559
|
+
<title>Vite + TS</title>
|
|
560
|
+
</head>
|
|
561
|
+
<body>
|
|
562
|
+
<div id="target"></div>
|
|
563
|
+
<script type="module">
|
|
564
|
+
import {makeCompositeJayComponent} from "@jay-framework/stack-client-runtime";
|
|
565
|
+
${automationImport}
|
|
566
|
+
${pluginClientInitImports}
|
|
567
|
+
${projectInitImport}
|
|
568
|
+
import { render } from '${jayHtmlPath}';
|
|
569
|
+
${partImports}${slowViewStateDecl}
|
|
570
|
+
const viewState = ${JSON.stringify(defaultViewState)};
|
|
571
|
+
${generatePromiseReconstruction(outcomes)}
|
|
572
|
+
const fastCarryForward = ${JSON.stringify(fastCarryForward)};
|
|
573
|
+
const trackByMap = ${JSON.stringify(trackByMap)};
|
|
574
|
+
${clientInitExecution}
|
|
575
|
+
const target = document.getElementById('target');
|
|
576
|
+
const pageComp = makeCompositeJayComponent(render, viewState, fastCarryForward, ${compositeParts}, trackByMap)
|
|
577
|
+
|
|
578
|
+
const instance = pageComp({/* placeholder for page props */})
|
|
579
|
+
${automationWrap}
|
|
580
|
+
<\/script>
|
|
581
|
+
</body>
|
|
582
|
+
</html>`;
|
|
583
|
+
}
|
|
584
|
+
const serverModuleCache = /* @__PURE__ */ new Map();
|
|
585
|
+
function invalidateServerElementCache(jayHtmlPath) {
|
|
586
|
+
if (serverModuleCache.delete(jayHtmlPath)) {
|
|
587
|
+
getLogger().info(`[SSR] Invalidated server element cache for ${jayHtmlPath}`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function clearServerElementCache() {
|
|
591
|
+
serverModuleCache.clear();
|
|
592
|
+
}
|
|
593
|
+
function getByPath(obj, dotPath) {
|
|
594
|
+
const segments = dotPath.split(".");
|
|
595
|
+
let current = obj;
|
|
596
|
+
for (const seg of segments) {
|
|
597
|
+
if (current == null || typeof current !== "object") return void 0;
|
|
598
|
+
current = current[seg];
|
|
599
|
+
}
|
|
600
|
+
return current;
|
|
601
|
+
}
|
|
602
|
+
function resolveParts(parts, viewState) {
|
|
603
|
+
return parts.map((p) => {
|
|
604
|
+
if (p.kind === "static") return p.value;
|
|
605
|
+
if (!viewState) return `{${p.value}}`;
|
|
606
|
+
const resolved = getByPath(viewState, p.value);
|
|
607
|
+
return resolved !== void 0 && resolved !== null ? String(resolved) : `{${p.value}}`;
|
|
608
|
+
}).join("");
|
|
609
|
+
}
|
|
610
|
+
function headMetaToHeadTags(headMeta, viewState) {
|
|
611
|
+
if (!headMeta) return [];
|
|
612
|
+
const tags = [];
|
|
613
|
+
if (headMeta.title) {
|
|
614
|
+
tags.push({ tag: "title", children: resolveParts(headMeta.title, viewState) });
|
|
615
|
+
}
|
|
616
|
+
for (const m of headMeta.meta) {
|
|
617
|
+
const attrs = {};
|
|
618
|
+
if (m.name) attrs.name = m.name;
|
|
619
|
+
if (m.property) attrs.property = m.property;
|
|
620
|
+
attrs.content = resolveParts(m.content, viewState);
|
|
621
|
+
tags.push({ tag: "meta", attrs });
|
|
622
|
+
}
|
|
623
|
+
for (const l of headMeta.links) {
|
|
624
|
+
if (l.rel === "stylesheet" || l.rel === "import") continue;
|
|
625
|
+
const attrs = { rel: l.rel };
|
|
626
|
+
attrs.href = resolveParts(l.href, viewState);
|
|
627
|
+
for (const [k, v] of Object.entries(l)) {
|
|
628
|
+
if (k !== "rel" && k !== "href" && typeof v === "string") attrs[k] = v;
|
|
629
|
+
}
|
|
630
|
+
tags.push({ tag: "link", attrs });
|
|
631
|
+
}
|
|
632
|
+
return tags;
|
|
633
|
+
}
|
|
634
|
+
async function generateSSRPageHtml(vite, jayHtmlContent, jayHtmlFilename, jayHtmlDir, viewState, jayHtmlImportPath, parts, carryForward, trackByMap = {}, clientInitData = {}, buildFolder, projectRoot, routeDir, tsConfigFilePath, projectInit, pluginInits = [], options = {}, sourceDir, headTags, scripts) {
|
|
635
|
+
const jayHtmlPath = path__default.join(jayHtmlDir, jayHtmlFilename);
|
|
636
|
+
let cached = serverModuleCache.get(jayHtmlPath);
|
|
637
|
+
if (!cached) {
|
|
638
|
+
cached = await compileAndLoadServerElement(
|
|
639
|
+
vite,
|
|
640
|
+
jayHtmlContent,
|
|
641
|
+
jayHtmlFilename,
|
|
642
|
+
jayHtmlDir,
|
|
643
|
+
buildFolder,
|
|
644
|
+
projectRoot,
|
|
645
|
+
routeDir,
|
|
646
|
+
tsConfigFilePath,
|
|
647
|
+
sourceDir
|
|
648
|
+
);
|
|
649
|
+
serverModuleCache.set(jayHtmlPath, cached);
|
|
650
|
+
}
|
|
651
|
+
const htmlChunks = [];
|
|
652
|
+
const asyncPromises = [];
|
|
653
|
+
const asyncOutcomes = [];
|
|
654
|
+
const ctx = {
|
|
655
|
+
write: (chunk) => {
|
|
656
|
+
htmlChunks.push(chunk);
|
|
657
|
+
},
|
|
658
|
+
onAsync: (promise, id, templates) => {
|
|
659
|
+
const asyncPromise = promise.then(
|
|
660
|
+
(val) => {
|
|
661
|
+
asyncOutcomes.push({ id, status: "resolved", value: val });
|
|
662
|
+
if (templates.resolved) {
|
|
663
|
+
return asyncSwapScript(id, templates.resolved(val));
|
|
664
|
+
}
|
|
665
|
+
return "";
|
|
666
|
+
},
|
|
667
|
+
(err) => {
|
|
668
|
+
asyncOutcomes.push({
|
|
669
|
+
id,
|
|
670
|
+
status: "rejected",
|
|
671
|
+
value: { message: err?.message ?? String(err) }
|
|
672
|
+
});
|
|
673
|
+
if (templates.rejected) {
|
|
674
|
+
return asyncSwapScript(id, templates.rejected(err));
|
|
675
|
+
}
|
|
676
|
+
return "";
|
|
677
|
+
}
|
|
678
|
+
);
|
|
679
|
+
asyncPromises.push(asyncPromise);
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
cached.renderToStream(viewState, ctx);
|
|
683
|
+
const asyncResults = await Promise.all(asyncPromises);
|
|
684
|
+
const ssrHtml = htmlChunks.join("");
|
|
685
|
+
const asyncScripts = asyncResults.filter((r) => r !== "").join("");
|
|
686
|
+
const hydrationScript = generateHydrationScript(
|
|
687
|
+
viewState,
|
|
688
|
+
carryForward,
|
|
689
|
+
parts,
|
|
690
|
+
jayHtmlImportPath,
|
|
691
|
+
trackByMap,
|
|
692
|
+
clientInitData,
|
|
693
|
+
projectInit,
|
|
694
|
+
pluginInits,
|
|
695
|
+
options,
|
|
696
|
+
asyncOutcomes
|
|
697
|
+
);
|
|
698
|
+
const cssLink = cached.cssHref ? ` <link rel="stylesheet" href="${cached.cssHref}" />` : "";
|
|
699
|
+
const tagSources = [];
|
|
700
|
+
if (headTags && headTags.length > 0) tagSources.push(headTags);
|
|
701
|
+
const headMetaTags = headMetaToHeadTags(cached.headMeta, viewState);
|
|
702
|
+
if (headMetaTags.length > 0) tagSources.push(headMetaTags);
|
|
703
|
+
const mergedTags = mergeHeadTags(tagSources);
|
|
704
|
+
const hasCustomTitle = mergedTags.some((t) => t.tag.toLowerCase() === "title");
|
|
705
|
+
const titleTag = hasCustomTitle ? "" : " <title>Vite + TS</title>\n";
|
|
706
|
+
const headTagsHtml = mergedTags.length > 0 ? serializeHeadTags(mergedTags) : "";
|
|
707
|
+
const headScriptsHtml = serializeScripts(scripts, "head");
|
|
708
|
+
const bodyScriptsHtml = serializeScripts(scripts, "body");
|
|
709
|
+
const headExtras = [cssLink, headTagsHtml, headScriptsHtml].filter((_) => _).join("\n");
|
|
710
|
+
return `<!doctype html>
|
|
711
|
+
<html lang="en">
|
|
712
|
+
<head>
|
|
713
|
+
<meta charset="UTF-8" />
|
|
714
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
715
|
+
${titleTag}${headExtras ? headExtras + "\n" : ""} </head>
|
|
716
|
+
<body>
|
|
717
|
+
<div id="target">${ssrHtml}</div>${asyncScripts}
|
|
718
|
+
${hydrationScript}${bodyScriptsHtml ? "\n " + bodyScriptsHtml : ""}
|
|
719
|
+
</body>
|
|
720
|
+
</html>`;
|
|
721
|
+
}
|
|
722
|
+
async function generateFrozenPageHtml(vite, jayHtmlContent, jayHtmlFilename, jayHtmlDir, viewState, buildFolder, projectRoot, routeDir, tsConfigFilePath, sourceDir, format = "page", freezeName) {
|
|
723
|
+
const jayHtmlPath = path__default.join(jayHtmlDir, jayHtmlFilename);
|
|
724
|
+
let cached = serverModuleCache.get(jayHtmlPath);
|
|
725
|
+
if (!cached) {
|
|
726
|
+
cached = await compileAndLoadServerElement(
|
|
727
|
+
vite,
|
|
728
|
+
jayHtmlContent,
|
|
729
|
+
jayHtmlFilename,
|
|
730
|
+
jayHtmlDir,
|
|
731
|
+
buildFolder,
|
|
732
|
+
projectRoot,
|
|
733
|
+
routeDir,
|
|
734
|
+
tsConfigFilePath,
|
|
735
|
+
sourceDir
|
|
736
|
+
);
|
|
737
|
+
serverModuleCache.set(jayHtmlPath, cached);
|
|
738
|
+
}
|
|
739
|
+
const htmlChunks = [];
|
|
740
|
+
const ctx = {
|
|
741
|
+
write: (chunk) => {
|
|
742
|
+
htmlChunks.push(chunk);
|
|
743
|
+
},
|
|
744
|
+
onAsync: () => {
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
cached.renderToStream(viewState, ctx);
|
|
748
|
+
const ssrHtml = htmlChunks.join("");
|
|
749
|
+
if (format === "fragment") {
|
|
750
|
+
let inlineCss = "";
|
|
751
|
+
if (cached.cssHref) {
|
|
752
|
+
try {
|
|
753
|
+
const cssPath = cached.cssHref.replace(/^\/@fs/, "").replace(/\?.*$/, "");
|
|
754
|
+
const cssContent = await fs$1.readFile(cssPath, "utf-8");
|
|
755
|
+
inlineCss = `<style>${cssContent}</style>`;
|
|
756
|
+
} catch {
|
|
757
|
+
inlineCss = `<link rel="stylesheet" href="${cached.cssHref}" />`;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return `${inlineCss}
|
|
761
|
+
${ssrHtml}`;
|
|
762
|
+
}
|
|
763
|
+
const frozenHeadTags = headMetaToHeadTags(cached.headMeta, viewState);
|
|
764
|
+
const frozenHeadTagsHtml = frozenHeadTags.length > 0 ? serializeHeadTags(frozenHeadTags) : "";
|
|
765
|
+
const cssLink = cached.cssHref ? ` <link rel="stylesheet" href="${cached.cssHref}" />` : "";
|
|
766
|
+
const headExtras = [cssLink, frozenHeadTagsHtml].filter((_) => _).join("\n");
|
|
767
|
+
const label = freezeName ? ` — ${freezeName}` : "";
|
|
768
|
+
return `<!doctype html>
|
|
769
|
+
<html lang="en">
|
|
770
|
+
<head>
|
|
771
|
+
<meta charset="UTF-8" />
|
|
772
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
773
|
+
<title>Frozen${label}</title>
|
|
774
|
+
${headExtras ? headExtras + "\n" : ""} <style>
|
|
775
|
+
body::before {
|
|
776
|
+
content: 'FROZEN${label ? `: ${freezeName}` : ""}';
|
|
777
|
+
position: fixed;
|
|
778
|
+
top: 0;
|
|
779
|
+
right: 0;
|
|
780
|
+
background: #1a1a2e;
|
|
781
|
+
color: #e0e0ff;
|
|
782
|
+
padding: 2px 10px;
|
|
783
|
+
font: 11px/1.6 system-ui;
|
|
784
|
+
z-index: 99999;
|
|
785
|
+
border-bottom-left-radius: 4px;
|
|
786
|
+
opacity: 0.8;
|
|
787
|
+
}
|
|
788
|
+
</style>
|
|
789
|
+
</head>
|
|
790
|
+
<body>
|
|
791
|
+
<div id="target">${ssrHtml}</div>
|
|
792
|
+
</body>
|
|
793
|
+
</html>`;
|
|
794
|
+
}
|
|
795
|
+
function serializeScripts(scripts, position) {
|
|
796
|
+
if (!scripts) return "";
|
|
797
|
+
const filtered = scripts.filter((s2) => s2.position === position);
|
|
798
|
+
if (filtered.length === 0) return "";
|
|
799
|
+
return filtered.map((s2) => {
|
|
800
|
+
const attrs = Object.entries(s2.attributes).map(([k, v]) => v === "" ? k : `${k}="${v}"`).join(" ");
|
|
801
|
+
const attrStr = attrs ? " " + attrs : "";
|
|
802
|
+
if (s2.src) {
|
|
803
|
+
return ` <script src="${s2.src}"${attrStr}><\/script>`;
|
|
804
|
+
}
|
|
805
|
+
return ` <script${attrStr}>${s2.inline}<\/script>`;
|
|
806
|
+
}).join("\n");
|
|
807
|
+
}
|
|
808
|
+
function rebaseRelativeImports(code, fromDir, toDir) {
|
|
809
|
+
return code.replace(/from "(\.\.\/[^"]+)"/g, (_match, relPath) => {
|
|
810
|
+
const absolutePath = path__default.resolve(fromDir, relPath);
|
|
811
|
+
let newRelPath = path__default.relative(toDir, absolutePath);
|
|
812
|
+
if (!newRelPath.startsWith(".")) {
|
|
813
|
+
newRelPath = "./" + newRelPath;
|
|
814
|
+
}
|
|
815
|
+
return `from "${newRelPath}"`;
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
async function compileAndLoadServerElement(vite, jayHtmlContent, jayHtmlFilename, jayHtmlDir, buildFolder, projectRoot, routeDir, tsConfigFilePath, sourceDir) {
|
|
819
|
+
const jayFile = await parseJayFile(
|
|
820
|
+
jayHtmlContent,
|
|
821
|
+
jayHtmlFilename,
|
|
822
|
+
jayHtmlDir,
|
|
823
|
+
{ relativePath: tsConfigFilePath },
|
|
824
|
+
JAY_IMPORT_RESOLVER,
|
|
825
|
+
projectRoot,
|
|
826
|
+
sourceDir
|
|
827
|
+
);
|
|
828
|
+
const parsedJayFile = checkValidationErrors(jayFile);
|
|
829
|
+
const pageName = jayHtmlFilename.replace(".jay-html", "");
|
|
830
|
+
const debugCoordinatePreprocessPath = path__default.join(
|
|
831
|
+
buildFolder,
|
|
832
|
+
"debug",
|
|
833
|
+
routeDir,
|
|
834
|
+
`${pageName}.coordinate-preprocess.jay-html`
|
|
835
|
+
);
|
|
836
|
+
const serverElementOptions = {
|
|
837
|
+
debugCoordinatePreprocessPath
|
|
838
|
+
};
|
|
839
|
+
const serverElementCode = checkValidationErrors(
|
|
840
|
+
generateServerElementFile(parsedJayFile, serverElementOptions)
|
|
841
|
+
);
|
|
842
|
+
const serverElementDir = path__default.join(buildFolder, "pre-rendered", routeDir);
|
|
843
|
+
await fs$1.mkdir(serverElementDir, { recursive: true });
|
|
844
|
+
const adjustedCode = rebaseRelativeImports(serverElementCode, jayHtmlDir, serverElementDir);
|
|
845
|
+
const serverElementFilename = jayHtmlFilename.replace(".jay-html", ".server-element.ts");
|
|
846
|
+
const serverElementPath = path__default.join(serverElementDir, serverElementFilename);
|
|
847
|
+
await fs$1.writeFile(serverElementPath, adjustedCode, "utf-8");
|
|
848
|
+
const existingModule = vite.moduleGraph.getModuleById(serverElementPath);
|
|
849
|
+
if (existingModule) {
|
|
850
|
+
vite.moduleGraph.invalidateModule(existingModule);
|
|
851
|
+
}
|
|
852
|
+
const jayHtmlPath = path__default.join(serverElementDir, jayHtmlFilename);
|
|
853
|
+
invalidateJayHtmlModules(vite, jayHtmlPath);
|
|
854
|
+
const serverModule = await vite.ssrLoadModule(serverElementPath);
|
|
855
|
+
let cssHref;
|
|
856
|
+
if (parsedJayFile.css) {
|
|
857
|
+
const cssFilename = jayHtmlFilename.replace(".jay-html", ".css");
|
|
858
|
+
const cssPath = path__default.join(serverElementDir, cssFilename);
|
|
859
|
+
await fs$1.writeFile(cssPath, parsedJayFile.css, "utf-8");
|
|
860
|
+
const cssModules = vite.moduleGraph.getModulesByFile(cssPath);
|
|
861
|
+
if (cssModules) {
|
|
862
|
+
for (const mod of cssModules) {
|
|
863
|
+
vite.moduleGraph.invalidateModule(mod);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
const hash = crypto.createHash("md5").update(parsedJayFile.css).digest("hex").slice(0, 8);
|
|
867
|
+
cssHref = "/@fs" + cssPath + "?v=" + hash + "&direct";
|
|
868
|
+
}
|
|
869
|
+
return {
|
|
870
|
+
renderToStream: serverModule.renderToStream,
|
|
871
|
+
headLinks: parsedJayFile.headLinks,
|
|
872
|
+
headMeta: parsedJayFile.headMeta,
|
|
873
|
+
cssHref
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
function invalidateJayHtmlModules(vite, jayHtmlPath) {
|
|
877
|
+
let count = 0;
|
|
878
|
+
const byFile = vite.moduleGraph.getModulesByFile(jayHtmlPath);
|
|
879
|
+
if (byFile) {
|
|
880
|
+
for (const mod of byFile) {
|
|
881
|
+
vite.moduleGraph.invalidateModule(mod);
|
|
882
|
+
count++;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
const knownIds = [jayHtmlPath + ".ts", jayHtmlPath + JAY_QUERY_HYDRATE + ".ts"];
|
|
886
|
+
for (const id of knownIds) {
|
|
887
|
+
const mod = vite.moduleGraph.getModuleById(id);
|
|
888
|
+
if (mod) {
|
|
889
|
+
vite.moduleGraph.invalidateModule(mod);
|
|
890
|
+
count++;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
const idMap = vite.moduleGraph.idToModuleMap;
|
|
894
|
+
for (const [id, mod] of idMap) {
|
|
895
|
+
if (id.includes(jayHtmlPath)) {
|
|
896
|
+
vite.moduleGraph.invalidateModule(mod);
|
|
897
|
+
count++;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
if (count > 0) {
|
|
901
|
+
getLogger().info(`[SSR] Invalidated ${count} Vite module(s) for ${jayHtmlPath}`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
function generateHydrationScript(defaultViewState, fastCarryForward, parts, jayHtmlPath, trackByMap = {}, clientInitData = {}, projectInit, pluginInits = [], options = {}, asyncOutcomes = []) {
|
|
905
|
+
const {
|
|
906
|
+
partImports,
|
|
907
|
+
compositeParts,
|
|
908
|
+
pluginClientInitImports,
|
|
909
|
+
projectInitImport,
|
|
910
|
+
clientInitExecution,
|
|
911
|
+
automationImport,
|
|
912
|
+
slowViewStateDecl
|
|
913
|
+
} = buildScriptFragments(parts, clientInitData, projectInit, pluginInits, options);
|
|
914
|
+
const automationWrap = buildAutomationWrap(options, "hydrate");
|
|
915
|
+
const hydrateImportPath = `${jayHtmlPath}${JAY_QUERY_HYDRATE}`;
|
|
916
|
+
return `<script type="module">
|
|
917
|
+
import {hydrateCompositeJayComponent} from "@jay-framework/stack-client-runtime";
|
|
918
|
+
${automationImport}
|
|
919
|
+
${pluginClientInitImports}
|
|
920
|
+
${projectInitImport}
|
|
921
|
+
import { hydrate } from '${hydrateImportPath}';
|
|
922
|
+
${partImports}${slowViewStateDecl}
|
|
923
|
+
const viewState = ${JSON.stringify(defaultViewState)};
|
|
924
|
+
${generatePromiseReconstruction(asyncOutcomes)} const fastCarryForward = ${JSON.stringify(fastCarryForward)};
|
|
925
|
+
const trackByMap = ${JSON.stringify(trackByMap)};
|
|
926
|
+
|
|
927
|
+
const target = document.getElementById('target');
|
|
928
|
+
const rootElement = target.firstElementChild;
|
|
929
|
+
const pageComp = hydrateCompositeJayComponent(hydrate, viewState, fastCarryForward, ${compositeParts}, trackByMap, rootElement);
|
|
930
|
+
${clientInitExecution}
|
|
931
|
+
const instance = pageComp({/* placeholder for page props */});
|
|
932
|
+
${automationWrap}
|
|
933
|
+
<\/script>`;
|
|
934
|
+
}
|
|
935
|
+
function parseActionMetadata(yamlContent, fileName) {
|
|
936
|
+
try {
|
|
937
|
+
const parsed = parseAction(yamlContent, fileName);
|
|
938
|
+
if (parsed.validations.length > 0) {
|
|
939
|
+
getLogger().warn(
|
|
940
|
+
`[ActionMetadata] ${fileName}: validation errors: ${parsed.validations.join(", ")}`
|
|
941
|
+
);
|
|
942
|
+
return null;
|
|
943
|
+
}
|
|
944
|
+
if (!parsed.val) {
|
|
945
|
+
getLogger().warn(`[ActionMetadata] ${fileName}: parsing returned no result`);
|
|
946
|
+
return null;
|
|
947
|
+
}
|
|
948
|
+
const action = parsed.val;
|
|
949
|
+
const inputJsonSchema = jayTypeToJsonSchema(action.inputType);
|
|
950
|
+
let inputSchema;
|
|
951
|
+
if (inputJsonSchema && inputJsonSchema.type === "object") {
|
|
952
|
+
inputSchema = {
|
|
953
|
+
type: "object",
|
|
954
|
+
properties: inputJsonSchema.properties || {},
|
|
955
|
+
...inputJsonSchema.required && inputJsonSchema.required.length > 0 && { required: inputJsonSchema.required }
|
|
956
|
+
};
|
|
957
|
+
} else {
|
|
958
|
+
inputSchema = { type: "object", properties: {} };
|
|
959
|
+
}
|
|
960
|
+
const metadata = {
|
|
961
|
+
name: action.name,
|
|
962
|
+
description: action.description,
|
|
963
|
+
inputSchema
|
|
964
|
+
};
|
|
965
|
+
if (action.outputType) {
|
|
966
|
+
const outputJsonSchema = jayTypeToJsonSchema(action.outputType);
|
|
967
|
+
if (outputJsonSchema) {
|
|
968
|
+
metadata.outputSchema = outputJsonSchema;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
return metadata;
|
|
972
|
+
} catch (error) {
|
|
973
|
+
getLogger().error(
|
|
974
|
+
`[ActionMetadata] Failed to parse ${fileName}: ${error instanceof Error ? error.message : error}`
|
|
975
|
+
);
|
|
976
|
+
return null;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
function loadActionMetadata(filePath) {
|
|
980
|
+
if (!fs.existsSync(filePath)) {
|
|
981
|
+
getLogger().warn(`[ActionMetadata] File not found: ${filePath}`);
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
984
|
+
const yamlContent = fs.readFileSync(filePath, "utf-8");
|
|
985
|
+
const fileName = path.basename(filePath);
|
|
986
|
+
return parseActionMetadata(yamlContent, fileName);
|
|
987
|
+
}
|
|
988
|
+
function resolveActionMetadataPath(actionPath, pluginDir) {
|
|
989
|
+
return path.resolve(pluginDir, actionPath);
|
|
990
|
+
}
|
|
991
|
+
const require$2 = createRequire$1(import.meta.url);
|
|
992
|
+
async function discoverAndRegisterActions(options) {
|
|
993
|
+
const {
|
|
994
|
+
projectRoot,
|
|
995
|
+
actionsDir = "src/actions",
|
|
996
|
+
registry = actionRegistry,
|
|
997
|
+
verbose = false,
|
|
998
|
+
viteServer
|
|
999
|
+
} = options;
|
|
1000
|
+
const result = {
|
|
1001
|
+
actionCount: 0,
|
|
1002
|
+
actionNames: [],
|
|
1003
|
+
scannedFiles: []
|
|
1004
|
+
};
|
|
1005
|
+
const actionsPath = path.resolve(projectRoot, actionsDir);
|
|
1006
|
+
if (!fs.existsSync(actionsPath)) {
|
|
1007
|
+
if (verbose) {
|
|
1008
|
+
getLogger().info(`[Actions] No actions directory found at ${actionsPath}`);
|
|
1009
|
+
}
|
|
1010
|
+
return result;
|
|
1011
|
+
}
|
|
1012
|
+
const actionFiles = await findActionFiles(actionsPath);
|
|
1013
|
+
if (verbose) {
|
|
1014
|
+
getLogger().info(`[Actions] Found ${actionFiles.length} action file(s)`);
|
|
1015
|
+
}
|
|
1016
|
+
for (const filePath of actionFiles) {
|
|
1017
|
+
result.scannedFiles.push(filePath);
|
|
1018
|
+
try {
|
|
1019
|
+
let module;
|
|
1020
|
+
if (viteServer) {
|
|
1021
|
+
module = await viteServer.ssrLoadModule(filePath);
|
|
1022
|
+
} else {
|
|
1023
|
+
module = await import(filePath);
|
|
1024
|
+
}
|
|
1025
|
+
for (const [exportName, exportValue] of Object.entries(module)) {
|
|
1026
|
+
if (isJayAction(exportValue)) {
|
|
1027
|
+
registry.register(exportValue);
|
|
1028
|
+
result.actionNames.push(exportValue.actionName);
|
|
1029
|
+
result.actionCount++;
|
|
1030
|
+
if (verbose) {
|
|
1031
|
+
getLogger().info(
|
|
1032
|
+
`[Actions] Registered: ${exportValue.actionName}`
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
} else if (isJayStreamAction(exportValue)) {
|
|
1036
|
+
registry.registerStream(exportValue);
|
|
1037
|
+
result.actionNames.push(exportValue.actionName);
|
|
1038
|
+
result.actionCount++;
|
|
1039
|
+
if (verbose) {
|
|
1040
|
+
getLogger().info(
|
|
1041
|
+
`[Actions] Registered stream: ${exportValue.actionName}`
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
getLogger().error(`[Actions] Failed to import ${filePath}: ${error}`);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return result;
|
|
1051
|
+
}
|
|
1052
|
+
async function findActionFiles(dir) {
|
|
1053
|
+
const files = [];
|
|
1054
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
1055
|
+
for (const entry of entries) {
|
|
1056
|
+
const fullPath = path.join(dir, entry.name);
|
|
1057
|
+
if (entry.isDirectory()) {
|
|
1058
|
+
const subFiles = await findActionFiles(fullPath);
|
|
1059
|
+
files.push(...subFiles);
|
|
1060
|
+
} else if (entry.isFile() && entry.name.endsWith(".actions.ts")) {
|
|
1061
|
+
files.push(fullPath);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return files;
|
|
1065
|
+
}
|
|
1066
|
+
async function discoverAllPluginActions(options) {
|
|
1067
|
+
const { projectRoot, registry = actionRegistry, verbose = false, viteServer } = options;
|
|
1068
|
+
const allActions = [];
|
|
1069
|
+
const localPluginsPath = path.join(projectRoot, "src/plugins");
|
|
1070
|
+
if (fs.existsSync(localPluginsPath)) {
|
|
1071
|
+
const pluginDirs = await fs.promises.readdir(localPluginsPath, { withFileTypes: true });
|
|
1072
|
+
for (const entry of pluginDirs) {
|
|
1073
|
+
if (entry.isDirectory()) {
|
|
1074
|
+
const pluginPath = path.join(localPluginsPath, entry.name);
|
|
1075
|
+
const actions = await discoverPluginActions(
|
|
1076
|
+
pluginPath,
|
|
1077
|
+
projectRoot,
|
|
1078
|
+
registry,
|
|
1079
|
+
verbose,
|
|
1080
|
+
viteServer
|
|
1081
|
+
);
|
|
1082
|
+
allActions.push(...actions);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
const npmActions = await discoverNpmPluginActions(projectRoot, registry, verbose, viteServer);
|
|
1087
|
+
allActions.push(...npmActions);
|
|
1088
|
+
return allActions;
|
|
1089
|
+
}
|
|
1090
|
+
async function discoverNpmPluginActions(projectRoot, registry, verbose, viteServer) {
|
|
1091
|
+
const allActions = [];
|
|
1092
|
+
const packageJsonPath = path.join(projectRoot, "package.json");
|
|
1093
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
1094
|
+
return allActions;
|
|
1095
|
+
}
|
|
1096
|
+
try {
|
|
1097
|
+
const packageJson = JSON.parse(await fs.promises.readFile(packageJsonPath, "utf-8"));
|
|
1098
|
+
const dependencies = {
|
|
1099
|
+
...packageJson.dependencies,
|
|
1100
|
+
...packageJson.devDependencies
|
|
1101
|
+
};
|
|
1102
|
+
for (const packageName of Object.keys(dependencies)) {
|
|
1103
|
+
try {
|
|
1104
|
+
const pluginYamlPath = tryResolvePluginYaml(packageName, projectRoot);
|
|
1105
|
+
if (!pluginYamlPath) {
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
const pluginDir = path.dirname(pluginYamlPath);
|
|
1109
|
+
const pluginConfig = loadPluginManifest(pluginDir);
|
|
1110
|
+
if (!pluginConfig || !pluginConfig.actions || !Array.isArray(pluginConfig.actions)) {
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
if (verbose) {
|
|
1114
|
+
getLogger().info(
|
|
1115
|
+
`[Actions] NPM plugin "${packageName}" declares actions: ${JSON.stringify(pluginConfig.actions)}`
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
const actions = await registerNpmPluginActions(
|
|
1119
|
+
packageName,
|
|
1120
|
+
pluginConfig,
|
|
1121
|
+
pluginDir,
|
|
1122
|
+
registry,
|
|
1123
|
+
verbose,
|
|
1124
|
+
viteServer
|
|
1125
|
+
);
|
|
1126
|
+
allActions.push(...actions);
|
|
1127
|
+
} catch {
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
getLogger().error(`[Actions] Failed to read project package.json: ${error}`);
|
|
1133
|
+
}
|
|
1134
|
+
return allActions;
|
|
1135
|
+
}
|
|
1136
|
+
function tryResolvePluginYaml(packageName, projectRoot) {
|
|
1137
|
+
try {
|
|
1138
|
+
return require$2.resolve(`${packageName}/plugin.yaml`, {
|
|
1139
|
+
paths: [projectRoot]
|
|
1140
|
+
});
|
|
1141
|
+
} catch {
|
|
1142
|
+
return null;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
function resolveNpmActionMetadataPath(actionPath, packageName, pluginDir) {
|
|
1146
|
+
if (!actionPath.startsWith(".")) {
|
|
1147
|
+
try {
|
|
1148
|
+
return require$2.resolve(`${packageName}/${actionPath}`, {
|
|
1149
|
+
paths: [pluginDir]
|
|
1150
|
+
});
|
|
1151
|
+
} catch {
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
const resolved = resolveActionMetadataPath(actionPath, pluginDir);
|
|
1155
|
+
if (fs.existsSync(resolved)) {
|
|
1156
|
+
return resolved;
|
|
1157
|
+
}
|
|
1158
|
+
getLogger().warn(
|
|
1159
|
+
`[Actions] Could not resolve .jay-action file "${actionPath}" for package "${packageName}"`
|
|
1160
|
+
);
|
|
1161
|
+
return null;
|
|
1162
|
+
}
|
|
1163
|
+
async function registerNpmPluginActions(packageName, pluginConfig, pluginDir, registry, verbose, viteServer) {
|
|
1164
|
+
const registeredActions = [];
|
|
1165
|
+
try {
|
|
1166
|
+
let pluginModule;
|
|
1167
|
+
if (viteServer) {
|
|
1168
|
+
pluginModule = await viteServer.ssrLoadModule(packageName);
|
|
1169
|
+
} else {
|
|
1170
|
+
pluginModule = await import(packageName);
|
|
1171
|
+
}
|
|
1172
|
+
for (const entry of pluginConfig.actions) {
|
|
1173
|
+
const { name: actionName, action: actionPath } = normalizeActionEntry(entry);
|
|
1174
|
+
const actionExport = pluginModule[actionName];
|
|
1175
|
+
if (actionExport && isJayAction(actionExport)) {
|
|
1176
|
+
registry.register(actionExport);
|
|
1177
|
+
const registeredName = actionExport.actionName;
|
|
1178
|
+
registeredActions.push(registeredName);
|
|
1179
|
+
if (actionPath) {
|
|
1180
|
+
const metadataFilePath = resolveNpmActionMetadataPath(
|
|
1181
|
+
actionPath,
|
|
1182
|
+
packageName,
|
|
1183
|
+
pluginDir
|
|
1184
|
+
);
|
|
1185
|
+
if (metadataFilePath) {
|
|
1186
|
+
const metadata = loadActionMetadata(metadataFilePath);
|
|
1187
|
+
if (metadata) {
|
|
1188
|
+
registry.setMetadata(registeredName, metadata);
|
|
1189
|
+
if (verbose) {
|
|
1190
|
+
getLogger().info(
|
|
1191
|
+
`[Actions] Loaded metadata for "${registeredName}" from ${actionPath}`
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
if (verbose) {
|
|
1198
|
+
getLogger().info(`[Actions] Registered NPM plugin action: ${registeredName}`);
|
|
1199
|
+
}
|
|
1200
|
+
} else if (actionExport && isJayStreamAction(actionExport)) {
|
|
1201
|
+
registry.registerStream(actionExport);
|
|
1202
|
+
const registeredName = actionExport.actionName;
|
|
1203
|
+
registeredActions.push(registeredName);
|
|
1204
|
+
if (verbose) {
|
|
1205
|
+
getLogger().info(`[Actions] Registered NPM plugin stream: ${registeredName}`);
|
|
1206
|
+
}
|
|
1207
|
+
} else {
|
|
1208
|
+
getLogger().warn(
|
|
1209
|
+
`[Actions] NPM plugin "${packageName}" declares action "${actionName}" but it's not exported or not a JayAction`
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
} catch (importError) {
|
|
1214
|
+
getLogger().error(`[Actions] Failed to import NPM plugin "${packageName}": ${importError}`);
|
|
1215
|
+
}
|
|
1216
|
+
return registeredActions;
|
|
1217
|
+
}
|
|
1218
|
+
async function discoverPluginActions(pluginPath, projectRoot, registry = actionRegistry, verbose = false, viteServer) {
|
|
1219
|
+
const pluginConfig = loadPluginManifest(pluginPath);
|
|
1220
|
+
if (!pluginConfig) {
|
|
1221
|
+
return [];
|
|
1222
|
+
}
|
|
1223
|
+
if (!pluginConfig.actions || !Array.isArray(pluginConfig.actions)) {
|
|
1224
|
+
return [];
|
|
1225
|
+
}
|
|
1226
|
+
const registeredActions = [];
|
|
1227
|
+
const pluginName = pluginConfig.name || path.basename(pluginPath);
|
|
1228
|
+
if (verbose) {
|
|
1229
|
+
getLogger().info(
|
|
1230
|
+
`[Actions] Plugin "${pluginName}" declares actions: ${JSON.stringify(pluginConfig.actions)}`
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
let modulePath = pluginConfig.module ? path.join(pluginPath, pluginConfig.module) : path.join(pluginPath, "index.ts");
|
|
1234
|
+
if (!fs.existsSync(modulePath)) {
|
|
1235
|
+
const tsPath = modulePath + ".ts";
|
|
1236
|
+
const jsPath = modulePath + ".js";
|
|
1237
|
+
if (fs.existsSync(tsPath)) {
|
|
1238
|
+
modulePath = tsPath;
|
|
1239
|
+
} else if (fs.existsSync(jsPath)) {
|
|
1240
|
+
modulePath = jsPath;
|
|
1241
|
+
} else {
|
|
1242
|
+
getLogger().warn(`[Actions] Plugin "${pluginName}" module not found at ${modulePath}`);
|
|
1243
|
+
return [];
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
try {
|
|
1247
|
+
let pluginModule;
|
|
1248
|
+
if (viteServer) {
|
|
1249
|
+
pluginModule = await viteServer.ssrLoadModule(modulePath);
|
|
1250
|
+
} else {
|
|
1251
|
+
pluginModule = await import(modulePath);
|
|
1252
|
+
}
|
|
1253
|
+
for (const entry of pluginConfig.actions) {
|
|
1254
|
+
const { name: actionName, action: actionPath } = normalizeActionEntry(entry);
|
|
1255
|
+
const actionExport = pluginModule[actionName];
|
|
1256
|
+
if (actionExport && isJayAction(actionExport)) {
|
|
1257
|
+
registry.register(actionExport);
|
|
1258
|
+
const registeredName = actionExport.actionName;
|
|
1259
|
+
registeredActions.push(registeredName);
|
|
1260
|
+
if (actionPath) {
|
|
1261
|
+
const metadataFilePath = resolveActionMetadataPath(actionPath, pluginPath);
|
|
1262
|
+
const metadata = loadActionMetadata(metadataFilePath);
|
|
1263
|
+
if (metadata) {
|
|
1264
|
+
registry.setMetadata(registeredName, metadata);
|
|
1265
|
+
if (verbose) {
|
|
1266
|
+
getLogger().info(
|
|
1267
|
+
`[Actions] Loaded metadata for "${registeredName}" from ${actionPath}`
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
if (verbose) {
|
|
1273
|
+
getLogger().info(`[Actions] Registered plugin action: ${registeredName}`);
|
|
1274
|
+
}
|
|
1275
|
+
} else if (actionExport && isJayStreamAction(actionExport)) {
|
|
1276
|
+
registry.registerStream(actionExport);
|
|
1277
|
+
const registeredName = actionExport.actionName;
|
|
1278
|
+
registeredActions.push(registeredName);
|
|
1279
|
+
if (verbose) {
|
|
1280
|
+
getLogger().info(`[Actions] Registered plugin stream: ${registeredName}`);
|
|
1281
|
+
}
|
|
1282
|
+
} else {
|
|
1283
|
+
getLogger().warn(
|
|
1284
|
+
`[Actions] Plugin "${pluginName}" declares action "${actionName}" but it's not exported or not a JayAction`
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
} catch (importError) {
|
|
1289
|
+
getLogger().error(
|
|
1290
|
+
`[Actions] Failed to import plugin module at ${modulePath}: ${importError}`
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
return registeredActions;
|
|
1294
|
+
}
|
|
1295
|
+
const require$1 = createRequire(import.meta.url);
|
|
1296
|
+
async function executeDynamicGenerator(plugin, config, projectRoot, services, verbose, viteServer) {
|
|
1297
|
+
const { pluginPath, name: pluginName, isLocal, packageName } = plugin;
|
|
1298
|
+
if (!config.generator) {
|
|
1299
|
+
throw new Error(
|
|
1300
|
+
`Plugin "${pluginName}" has dynamic_contracts entry but no generator specified`
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
const isFilePath = config.generator.startsWith("./") || config.generator.startsWith("/") || config.generator.includes(".ts") || config.generator.includes(".js");
|
|
1304
|
+
let generator;
|
|
1305
|
+
if (isFilePath) {
|
|
1306
|
+
let generatorPath;
|
|
1307
|
+
if (!isLocal) {
|
|
1308
|
+
try {
|
|
1309
|
+
generatorPath = require$1.resolve(`${packageName}/${config.generator}`, {
|
|
1310
|
+
paths: [projectRoot]
|
|
1311
|
+
});
|
|
1312
|
+
} catch {
|
|
1313
|
+
generatorPath = path.join(pluginPath, config.generator);
|
|
1314
|
+
}
|
|
1315
|
+
} else {
|
|
1316
|
+
generatorPath = path.join(pluginPath, config.generator);
|
|
1317
|
+
}
|
|
1318
|
+
if (!fs.existsSync(generatorPath)) {
|
|
1319
|
+
const withTs = generatorPath + ".ts";
|
|
1320
|
+
const withJs = generatorPath + ".js";
|
|
1321
|
+
if (fs.existsSync(withTs)) {
|
|
1322
|
+
generatorPath = withTs;
|
|
1323
|
+
} else if (fs.existsSync(withJs)) {
|
|
1324
|
+
generatorPath = withJs;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
if (!fs.existsSync(generatorPath)) {
|
|
1328
|
+
throw new Error(
|
|
1329
|
+
`Generator file not found for plugin "${pluginName}": ${config.generator}`
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
if (verbose) {
|
|
1333
|
+
getLogger().info(` Loading generator from file: ${generatorPath}`);
|
|
1334
|
+
}
|
|
1335
|
+
let generatorModule;
|
|
1336
|
+
if (viteServer) {
|
|
1337
|
+
generatorModule = await viteServer.ssrLoadModule(generatorPath);
|
|
1338
|
+
} else {
|
|
1339
|
+
generatorModule = await import(generatorPath);
|
|
1340
|
+
}
|
|
1341
|
+
generator = generatorModule.generator || generatorModule.default;
|
|
1342
|
+
} else {
|
|
1343
|
+
if (verbose) {
|
|
1344
|
+
getLogger().info(
|
|
1345
|
+
` Loading generator export: ${config.generator} from ${packageName}`
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
let pluginModule;
|
|
1349
|
+
if (viteServer) {
|
|
1350
|
+
pluginModule = await viteServer.ssrLoadModule(packageName);
|
|
1351
|
+
} else {
|
|
1352
|
+
pluginModule = await import(packageName);
|
|
1353
|
+
}
|
|
1354
|
+
generator = pluginModule[config.generator];
|
|
1355
|
+
if (!generator) {
|
|
1356
|
+
throw new Error(
|
|
1357
|
+
`Generator "${config.generator}" not exported from plugin "${pluginName}". Ensure it's exported from the package's index.ts`
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
if (!generator || typeof generator.generate !== "function") {
|
|
1362
|
+
throw new Error(
|
|
1363
|
+
`Generator "${config.generator}" for plugin "${pluginName}" must have a 'generate' function. Use makeContractGenerator() to create valid generators.`
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1366
|
+
const resolvedServices = [];
|
|
1367
|
+
for (const marker of generator.services) {
|
|
1368
|
+
const service = services.get(marker);
|
|
1369
|
+
if (!service) {
|
|
1370
|
+
const markerName = marker.description ?? "unknown";
|
|
1371
|
+
throw new Error(
|
|
1372
|
+
`Service "${markerName}" required by ${pluginName} generator not found. Ensure it's registered in init.ts`
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
resolvedServices.push(service);
|
|
1376
|
+
}
|
|
1377
|
+
if (verbose) {
|
|
1378
|
+
getLogger().info(` Executing generator...`);
|
|
1379
|
+
}
|
|
1380
|
+
return await generator.generate(...resolvedServices);
|
|
1381
|
+
}
|
|
1382
|
+
function resolveStaticContractPath(plugin, contractSpec, projectRoot) {
|
|
1383
|
+
const { pluginPath, isLocal, packageName } = plugin;
|
|
1384
|
+
if (!isLocal) {
|
|
1385
|
+
try {
|
|
1386
|
+
return require$1.resolve(`${packageName}/${contractSpec}`, {
|
|
1387
|
+
paths: [projectRoot]
|
|
1388
|
+
});
|
|
1389
|
+
} catch {
|
|
1390
|
+
const possiblePaths = [
|
|
1391
|
+
path.join(pluginPath, "dist", contractSpec),
|
|
1392
|
+
path.join(pluginPath, "lib", contractSpec),
|
|
1393
|
+
path.join(pluginPath, contractSpec)
|
|
1394
|
+
];
|
|
1395
|
+
const found = possiblePaths.find((p) => fs.existsSync(p));
|
|
1396
|
+
return found || possiblePaths[0];
|
|
1397
|
+
}
|
|
1398
|
+
} else {
|
|
1399
|
+
return path.join(pluginPath, contractSpec);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
function resolveActionFilePath(actionPath, packageName, pluginPath, isLocal, projectRoot) {
|
|
1403
|
+
if (!isLocal && !actionPath.startsWith(".")) {
|
|
1404
|
+
try {
|
|
1405
|
+
return require$1.resolve(`${packageName}/${actionPath}`, {
|
|
1406
|
+
paths: [projectRoot]
|
|
1407
|
+
});
|
|
1408
|
+
} catch {
|
|
1409
|
+
const possiblePaths = [
|
|
1410
|
+
path.join(pluginPath, "dist", actionPath),
|
|
1411
|
+
path.join(pluginPath, "lib", actionPath),
|
|
1412
|
+
path.join(pluginPath, actionPath)
|
|
1413
|
+
];
|
|
1414
|
+
const found = possiblePaths.find((p) => fs.existsSync(p));
|
|
1415
|
+
return found || null;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
const resolved = resolveActionMetadataPath(actionPath, pluginPath);
|
|
1419
|
+
return fs.existsSync(resolved) ? resolved : null;
|
|
1420
|
+
}
|
|
1421
|
+
function toKebabCase(str) {
|
|
1422
|
+
return str.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
|
|
1423
|
+
}
|
|
1424
|
+
async function materializeContracts(options, services = /* @__PURE__ */ new Map()) {
|
|
1425
|
+
const {
|
|
1426
|
+
projectRoot,
|
|
1427
|
+
outputDir = path.join(projectRoot, "agent-kit", "materialized-contracts"),
|
|
1428
|
+
dynamicOnly = false,
|
|
1429
|
+
pluginFilter,
|
|
1430
|
+
verbose = false,
|
|
1431
|
+
viteServer
|
|
1432
|
+
} = options;
|
|
1433
|
+
const pluginsIndexMap = /* @__PURE__ */ new Map();
|
|
1434
|
+
let staticCount = 0;
|
|
1435
|
+
let dynamicCount = 0;
|
|
1436
|
+
if (verbose) {
|
|
1437
|
+
getLogger().info("Scanning for plugins...");
|
|
1438
|
+
}
|
|
1439
|
+
const plugins = await scanPlugins({
|
|
1440
|
+
projectRoot,
|
|
1441
|
+
verbose,
|
|
1442
|
+
includeDevDeps: true
|
|
1443
|
+
// Include dev deps for contract discovery
|
|
1444
|
+
});
|
|
1445
|
+
if (verbose) {
|
|
1446
|
+
getLogger().info(`Found ${plugins.size} plugin(s)`);
|
|
1447
|
+
}
|
|
1448
|
+
for (const [pluginKey, plugin] of plugins) {
|
|
1449
|
+
if (pluginFilter && plugin.name !== pluginFilter && pluginKey !== pluginFilter) continue;
|
|
1450
|
+
if (verbose) {
|
|
1451
|
+
getLogger().info(`
|
|
1452
|
+
📦 Processing plugin: ${plugin.name}`);
|
|
1453
|
+
}
|
|
1454
|
+
const { manifest } = plugin;
|
|
1455
|
+
const pluginRelPath = path.relative(projectRoot, plugin.pluginPath);
|
|
1456
|
+
if (!pluginsIndexMap.has(plugin.name)) {
|
|
1457
|
+
const entry = {
|
|
1458
|
+
path: "./" + pluginRelPath.replace(/\\/g, "/"),
|
|
1459
|
+
contracts: [],
|
|
1460
|
+
actions: []
|
|
1461
|
+
};
|
|
1462
|
+
if (manifest.services?.length) {
|
|
1463
|
+
entry.services = manifest.services.map((s2) => {
|
|
1464
|
+
const docPath = s2.doc ? "./" + path.relative(projectRoot, path.resolve(plugin.pluginPath, s2.doc)) : void 0;
|
|
1465
|
+
return {
|
|
1466
|
+
name: s2.name,
|
|
1467
|
+
marker: s2.marker,
|
|
1468
|
+
...s2.description && { description: s2.description },
|
|
1469
|
+
...docPath && { doc: docPath }
|
|
1470
|
+
};
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
if (manifest.contexts?.length) {
|
|
1474
|
+
entry.contexts = manifest.contexts.map((c) => {
|
|
1475
|
+
const docPath = c.doc ? "./" + path.relative(projectRoot, path.resolve(plugin.pluginPath, c.doc)) : void 0;
|
|
1476
|
+
return {
|
|
1477
|
+
name: c.name,
|
|
1478
|
+
marker: c.marker,
|
|
1479
|
+
...c.description && { description: c.description },
|
|
1480
|
+
...docPath && { doc: docPath }
|
|
1481
|
+
};
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
if (manifest.routes?.length) {
|
|
1485
|
+
entry.routes = manifest.routes.map((r) => ({
|
|
1486
|
+
path: r.path,
|
|
1487
|
+
...r.description && { description: r.description }
|
|
1488
|
+
}));
|
|
1489
|
+
}
|
|
1490
|
+
if (manifest.commands?.length) {
|
|
1491
|
+
entry.commands = manifest.commands.map((c) => {
|
|
1492
|
+
let description;
|
|
1493
|
+
if (c.command) {
|
|
1494
|
+
try {
|
|
1495
|
+
const cmdPath = path.resolve(plugin.pluginPath, c.command);
|
|
1496
|
+
const cmdContent = fs.readFileSync(cmdPath, "utf-8");
|
|
1497
|
+
const parsed = YAML.parse(cmdContent);
|
|
1498
|
+
description = parsed?.description;
|
|
1499
|
+
} catch {
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
return {
|
|
1503
|
+
name: c.name,
|
|
1504
|
+
...description && { description }
|
|
1505
|
+
};
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
pluginsIndexMap.set(plugin.name, entry);
|
|
1509
|
+
}
|
|
1510
|
+
if (!dynamicOnly && manifest.contracts) {
|
|
1511
|
+
for (const contract of manifest.contracts) {
|
|
1512
|
+
const contractPath = resolveStaticContractPath(
|
|
1513
|
+
plugin,
|
|
1514
|
+
contract.contract,
|
|
1515
|
+
projectRoot
|
|
1516
|
+
);
|
|
1517
|
+
const relativePath = path.relative(projectRoot, contractPath);
|
|
1518
|
+
let description = contract.description;
|
|
1519
|
+
if (!description) {
|
|
1520
|
+
try {
|
|
1521
|
+
const contractContent = fs.readFileSync(contractPath, "utf-8");
|
|
1522
|
+
const parsed = YAML.parse(contractContent);
|
|
1523
|
+
if (parsed?.description && typeof parsed.description === "string") {
|
|
1524
|
+
description = parsed.description;
|
|
1525
|
+
}
|
|
1526
|
+
} catch {
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
pluginsIndexMap.get(plugin.name).contracts.push({
|
|
1530
|
+
name: contract.name,
|
|
1531
|
+
...description && { description },
|
|
1532
|
+
type: "static",
|
|
1533
|
+
path: "./" + relativePath
|
|
1534
|
+
});
|
|
1535
|
+
staticCount++;
|
|
1536
|
+
if (verbose) {
|
|
1537
|
+
getLogger().info(` 📄 Static: ${contract.name}`);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
if (manifest.dynamic_contracts) {
|
|
1542
|
+
const dynamicConfigs = Array.isArray(manifest.dynamic_contracts) ? manifest.dynamic_contracts : [manifest.dynamic_contracts];
|
|
1543
|
+
const pluginOutputDir = path.join(outputDir, plugin.name.replace(/[@/]/g, "_"));
|
|
1544
|
+
fs.mkdirSync(pluginOutputDir, { recursive: true });
|
|
1545
|
+
for (const config of dynamicConfigs) {
|
|
1546
|
+
if (verbose) {
|
|
1547
|
+
getLogger().info(` ⚡ Dynamic contracts (prefix: ${config.prefix})`);
|
|
1548
|
+
}
|
|
1549
|
+
try {
|
|
1550
|
+
const generatedContracts = await executeDynamicGenerator(
|
|
1551
|
+
plugin,
|
|
1552
|
+
config,
|
|
1553
|
+
projectRoot,
|
|
1554
|
+
services,
|
|
1555
|
+
verbose,
|
|
1556
|
+
viteServer
|
|
1557
|
+
);
|
|
1558
|
+
const prefix = config.prefix;
|
|
1559
|
+
if (generatedContracts.length > 1) {
|
|
1560
|
+
const missing = generatedContracts.filter((c) => !c.name);
|
|
1561
|
+
if (missing.length > 0) {
|
|
1562
|
+
getLogger().error(
|
|
1563
|
+
` ❌ Dynamic contract generator for "${prefix}" returned ${generatedContracts.length} contracts but ${missing.length} are missing a name. Names are required when a generator returns multiple contracts.`
|
|
1564
|
+
);
|
|
1565
|
+
continue;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
for (const generated of generatedContracts) {
|
|
1569
|
+
const kebabName = generated.name ? toKebabCase(generated.name) : null;
|
|
1570
|
+
const fullName = kebabName ? `${prefix}/${kebabName}` : prefix;
|
|
1571
|
+
const fileName = kebabName ? `${prefix}-${kebabName}.jay-contract` : `${prefix}.jay-contract`;
|
|
1572
|
+
const filePath = path.join(pluginOutputDir, fileName);
|
|
1573
|
+
fs.writeFileSync(filePath, generated.yaml, "utf-8");
|
|
1574
|
+
const relativePath = path.relative(projectRoot, filePath);
|
|
1575
|
+
let dynDescription;
|
|
1576
|
+
try {
|
|
1577
|
+
const parsedYaml = YAML.parse(generated.yaml);
|
|
1578
|
+
if (parsedYaml?.description && typeof parsedYaml.description === "string") {
|
|
1579
|
+
dynDescription = parsedYaml.description;
|
|
1580
|
+
}
|
|
1581
|
+
} catch {
|
|
1582
|
+
}
|
|
1583
|
+
const contractEntry = {
|
|
1584
|
+
name: fullName,
|
|
1585
|
+
...dynDescription && { description: dynDescription },
|
|
1586
|
+
type: "dynamic",
|
|
1587
|
+
path: "./" + relativePath,
|
|
1588
|
+
...generated.metadata && { metadata: generated.metadata }
|
|
1589
|
+
};
|
|
1590
|
+
pluginsIndexMap.get(plugin.name).contracts.push(contractEntry);
|
|
1591
|
+
dynamicCount++;
|
|
1592
|
+
if (verbose) {
|
|
1593
|
+
getLogger().info(` ⚡ Materialized: ${fullName}`);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
} catch (error) {
|
|
1597
|
+
getLogger().error(
|
|
1598
|
+
` ❌ Failed to materialize dynamic contracts for ${plugin.name} (${config.prefix}): ${error}`
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
if (manifest.actions && Array.isArray(manifest.actions)) {
|
|
1604
|
+
for (const entry of manifest.actions) {
|
|
1605
|
+
const { name: actionName, action: actionPath } = normalizeActionEntry(entry);
|
|
1606
|
+
if (!actionPath) continue;
|
|
1607
|
+
const metadataFilePath = resolveActionFilePath(
|
|
1608
|
+
actionPath,
|
|
1609
|
+
plugin.packageName,
|
|
1610
|
+
plugin.pluginPath,
|
|
1611
|
+
plugin.isLocal,
|
|
1612
|
+
projectRoot
|
|
1613
|
+
);
|
|
1614
|
+
if (!metadataFilePath) continue;
|
|
1615
|
+
const metadata = loadActionMetadata(metadataFilePath);
|
|
1616
|
+
if (!metadata) continue;
|
|
1617
|
+
const actionRelPath = path.relative(projectRoot, metadataFilePath);
|
|
1618
|
+
const pluginEntry = pluginsIndexMap.get(plugin.name);
|
|
1619
|
+
if (!pluginEntry.actions) pluginEntry.actions = [];
|
|
1620
|
+
pluginEntry.actions.push({
|
|
1621
|
+
name: metadata.name,
|
|
1622
|
+
description: metadata.description,
|
|
1623
|
+
path: "./" + actionRelPath.replace(/\\/g, "/")
|
|
1624
|
+
});
|
|
1625
|
+
if (verbose) {
|
|
1626
|
+
getLogger().info(` 🔧 Action: ${metadata.name} (${actionPath})`);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
const pluginsIndex = {
|
|
1632
|
+
plugins: Array.from(pluginsIndexMap.entries()).map(([name, data]) => ({
|
|
1633
|
+
name,
|
|
1634
|
+
path: data.path,
|
|
1635
|
+
contracts: data.contracts,
|
|
1636
|
+
...data.actions && data.actions.length > 0 && { actions: data.actions },
|
|
1637
|
+
...data.services?.length && { services: data.services },
|
|
1638
|
+
...data.contexts?.length && { contexts: data.contexts },
|
|
1639
|
+
...data.routes?.length && { routes: data.routes },
|
|
1640
|
+
...data.commands?.length && { commands: data.commands }
|
|
1641
|
+
}))
|
|
1642
|
+
};
|
|
1643
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
1644
|
+
const agentKitDir = path.dirname(outputDir);
|
|
1645
|
+
const pluginsIndexPath = path.join(agentKitDir, "plugins-index.yaml");
|
|
1646
|
+
fs.writeFileSync(pluginsIndexPath, YAML.stringify(pluginsIndex), "utf-8");
|
|
1647
|
+
if (verbose) {
|
|
1648
|
+
getLogger().info(`
|
|
1649
|
+
✅ Plugins index written to: ${pluginsIndexPath}`);
|
|
1650
|
+
}
|
|
1651
|
+
return {
|
|
1652
|
+
pluginsIndex,
|
|
1653
|
+
staticCount,
|
|
1654
|
+
dynamicCount,
|
|
1655
|
+
outputDir
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
async function listContracts(options) {
|
|
1659
|
+
const { projectRoot, dynamicOnly = false, pluginFilter } = options;
|
|
1660
|
+
const pluginsMap = /* @__PURE__ */ new Map();
|
|
1661
|
+
const plugins = await scanPlugins({
|
|
1662
|
+
projectRoot,
|
|
1663
|
+
includeDevDeps: true
|
|
1664
|
+
});
|
|
1665
|
+
for (const [pluginKey, plugin] of plugins) {
|
|
1666
|
+
if (pluginFilter && plugin.name !== pluginFilter && pluginKey !== pluginFilter) continue;
|
|
1667
|
+
const { manifest } = plugin;
|
|
1668
|
+
const pluginRelPath = path.relative(projectRoot, plugin.pluginPath);
|
|
1669
|
+
if (!pluginsMap.has(plugin.name)) {
|
|
1670
|
+
const entry = {
|
|
1671
|
+
path: "./" + pluginRelPath.replace(/\\/g, "/"),
|
|
1672
|
+
contracts: []
|
|
1673
|
+
};
|
|
1674
|
+
if (manifest.services?.length) {
|
|
1675
|
+
entry.services = manifest.services.map((s2) => {
|
|
1676
|
+
const docPath = s2.doc ? "./" + path.relative(projectRoot, path.resolve(plugin.pluginPath, s2.doc)) : void 0;
|
|
1677
|
+
return {
|
|
1678
|
+
name: s2.name,
|
|
1679
|
+
marker: s2.marker,
|
|
1680
|
+
...s2.description && { description: s2.description },
|
|
1681
|
+
...docPath && { doc: docPath }
|
|
1682
|
+
};
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
if (manifest.contexts?.length) {
|
|
1686
|
+
entry.contexts = manifest.contexts.map((c) => {
|
|
1687
|
+
const docPath = c.doc ? "./" + path.relative(projectRoot, path.resolve(plugin.pluginPath, c.doc)) : void 0;
|
|
1688
|
+
return {
|
|
1689
|
+
name: c.name,
|
|
1690
|
+
marker: c.marker,
|
|
1691
|
+
...c.description && { description: c.description },
|
|
1692
|
+
...docPath && { doc: docPath }
|
|
1693
|
+
};
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
if (manifest.routes?.length) {
|
|
1697
|
+
entry.routes = manifest.routes.map((r) => ({
|
|
1698
|
+
path: r.path,
|
|
1699
|
+
...r.description && { description: r.description }
|
|
1700
|
+
}));
|
|
1701
|
+
}
|
|
1702
|
+
if (manifest.commands?.length) {
|
|
1703
|
+
entry.commands = manifest.commands.map((c) => {
|
|
1704
|
+
let description;
|
|
1705
|
+
if (c.command) {
|
|
1706
|
+
try {
|
|
1707
|
+
const cmdPath = path.resolve(plugin.pluginPath, c.command);
|
|
1708
|
+
const cmdContent = fs.readFileSync(cmdPath, "utf-8");
|
|
1709
|
+
const parsed = YAML.parse(cmdContent);
|
|
1710
|
+
description = parsed?.description;
|
|
1711
|
+
} catch {
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
return {
|
|
1715
|
+
name: c.name,
|
|
1716
|
+
...description && { description }
|
|
1717
|
+
};
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
pluginsMap.set(plugin.name, entry);
|
|
1721
|
+
}
|
|
1722
|
+
if (!dynamicOnly && manifest.contracts) {
|
|
1723
|
+
for (const contract of manifest.contracts) {
|
|
1724
|
+
const contractPath = resolveStaticContractPath(
|
|
1725
|
+
plugin,
|
|
1726
|
+
contract.contract,
|
|
1727
|
+
projectRoot
|
|
1728
|
+
);
|
|
1729
|
+
const relativePath = path.relative(projectRoot, contractPath);
|
|
1730
|
+
let listDescription = contract.description;
|
|
1731
|
+
if (!listDescription) {
|
|
1732
|
+
try {
|
|
1733
|
+
const contractContent = fs.readFileSync(contractPath, "utf-8");
|
|
1734
|
+
const parsed = YAML.parse(contractContent);
|
|
1735
|
+
if (parsed?.description && typeof parsed.description === "string") {
|
|
1736
|
+
listDescription = parsed.description;
|
|
1737
|
+
}
|
|
1738
|
+
} catch {
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
pluginsMap.get(plugin.name).contracts.push({
|
|
1742
|
+
name: contract.name,
|
|
1743
|
+
...listDescription && { description: listDescription },
|
|
1744
|
+
type: "static",
|
|
1745
|
+
path: "./" + relativePath
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
if (manifest.dynamic_contracts) {
|
|
1750
|
+
const dynamicConfigs = Array.isArray(manifest.dynamic_contracts) ? manifest.dynamic_contracts : [manifest.dynamic_contracts];
|
|
1751
|
+
for (const config of dynamicConfigs) {
|
|
1752
|
+
pluginsMap.get(plugin.name).contracts.push({
|
|
1753
|
+
name: `${config.prefix}/*`,
|
|
1754
|
+
type: "dynamic",
|
|
1755
|
+
path: "(run materialization to generate)"
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
return {
|
|
1761
|
+
plugins: Array.from(pluginsMap.entries()).map(([name, data]) => ({
|
|
1762
|
+
name,
|
|
1763
|
+
path: data.path,
|
|
1764
|
+
contracts: data.contracts,
|
|
1765
|
+
...data.services?.length && { services: data.services },
|
|
1766
|
+
...data.contexts?.length && { contexts: data.contexts },
|
|
1767
|
+
...data.routes?.length && { routes: data.routes },
|
|
1768
|
+
...data.commands?.length && { commands: data.commands }
|
|
1769
|
+
}))
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
async function discoverPluginCommands(options) {
|
|
1773
|
+
const { projectRoot, verbose, pluginFilter } = options;
|
|
1774
|
+
const allPlugins = await scanPlugins({
|
|
1775
|
+
projectRoot,
|
|
1776
|
+
verbose,
|
|
1777
|
+
discoverTransitive: true,
|
|
1778
|
+
includeDevDeps: true
|
|
1779
|
+
});
|
|
1780
|
+
const commands = [];
|
|
1781
|
+
for (const [packageName, plugin] of allPlugins) {
|
|
1782
|
+
if (!plugin.manifest.commands || plugin.manifest.commands.length === 0) continue;
|
|
1783
|
+
if (pluginFilter && plugin.name !== pluginFilter && packageName !== pluginFilter) {
|
|
1784
|
+
continue;
|
|
1785
|
+
}
|
|
1786
|
+
for (const cmd of plugin.manifest.commands) {
|
|
1787
|
+
let metadata;
|
|
1788
|
+
let metadataPath;
|
|
1789
|
+
if (cmd.command) {
|
|
1790
|
+
metadataPath = path.resolve(plugin.pluginPath, cmd.command);
|
|
1791
|
+
metadata = loadCommandMetadata(metadataPath);
|
|
1792
|
+
}
|
|
1793
|
+
commands.push({
|
|
1794
|
+
pluginName: plugin.name,
|
|
1795
|
+
pluginPath: plugin.pluginPath,
|
|
1796
|
+
packageName: plugin.packageName,
|
|
1797
|
+
isLocal: plugin.isLocal,
|
|
1798
|
+
commandName: cmd.name,
|
|
1799
|
+
handlerExport: cmd.name,
|
|
1800
|
+
pluginModule: plugin.manifest.module,
|
|
1801
|
+
metadata,
|
|
1802
|
+
metadataPath
|
|
1803
|
+
});
|
|
1804
|
+
if (verbose) {
|
|
1805
|
+
getLogger().info(`[Commands] Found ${plugin.name}/${cmd.name}`);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
return commands;
|
|
1810
|
+
}
|
|
1811
|
+
function loadCommandMetadata(filePath) {
|
|
1812
|
+
try {
|
|
1813
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
1814
|
+
return YAML.parse(content);
|
|
1815
|
+
} catch {
|
|
1816
|
+
return void 0;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
function commandSchemaToFlags(inputSchema) {
|
|
1820
|
+
const flags = [];
|
|
1821
|
+
for (const [field, type] of Object.entries(inputSchema)) {
|
|
1822
|
+
const isOptional = field.endsWith("?");
|
|
1823
|
+
const cleanName = isOptional ? field.slice(0, -1) : field;
|
|
1824
|
+
const kebabName = camelToKebab(cleanName);
|
|
1825
|
+
const cleanType = type.toLowerCase().trim();
|
|
1826
|
+
const isBoolean = cleanType === "boolean";
|
|
1827
|
+
flags.push({
|
|
1828
|
+
flag: isBoolean ? `--${kebabName}` : `--${kebabName} <value>`,
|
|
1829
|
+
description: "",
|
|
1830
|
+
required: !isOptional,
|
|
1831
|
+
type: cleanType === "number" ? "number" : isBoolean ? "boolean" : "string"
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
return flags;
|
|
1835
|
+
}
|
|
1836
|
+
function camelToKebab(str) {
|
|
1837
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
1838
|
+
}
|
|
1839
|
+
function parseInputFromFlags(rawOptions, schema) {
|
|
1840
|
+
const input = {};
|
|
1841
|
+
for (const [field, type] of Object.entries(schema)) {
|
|
1842
|
+
const isOptional = field.endsWith("?");
|
|
1843
|
+
const cleanName = isOptional ? field.slice(0, -1) : field;
|
|
1844
|
+
const kebabName = camelToKebab(cleanName);
|
|
1845
|
+
const value = rawOptions[kebabName];
|
|
1846
|
+
if (value === void 0) {
|
|
1847
|
+
if (!isOptional) {
|
|
1848
|
+
throw new Error(`Missing required flag: --${kebabName}`);
|
|
1849
|
+
}
|
|
1850
|
+
continue;
|
|
1851
|
+
}
|
|
1852
|
+
const cleanType = type.toLowerCase().trim();
|
|
1853
|
+
if (cleanType === "number") {
|
|
1854
|
+
const parsed = Number(value);
|
|
1855
|
+
if (isNaN(parsed)) throw new Error(`Flag --${kebabName} must be a number`);
|
|
1856
|
+
input[cleanName] = parsed;
|
|
1857
|
+
} else if (cleanType === "boolean") {
|
|
1858
|
+
input[cleanName] = value === true || value === "true";
|
|
1859
|
+
} else {
|
|
1860
|
+
input[cleanName] = String(value);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
return input;
|
|
1864
|
+
}
|
|
1865
|
+
async function executePluginCommand(command, input, viteServer) {
|
|
1866
|
+
const cliCommand = await loadCommandHandler(command, viteServer);
|
|
1867
|
+
const services = resolveServices(cliCommand.services);
|
|
1868
|
+
return cliCommand.handler(input, ...services);
|
|
1869
|
+
}
|
|
1870
|
+
async function loadCommandHandler(command, viteServer) {
|
|
1871
|
+
let module;
|
|
1872
|
+
if (command.isLocal) {
|
|
1873
|
+
const moduleFile = command.pluginModule || "index";
|
|
1874
|
+
const modulePath = path.resolve(command.pluginPath, moduleFile);
|
|
1875
|
+
if (viteServer) {
|
|
1876
|
+
module = await viteServer.ssrLoadModule(modulePath);
|
|
1877
|
+
} else {
|
|
1878
|
+
module = await import(modulePath);
|
|
1879
|
+
}
|
|
1880
|
+
} else {
|
|
1881
|
+
if (viteServer) {
|
|
1882
|
+
module = await viteServer.ssrLoadModule(command.packageName);
|
|
1883
|
+
} else {
|
|
1884
|
+
module = await import(command.packageName);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
for (const [, exported] of Object.entries(module)) {
|
|
1888
|
+
if (isJayCliCommand(exported) && exported.commandName === command.commandName) {
|
|
1889
|
+
return exported;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
const byName = module[command.handlerExport];
|
|
1893
|
+
if (byName && isJayCliCommand(byName)) {
|
|
1894
|
+
return byName;
|
|
1895
|
+
}
|
|
1896
|
+
throw new Error(
|
|
1897
|
+
`CLI command "${command.commandName}" not found as export in "${command.isLocal ? command.pluginPath : command.packageName}". Available exports: ${Object.keys(module).join(", ")}`
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
class SetupNeedsAnswerError extends Error {
|
|
1901
|
+
constructor(plugin, key, type, promptMessage, choices) {
|
|
1902
|
+
super(`Setup needs answer for "${key}": ${promptMessage}`);
|
|
1903
|
+
this.plugin = plugin;
|
|
1904
|
+
this.key = key;
|
|
1905
|
+
this.type = type;
|
|
1906
|
+
this.promptMessage = promptMessage;
|
|
1907
|
+
this.choices = choices;
|
|
1908
|
+
this.name = "SetupNeedsAnswerError";
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
async function discoverPluginsWithSetup(options) {
|
|
1912
|
+
const { projectRoot, verbose, pluginFilter } = options;
|
|
1913
|
+
const allPlugins = await scanPlugins({
|
|
1914
|
+
projectRoot,
|
|
1915
|
+
verbose,
|
|
1916
|
+
includeDevDeps: true,
|
|
1917
|
+
discoverTransitive: true
|
|
1918
|
+
});
|
|
1919
|
+
const pluginsWithSetup = [];
|
|
1920
|
+
for (const [packageName, plugin] of allPlugins) {
|
|
1921
|
+
if (!plugin.manifest.setup) continue;
|
|
1922
|
+
if (pluginFilter && plugin.name !== pluginFilter && packageName !== pluginFilter) {
|
|
1923
|
+
continue;
|
|
1924
|
+
}
|
|
1925
|
+
pluginsWithSetup.push({
|
|
1926
|
+
name: plugin.name,
|
|
1927
|
+
pluginPath: plugin.pluginPath,
|
|
1928
|
+
packageName: plugin.packageName,
|
|
1929
|
+
isLocal: plugin.isLocal,
|
|
1930
|
+
setupHandler: plugin.manifest.setup,
|
|
1931
|
+
setupDescription: plugin.manifest.description,
|
|
1932
|
+
dependencies: plugin.dependencies
|
|
1933
|
+
});
|
|
1934
|
+
if (verbose) {
|
|
1935
|
+
getLogger().info(`[Setup] Found plugin with setup: ${plugin.name}`);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
return sortPluginsByDependencies(pluginsWithSetup);
|
|
1939
|
+
}
|
|
1940
|
+
async function discoverPluginsWithAgentKit(options) {
|
|
1941
|
+
const { projectRoot, verbose, pluginFilter } = options;
|
|
1942
|
+
const allPlugins = await scanPlugins({
|
|
1943
|
+
projectRoot,
|
|
1944
|
+
verbose,
|
|
1945
|
+
includeDevDeps: true,
|
|
1946
|
+
discoverTransitive: true
|
|
1947
|
+
});
|
|
1948
|
+
const pluginsWithAgentKit = [];
|
|
1949
|
+
for (const [packageName, plugin] of allPlugins) {
|
|
1950
|
+
if (!plugin.manifest.agentkit) continue;
|
|
1951
|
+
if (pluginFilter && plugin.name !== pluginFilter && packageName !== pluginFilter) {
|
|
1952
|
+
continue;
|
|
1953
|
+
}
|
|
1954
|
+
pluginsWithAgentKit.push({
|
|
1955
|
+
name: plugin.name,
|
|
1956
|
+
pluginPath: plugin.pluginPath,
|
|
1957
|
+
packageName: plugin.packageName,
|
|
1958
|
+
isLocal: plugin.isLocal,
|
|
1959
|
+
agentKitHandler: plugin.manifest.agentkit,
|
|
1960
|
+
dependencies: plugin.dependencies
|
|
1961
|
+
});
|
|
1962
|
+
if (verbose) {
|
|
1963
|
+
getLogger().info(`[AgentKit] Found plugin with agent-kit handler: ${plugin.name}`);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
return sortPluginsByDependencies(pluginsWithAgentKit);
|
|
1967
|
+
}
|
|
1968
|
+
async function executePluginSetup(plugin, options) {
|
|
1969
|
+
const { projectRoot, configDir, force, interactive, prompt, initError, viteServer } = options;
|
|
1970
|
+
const context = {
|
|
1971
|
+
pluginName: plugin.name,
|
|
1972
|
+
projectRoot,
|
|
1973
|
+
configDir,
|
|
1974
|
+
services: getServiceRegistry(),
|
|
1975
|
+
initError,
|
|
1976
|
+
force,
|
|
1977
|
+
interactive,
|
|
1978
|
+
prompt
|
|
1979
|
+
};
|
|
1980
|
+
const handler = await loadHandler(plugin, plugin.setupHandler, viteServer);
|
|
1981
|
+
return handler(context);
|
|
1982
|
+
}
|
|
1983
|
+
async function executePluginAgentKit(plugin, options) {
|
|
1984
|
+
const { projectRoot, force, initError, viteServer } = options;
|
|
1985
|
+
const referencesDir = path.join(projectRoot, "agent-kit", "references", plugin.name);
|
|
1986
|
+
const context = {
|
|
1987
|
+
pluginName: plugin.name,
|
|
1988
|
+
projectRoot,
|
|
1989
|
+
referencesDir,
|
|
1990
|
+
services: getServiceRegistry(),
|
|
1991
|
+
initError,
|
|
1992
|
+
force
|
|
1993
|
+
};
|
|
1994
|
+
const handler = await loadHandler(
|
|
1995
|
+
plugin,
|
|
1996
|
+
plugin.agentKitHandler,
|
|
1997
|
+
viteServer
|
|
1998
|
+
);
|
|
1999
|
+
return handler(context);
|
|
2000
|
+
}
|
|
2001
|
+
async function loadHandler(plugin, handlerName, viteServer) {
|
|
2002
|
+
let module;
|
|
2003
|
+
if (plugin.isLocal) {
|
|
2004
|
+
const handlerPath = path.resolve(plugin.pluginPath, handlerName);
|
|
2005
|
+
if (viteServer) {
|
|
2006
|
+
module = await viteServer.ssrLoadModule(handlerPath);
|
|
2007
|
+
} else {
|
|
2008
|
+
module = await import(handlerPath);
|
|
2009
|
+
}
|
|
2010
|
+
if (typeof module[handlerName] === "function") return module[handlerName];
|
|
2011
|
+
if (typeof module.agentkit === "function") return module.agentkit;
|
|
2012
|
+
if (typeof module.setup === "function") return module.setup;
|
|
2013
|
+
if (typeof module.default === "function") return module.default;
|
|
2014
|
+
throw new Error(
|
|
2015
|
+
`Handler "${handlerName}" not found in "${plugin.pluginPath}". Available exports: ${Object.keys(module).join(", ")}`
|
|
2016
|
+
);
|
|
2017
|
+
} else {
|
|
2018
|
+
if (viteServer) {
|
|
2019
|
+
module = await viteServer.ssrLoadModule(plugin.packageName);
|
|
2020
|
+
} else {
|
|
2021
|
+
module = await import(plugin.packageName);
|
|
2022
|
+
}
|
|
2023
|
+
if (typeof module[handlerName] !== "function") {
|
|
2024
|
+
throw new Error(
|
|
2025
|
+
`Handler "${handlerName}" not found as export in "${plugin.packageName}". Available exports: ${Object.keys(module).join(", ")}`
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
2028
|
+
return module[handlerName];
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
const CACHE_TAG_START = '<script type="application/jay-cache">';
|
|
2032
|
+
const CACHE_TAG_END = "<\/script>";
|
|
2033
|
+
function hashParams(params) {
|
|
2034
|
+
const sortedParams = Object.keys(params).sort().reduce(
|
|
2035
|
+
(acc, key) => {
|
|
2036
|
+
acc[key] = params[key];
|
|
2037
|
+
return acc;
|
|
2038
|
+
},
|
|
2039
|
+
{}
|
|
2040
|
+
);
|
|
2041
|
+
const json = JSON.stringify(sortedParams);
|
|
2042
|
+
if (json === "{}") return "";
|
|
2043
|
+
return "_" + crypto.createHash("md5").update(json).digest("hex").substring(0, 8);
|
|
2044
|
+
}
|
|
2045
|
+
function embedCacheMetadata(jayHtmlContent, slowViewState, carryForward, sourcePath) {
|
|
2046
|
+
const metadata = JSON.stringify({ slowViewState, carryForward, sourcePath });
|
|
2047
|
+
const cacheTag = `${CACHE_TAG_START}${metadata}${CACHE_TAG_END}`;
|
|
2048
|
+
const headMatch = jayHtmlContent.match(/<head[^>]*>/i);
|
|
2049
|
+
if (headMatch) {
|
|
2050
|
+
const insertPos = headMatch.index + headMatch[0].length;
|
|
2051
|
+
return jayHtmlContent.substring(0, insertPos) + "\n" + cacheTag + jayHtmlContent.substring(insertPos);
|
|
2052
|
+
}
|
|
2053
|
+
return `${cacheTag}
|
|
2054
|
+
${jayHtmlContent}`;
|
|
2055
|
+
}
|
|
2056
|
+
function extractCacheMetadata(fileContent) {
|
|
2057
|
+
const startIdx = fileContent.indexOf(CACHE_TAG_START);
|
|
2058
|
+
if (startIdx === -1) return void 0;
|
|
2059
|
+
const jsonStart = startIdx + CACHE_TAG_START.length;
|
|
2060
|
+
const endIdx = fileContent.indexOf(CACHE_TAG_END, jsonStart);
|
|
2061
|
+
if (endIdx === -1) return void 0;
|
|
2062
|
+
const jsonStr = fileContent.substring(jsonStart, endIdx);
|
|
2063
|
+
const metadata = JSON.parse(jsonStr);
|
|
2064
|
+
const tagEnd = endIdx + CACHE_TAG_END.length;
|
|
2065
|
+
const afterTag = fileContent[tagEnd] === "\n" ? tagEnd + 1 : tagEnd;
|
|
2066
|
+
const content = fileContent.substring(0, startIdx) + fileContent.substring(afterTag);
|
|
2067
|
+
return {
|
|
2068
|
+
content,
|
|
2069
|
+
slowViewState: metadata.slowViewState,
|
|
2070
|
+
carryForward: metadata.carryForward,
|
|
2071
|
+
sourcePath: metadata.sourcePath
|
|
2072
|
+
};
|
|
2073
|
+
}
|
|
2074
|
+
class SlowRenderCache {
|
|
2075
|
+
/**
|
|
2076
|
+
* @param cacheDir - Directory where pre-rendered jay-html files are stored
|
|
2077
|
+
* @param pagesRoot - Root directory of the pages (for relative path calculation)
|
|
2078
|
+
*/
|
|
2079
|
+
constructor(cacheDir, pagesRoot) {
|
|
2080
|
+
/** Maps source jay-html path → set of pre-rendered file paths (for invalidation) */
|
|
2081
|
+
__publicField(this, "pathToFiles", /* @__PURE__ */ new Map());
|
|
2082
|
+
__publicField(this, "cacheDir");
|
|
2083
|
+
__publicField(this, "pagesRoot");
|
|
2084
|
+
this.cacheDir = cacheDir;
|
|
2085
|
+
this.pagesRoot = pagesRoot;
|
|
2086
|
+
}
|
|
2087
|
+
/**
|
|
2088
|
+
* Get a cached pre-rendered jay-html entry by reading from disk.
|
|
2089
|
+
* Returns undefined if the cache file doesn't exist or has no metadata tag.
|
|
2090
|
+
*/
|
|
2091
|
+
async get(jayHtmlPath, params, routeDir) {
|
|
2092
|
+
const preRenderedPath = this.computeCachePath(jayHtmlPath, params, routeDir);
|
|
2093
|
+
let fileContent;
|
|
2094
|
+
try {
|
|
2095
|
+
fileContent = await fs$1.readFile(preRenderedPath, "utf-8");
|
|
2096
|
+
} catch {
|
|
2097
|
+
return void 0;
|
|
2098
|
+
}
|
|
2099
|
+
const extracted = extractCacheMetadata(fileContent);
|
|
2100
|
+
if (!extracted) return void 0;
|
|
2101
|
+
this.trackFile(jayHtmlPath, preRenderedPath);
|
|
2102
|
+
return {
|
|
2103
|
+
preRenderedPath,
|
|
2104
|
+
preRenderedContent: extracted.content,
|
|
2105
|
+
slowViewState: extracted.slowViewState,
|
|
2106
|
+
carryForward: extracted.carryForward,
|
|
2107
|
+
sourcePath: extracted.sourcePath
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
/**
|
|
2111
|
+
* Store a pre-rendered jay-html entry.
|
|
2112
|
+
* Embeds metadata as a <script> tag and writes to disk.
|
|
2113
|
+
* Returns the full cache entry with stripped content.
|
|
2114
|
+
*/
|
|
2115
|
+
async set(jayHtmlPath, params, preRenderedJayHtml, slowViewState, carryForward, routeDir) {
|
|
2116
|
+
const preRenderedPath = this.computeCachePath(jayHtmlPath, params, routeDir);
|
|
2117
|
+
const fileContent = embedCacheMetadata(
|
|
2118
|
+
preRenderedJayHtml,
|
|
2119
|
+
slowViewState,
|
|
2120
|
+
carryForward,
|
|
2121
|
+
jayHtmlPath
|
|
2122
|
+
);
|
|
2123
|
+
await fs$1.mkdir(path__default.dirname(preRenderedPath), { recursive: true });
|
|
2124
|
+
await fs$1.writeFile(preRenderedPath, fileContent, "utf-8");
|
|
2125
|
+
this.trackFile(jayHtmlPath, preRenderedPath);
|
|
2126
|
+
return {
|
|
2127
|
+
preRenderedPath,
|
|
2128
|
+
preRenderedContent: preRenderedJayHtml,
|
|
2129
|
+
slowViewState,
|
|
2130
|
+
carryForward,
|
|
2131
|
+
sourcePath: jayHtmlPath
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
/**
|
|
2135
|
+
* Check if a pre-rendered entry exists for the given path and params
|
|
2136
|
+
*/
|
|
2137
|
+
async has(jayHtmlPath, params, routeDir) {
|
|
2138
|
+
const preRenderedPath = this.computeCachePath(jayHtmlPath, params, routeDir);
|
|
2139
|
+
try {
|
|
2140
|
+
await fs$1.access(preRenderedPath);
|
|
2141
|
+
return true;
|
|
2142
|
+
} catch {
|
|
2143
|
+
return false;
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
/**
|
|
2147
|
+
* Invalidate all cached entries for a given jay-html source path.
|
|
2148
|
+
* Deletes cached files from disk.
|
|
2149
|
+
*/
|
|
2150
|
+
async invalidate(jayHtmlPath) {
|
|
2151
|
+
const files = this.pathToFiles.get(jayHtmlPath);
|
|
2152
|
+
if (files) {
|
|
2153
|
+
for (const filePath of files) {
|
|
2154
|
+
try {
|
|
2155
|
+
await fs$1.unlink(filePath);
|
|
2156
|
+
} catch {
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
this.pathToFiles.delete(jayHtmlPath);
|
|
2160
|
+
}
|
|
2161
|
+
await this.scanAndDeleteCacheFiles(jayHtmlPath);
|
|
2162
|
+
}
|
|
2163
|
+
/**
|
|
2164
|
+
* Invalidate all entries that depend on a changed file.
|
|
2165
|
+
* The changedPath could be:
|
|
2166
|
+
* - A jay-html file itself
|
|
2167
|
+
* - A component file (page.ts)
|
|
2168
|
+
* - Any other dependency
|
|
2169
|
+
*
|
|
2170
|
+
* @param changedPath - Absolute path to the changed file
|
|
2171
|
+
* @param resolveDependencies - Optional function to resolve which jay-html files depend on the changed file
|
|
2172
|
+
*/
|
|
2173
|
+
async invalidateByDependency(changedPath, resolveDependencies) {
|
|
2174
|
+
if (changedPath.endsWith(".jay-html")) {
|
|
2175
|
+
await this.invalidate(changedPath);
|
|
2176
|
+
return;
|
|
2177
|
+
}
|
|
2178
|
+
if (resolveDependencies) {
|
|
2179
|
+
const dependentPaths = resolveDependencies(changedPath);
|
|
2180
|
+
for (const depPath of dependentPaths) {
|
|
2181
|
+
await this.invalidate(depPath);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
/**
|
|
2186
|
+
* Clear all cached entries and delete cached files from disk
|
|
2187
|
+
*/
|
|
2188
|
+
async clear() {
|
|
2189
|
+
for (const files of this.pathToFiles.values()) {
|
|
2190
|
+
for (const filePath of files) {
|
|
2191
|
+
try {
|
|
2192
|
+
await fs$1.unlink(filePath);
|
|
2193
|
+
} catch {
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
this.pathToFiles.clear();
|
|
2198
|
+
try {
|
|
2199
|
+
await fs$1.rm(this.cacheDir, { recursive: true, force: true });
|
|
2200
|
+
} catch {
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
/**
|
|
2204
|
+
* Get all cached jay-html paths (for debugging/monitoring)
|
|
2205
|
+
*/
|
|
2206
|
+
getCachedPaths() {
|
|
2207
|
+
return Array.from(this.pathToFiles.keys());
|
|
2208
|
+
}
|
|
2209
|
+
/**
|
|
2210
|
+
* Compute the cache file path for a given jay-html path and params.
|
|
2211
|
+
*/
|
|
2212
|
+
computeCachePath(jayHtmlPath, params, routeDir) {
|
|
2213
|
+
let dir;
|
|
2214
|
+
if (routeDir) {
|
|
2215
|
+
dir = routeDir;
|
|
2216
|
+
} else {
|
|
2217
|
+
const relativePath = path__default.relative(this.pagesRoot, jayHtmlPath);
|
|
2218
|
+
dir = relativePath.startsWith("..") ? path__default.join(
|
|
2219
|
+
"_plugins",
|
|
2220
|
+
crypto.createHash("md5").update(jayHtmlPath).digest("hex").slice(0, 12)
|
|
2221
|
+
) : path__default.dirname(relativePath);
|
|
2222
|
+
}
|
|
2223
|
+
const basename = path__default.basename(jayHtmlPath, ".jay-html");
|
|
2224
|
+
const paramsHash = hashParams(params);
|
|
2225
|
+
const cacheFileName = `${basename}${paramsHash}.jay-html`;
|
|
2226
|
+
return path__default.join(this.cacheDir, dir, cacheFileName);
|
|
2227
|
+
}
|
|
2228
|
+
/**
|
|
2229
|
+
* Track a pre-rendered file path for invalidation.
|
|
2230
|
+
*/
|
|
2231
|
+
trackFile(jayHtmlPath, preRenderedPath) {
|
|
2232
|
+
if (!this.pathToFiles.has(jayHtmlPath)) {
|
|
2233
|
+
this.pathToFiles.set(jayHtmlPath, /* @__PURE__ */ new Set());
|
|
2234
|
+
}
|
|
2235
|
+
this.pathToFiles.get(jayHtmlPath).add(preRenderedPath);
|
|
2236
|
+
}
|
|
2237
|
+
/**
|
|
2238
|
+
* Scan the cache directory for files matching a route and delete them.
|
|
2239
|
+
* Handles the startup case where pathToFiles is not populated from a previous session.
|
|
2240
|
+
*/
|
|
2241
|
+
async scanAndDeleteCacheFiles(jayHtmlPath) {
|
|
2242
|
+
const relativePath = path__default.relative(this.pagesRoot, jayHtmlPath);
|
|
2243
|
+
const dir = relativePath.startsWith("..") ? path__default.join(
|
|
2244
|
+
"_plugins",
|
|
2245
|
+
crypto.createHash("md5").update(jayHtmlPath).digest("hex").slice(0, 12)
|
|
2246
|
+
) : path__default.dirname(relativePath);
|
|
2247
|
+
const basename = path__default.basename(jayHtmlPath, ".jay-html");
|
|
2248
|
+
const cacheSubDir = path__default.join(this.cacheDir, dir);
|
|
2249
|
+
try {
|
|
2250
|
+
const files = await fs$1.readdir(cacheSubDir);
|
|
2251
|
+
for (const file of files) {
|
|
2252
|
+
if (file.startsWith(basename)) {
|
|
2253
|
+
try {
|
|
2254
|
+
await fs$1.unlink(path__default.join(cacheSubDir, file));
|
|
2255
|
+
} catch {
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
} catch {
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
export {
|
|
2264
|
+
SetupNeedsAnswerError,
|
|
2265
|
+
SlowRenderCache,
|
|
2266
|
+
buildAutomationWrap,
|
|
2267
|
+
buildScriptFragments,
|
|
2268
|
+
clearServerElementCache,
|
|
2269
|
+
commandSchemaToFlags,
|
|
2270
|
+
discoverAllPluginActions,
|
|
2271
|
+
discoverAndRegisterActions,
|
|
2272
|
+
discoverPluginActions,
|
|
2273
|
+
discoverPluginCommands,
|
|
2274
|
+
discoverPluginsWithAgentKit,
|
|
2275
|
+
discoverPluginsWithSetup,
|
|
2276
|
+
executePluginAgentKit,
|
|
2277
|
+
executePluginCommand,
|
|
2278
|
+
executePluginSetup,
|
|
2279
|
+
generateClientScript,
|
|
2280
|
+
generateFrozenPageHtml,
|
|
2281
|
+
generatePromiseReconstruction,
|
|
2282
|
+
generateSSRPageHtml,
|
|
2283
|
+
headMetaToHeadTags,
|
|
2284
|
+
invalidateServerElementCache,
|
|
2285
|
+
listContracts,
|
|
2286
|
+
loadActionMetadata,
|
|
2287
|
+
loadPageParts,
|
|
2288
|
+
materializeContracts,
|
|
2289
|
+
parseActionMetadata,
|
|
2290
|
+
parseInputFromFlags,
|
|
2291
|
+
resolveActionMetadataPath,
|
|
2292
|
+
resolveViewStatePromises
|
|
2293
|
+
};
|