@jay-framework/dev-server 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +91 -9
- package/dist/index.js +1212 -134
- package/package.json +13 -12
package/dist/index.js
CHANGED
|
@@ -6,14 +6,16 @@ var __publicField = (obj, key, value) => {
|
|
|
6
6
|
};
|
|
7
7
|
import { createServer } from "vite";
|
|
8
8
|
import { scanRoutes, routeToExpressRoute } from "@jay-framework/stack-route-scanner";
|
|
9
|
-
import { runInitCallbacks, runShutdownCallbacks, clearLifecycleCallbacks, clearServiceRegistry, DevSlowlyChangingPhase, loadPageParts, renderFastChangingData, generateClientScript } from "@jay-framework/stack-server-runtime";
|
|
9
|
+
import { discoverPluginsWithInit, sortPluginsByDependencies, executePluginServerInits, runInitCallbacks, actionRegistry, discoverAndRegisterActions, discoverAllPluginActions, runShutdownCallbacks, clearLifecycleCallbacks, clearServiceRegistry, clearClientInitData, DevSlowlyChangingPhase, SlowRenderCache, preparePluginClientInits, loadPageParts, renderFastChangingData, generateClientScript, getClientInitData } from "@jay-framework/stack-server-runtime";
|
|
10
10
|
import { jayRuntime } from "@jay-framework/vite-plugin";
|
|
11
11
|
import { createRequire } from "module";
|
|
12
12
|
import "@jay-framework/compiler-shared";
|
|
13
13
|
import * as path from "node:path";
|
|
14
14
|
import path__default from "node:path";
|
|
15
|
-
import "@jay-framework/compiler-jay-html";
|
|
15
|
+
import { parseContract, slowRenderTransform } from "@jay-framework/compiler-jay-html";
|
|
16
|
+
import { createRequire as createRequire$1 } from "node:module";
|
|
16
17
|
import * as fs from "node:fs";
|
|
18
|
+
import fs$1 from "node:fs/promises";
|
|
17
19
|
import { pathToFileURL } from "node:url";
|
|
18
20
|
const s$1 = createRequire(import.meta.url), e$1 = s$1("typescript"), c$1 = new Proxy(e$1, {
|
|
19
21
|
get(t, r) {
|
|
@@ -365,7 +367,7 @@ const {
|
|
|
365
367
|
SyntaxKind: SyntaxKind$4,
|
|
366
368
|
isStringLiteral: isStringLiteral$7,
|
|
367
369
|
visitNode: visitNode$4,
|
|
368
|
-
isFunctionDeclaration: isFunctionDeclaration$1
|
|
370
|
+
isFunctionDeclaration: isFunctionDeclaration$1,
|
|
369
371
|
isVariableStatement: isVariableStatement$2,
|
|
370
372
|
isImportDeclaration: isImportDeclaration$3,
|
|
371
373
|
isBlock: isBlock$2,
|
|
@@ -464,7 +466,7 @@ class SourceFileBindingResolver {
|
|
|
464
466
|
return node;
|
|
465
467
|
};
|
|
466
468
|
const visitor = (node) => {
|
|
467
|
-
if (isFunctionDeclaration$1
|
|
469
|
+
if (isFunctionDeclaration$1(node))
|
|
468
470
|
nbResolversQueue[0].addFunctionDeclaration(node);
|
|
469
471
|
if (isVariableStatement$2(node))
|
|
470
472
|
nbResolversQueue[0].addVariableStatement(node);
|
|
@@ -599,18 +601,25 @@ function areVariableRootsEqual(root1, root2) {
|
|
|
599
601
|
return false;
|
|
600
602
|
}
|
|
601
603
|
}
|
|
602
|
-
const
|
|
604
|
+
const COMPONENT_SERVER_METHODS = /* @__PURE__ */ new Set([
|
|
603
605
|
"withServices",
|
|
604
606
|
"withLoadParams",
|
|
605
607
|
"withSlowlyRender",
|
|
606
608
|
"withFastRender"
|
|
607
609
|
]);
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
]);
|
|
610
|
+
const COMPONENT_CLIENT_METHODS = /* @__PURE__ */ new Set(["withInteractive", "withContexts"]);
|
|
611
|
+
const INIT_SERVER_METHODS = /* @__PURE__ */ new Set(["withServer"]);
|
|
612
|
+
const INIT_CLIENT_METHODS = /* @__PURE__ */ new Set(["withClient"]);
|
|
612
613
|
function shouldRemoveMethod(methodName, environment) {
|
|
613
|
-
|
|
614
|
+
if (environment === "client" && COMPONENT_SERVER_METHODS.has(methodName))
|
|
615
|
+
return true;
|
|
616
|
+
if (environment === "server" && COMPONENT_CLIENT_METHODS.has(methodName))
|
|
617
|
+
return true;
|
|
618
|
+
if (environment === "client" && INIT_SERVER_METHODS.has(methodName))
|
|
619
|
+
return true;
|
|
620
|
+
if (environment === "server" && INIT_CLIENT_METHODS.has(methodName))
|
|
621
|
+
return true;
|
|
622
|
+
return false;
|
|
614
623
|
}
|
|
615
624
|
const { isCallExpression: isCallExpression$1, isPropertyAccessExpression: isPropertyAccessExpression$1, isIdentifier: isIdentifier$2, isStringLiteral } = c$1;
|
|
616
625
|
function findBuilderMethodsToRemove(sourceFile, bindingResolver, environment) {
|
|
@@ -631,6 +640,7 @@ function findBuilderMethodsToRemove(sourceFile, bindingResolver, environment) {
|
|
|
631
640
|
sourceFile.forEachChild(visit);
|
|
632
641
|
return { callsToRemove, removedVariables };
|
|
633
642
|
}
|
|
643
|
+
const JAY_BUILDER_FUNCTIONS = /* @__PURE__ */ new Set(["makeJayStackComponent", "makeJayInit"]);
|
|
634
644
|
function isPartOfJayStackChain(callExpr, bindingResolver) {
|
|
635
645
|
let current = callExpr.expression;
|
|
636
646
|
while (true) {
|
|
@@ -640,7 +650,7 @@ function isPartOfJayStackChain(callExpr, bindingResolver) {
|
|
|
640
650
|
if (isIdentifier$2(current.expression)) {
|
|
641
651
|
const variable = bindingResolver.explain(current.expression);
|
|
642
652
|
const flattened = flattenVariable(variable);
|
|
643
|
-
if (flattened.path.length === 1 && flattened.path[0]
|
|
653
|
+
if (flattened.path.length === 1 && JAY_BUILDER_FUNCTIONS.has(flattened.path[0]) && isImportModuleVariableRoot(flattened.root) && isStringLiteral(flattened.root.module) && flattened.root.module.text === "@jay-framework/fullstack-component")
|
|
644
654
|
return true;
|
|
645
655
|
}
|
|
646
656
|
if (isPropertyAccessExpression$1(current.expression)) {
|
|
@@ -670,15 +680,15 @@ function collectVariablesFromArguments(args, bindingResolver, variables) {
|
|
|
670
680
|
const {
|
|
671
681
|
isIdentifier: isIdentifier$1,
|
|
672
682
|
isImportDeclaration: isImportDeclaration$1,
|
|
673
|
-
isFunctionDeclaration
|
|
674
|
-
isVariableStatement
|
|
675
|
-
isInterfaceDeclaration
|
|
676
|
-
isTypeAliasDeclaration
|
|
683
|
+
isFunctionDeclaration,
|
|
684
|
+
isVariableStatement,
|
|
685
|
+
isInterfaceDeclaration,
|
|
686
|
+
isTypeAliasDeclaration,
|
|
677
687
|
isClassDeclaration,
|
|
678
688
|
isEnumDeclaration,
|
|
679
689
|
SyntaxKind
|
|
680
690
|
} = c$1;
|
|
681
|
-
function analyzeUnusedStatements(sourceFile
|
|
691
|
+
function analyzeUnusedStatements(sourceFile) {
|
|
682
692
|
const statementsToRemove = /* @__PURE__ */ new Set();
|
|
683
693
|
const collectUsedIdentifiers = () => {
|
|
684
694
|
const used = /* @__PURE__ */ new Set();
|
|
@@ -739,19 +749,19 @@ function isExportStatement(statement) {
|
|
|
739
749
|
return false;
|
|
740
750
|
}
|
|
741
751
|
function getStatementDefinedName(statement) {
|
|
742
|
-
if (isFunctionDeclaration
|
|
752
|
+
if (isFunctionDeclaration(statement) && statement.name) {
|
|
743
753
|
return statement.name.text;
|
|
744
754
|
}
|
|
745
|
-
if (isVariableStatement
|
|
755
|
+
if (isVariableStatement(statement)) {
|
|
746
756
|
const firstDecl = statement.declarationList.declarations[0];
|
|
747
757
|
if (firstDecl && isIdentifier$1(firstDecl.name)) {
|
|
748
758
|
return firstDecl.name.text;
|
|
749
759
|
}
|
|
750
760
|
}
|
|
751
|
-
if (isInterfaceDeclaration
|
|
761
|
+
if (isInterfaceDeclaration(statement) && statement.name) {
|
|
752
762
|
return statement.name.text;
|
|
753
763
|
}
|
|
754
|
-
if (isTypeAliasDeclaration
|
|
764
|
+
if (isTypeAliasDeclaration(statement) && statement.name) {
|
|
755
765
|
return statement.name.text;
|
|
756
766
|
}
|
|
757
767
|
if (isClassDeclaration(statement) && statement.name) {
|
|
@@ -771,19 +781,10 @@ const {
|
|
|
771
781
|
isPropertyAccessExpression,
|
|
772
782
|
isImportDeclaration,
|
|
773
783
|
isNamedImports,
|
|
774
|
-
isIdentifier
|
|
775
|
-
isFunctionDeclaration,
|
|
776
|
-
isVariableStatement,
|
|
777
|
-
isInterfaceDeclaration,
|
|
778
|
-
isTypeAliasDeclaration
|
|
784
|
+
isIdentifier
|
|
779
785
|
} = c$1;
|
|
780
786
|
function transformJayStackBuilder(code, filePath, environment) {
|
|
781
|
-
const sourceFile = createSourceFile(
|
|
782
|
-
filePath,
|
|
783
|
-
code,
|
|
784
|
-
ScriptTarget.Latest,
|
|
785
|
-
true
|
|
786
|
-
);
|
|
787
|
+
const sourceFile = createSourceFile(filePath, code, ScriptTarget.Latest, true);
|
|
787
788
|
const transformers = [mkTransformer(mkJayStackCodeSplitTransformer, { environment })];
|
|
788
789
|
const printer = createPrinter();
|
|
789
790
|
const result = c$1.transform(sourceFile, transformers);
|
|
@@ -804,11 +805,7 @@ function mkJayStackCodeSplitTransformer({
|
|
|
804
805
|
environment
|
|
805
806
|
}) {
|
|
806
807
|
const bindingResolver = new SourceFileBindingResolver(sourceFile);
|
|
807
|
-
const { callsToRemove
|
|
808
|
-
sourceFile,
|
|
809
|
-
bindingResolver,
|
|
810
|
-
environment
|
|
811
|
-
);
|
|
808
|
+
const { callsToRemove } = findBuilderMethodsToRemove(sourceFile, bindingResolver, environment);
|
|
812
809
|
const transformVisitor = (node) => {
|
|
813
810
|
if (isCallExpression(node) && isPropertyAccessExpression(node.expression)) {
|
|
814
811
|
const variable = bindingResolver.explain(node.expression);
|
|
@@ -820,11 +817,12 @@ function mkJayStackCodeSplitTransformer({
|
|
|
820
817
|
}
|
|
821
818
|
return visitEachChild(node, transformVisitor, context);
|
|
822
819
|
};
|
|
823
|
-
let transformedSourceFile = visitEachChild(
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
820
|
+
let transformedSourceFile = visitEachChild(
|
|
821
|
+
sourceFile,
|
|
822
|
+
transformVisitor,
|
|
823
|
+
context
|
|
827
824
|
);
|
|
825
|
+
const { statementsToRemove, unusedImports } = analyzeUnusedStatements(transformedSourceFile);
|
|
828
826
|
const transformedStatements = transformedSourceFile.statements.map((statement) => {
|
|
829
827
|
if (statementsToRemove.has(statement)) {
|
|
830
828
|
return void 0;
|
|
@@ -854,32 +852,409 @@ function filterImportDeclaration(statement, unusedImports, factory) {
|
|
|
854
852
|
importClause,
|
|
855
853
|
importClause.isTypeOnly,
|
|
856
854
|
importClause.name,
|
|
857
|
-
factory.updateNamedImports(
|
|
858
|
-
importClause.namedBindings,
|
|
859
|
-
usedElements
|
|
860
|
-
)
|
|
855
|
+
factory.updateNamedImports(importClause.namedBindings, usedElements)
|
|
861
856
|
),
|
|
862
857
|
statement.moduleSpecifier,
|
|
863
858
|
statement.assertClause
|
|
864
859
|
);
|
|
865
860
|
}
|
|
866
|
-
|
|
867
|
-
|
|
861
|
+
const actionMetadataCache = /* @__PURE__ */ new Map();
|
|
862
|
+
function clearActionMetadataCache() {
|
|
863
|
+
actionMetadataCache.clear();
|
|
864
|
+
}
|
|
865
|
+
function isActionImport(importSource) {
|
|
866
|
+
return importSource.includes(".actions") || importSource.includes("-actions") || importSource.includes("/actions/") || importSource.endsWith("/actions");
|
|
867
|
+
}
|
|
868
|
+
function extractActionsFromSource(sourceCode, filePath) {
|
|
869
|
+
const cached = actionMetadataCache.get(filePath);
|
|
870
|
+
if (cached) {
|
|
871
|
+
return cached;
|
|
872
|
+
}
|
|
873
|
+
const actions = [];
|
|
874
|
+
const sourceFile = c$1.createSourceFile(
|
|
875
|
+
filePath,
|
|
876
|
+
sourceCode,
|
|
877
|
+
c$1.ScriptTarget.Latest,
|
|
878
|
+
true
|
|
879
|
+
);
|
|
880
|
+
function visit(node) {
|
|
881
|
+
if (c$1.isVariableStatement(node)) {
|
|
882
|
+
const hasExport = node.modifiers?.some(
|
|
883
|
+
(m) => m.kind === c$1.SyntaxKind.ExportKeyword
|
|
884
|
+
);
|
|
885
|
+
if (!hasExport) {
|
|
886
|
+
c$1.forEachChild(node, visit);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
for (const decl of node.declarationList.declarations) {
|
|
890
|
+
if (!c$1.isIdentifier(decl.name) || !decl.initializer) {
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
const exportName = decl.name.text;
|
|
894
|
+
const actionMeta = extractActionFromExpression(decl.initializer);
|
|
895
|
+
if (actionMeta) {
|
|
896
|
+
actions.push({
|
|
897
|
+
...actionMeta,
|
|
898
|
+
exportName
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
c$1.forEachChild(node, visit);
|
|
904
|
+
}
|
|
905
|
+
visit(sourceFile);
|
|
906
|
+
actionMetadataCache.set(filePath, actions);
|
|
907
|
+
return actions;
|
|
908
|
+
}
|
|
909
|
+
function extractActionFromExpression(node) {
|
|
910
|
+
let current = node;
|
|
911
|
+
let method = "POST";
|
|
912
|
+
let explicitMethod = null;
|
|
913
|
+
while (c$1.isCallExpression(current)) {
|
|
914
|
+
const expr = current.expression;
|
|
915
|
+
if (c$1.isPropertyAccessExpression(expr) && expr.name.text === "withMethod") {
|
|
916
|
+
const arg = current.arguments[0];
|
|
917
|
+
if (arg && c$1.isStringLiteral(arg)) {
|
|
918
|
+
explicitMethod = arg.text;
|
|
919
|
+
}
|
|
920
|
+
current = expr.expression;
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
if (c$1.isPropertyAccessExpression(expr) && ["withServices", "withCaching", "withHandler", "withTimeout"].includes(expr.name.text)) {
|
|
924
|
+
current = expr.expression;
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
if (c$1.isIdentifier(expr)) {
|
|
928
|
+
const funcName = expr.text;
|
|
929
|
+
if (funcName === "makeJayAction" || funcName === "makeJayQuery") {
|
|
930
|
+
const nameArg = current.arguments[0];
|
|
931
|
+
if (nameArg && c$1.isStringLiteral(nameArg)) {
|
|
932
|
+
method = funcName === "makeJayQuery" ? "GET" : "POST";
|
|
933
|
+
if (explicitMethod) {
|
|
934
|
+
method = explicitMethod;
|
|
935
|
+
}
|
|
936
|
+
return {
|
|
937
|
+
actionName: nameArg.text,
|
|
938
|
+
method
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
break;
|
|
944
|
+
}
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
const SERVER_ONLY_MODULES = /* @__PURE__ */ new Set([
|
|
948
|
+
"module",
|
|
949
|
+
// createRequire
|
|
950
|
+
"fs",
|
|
951
|
+
"path",
|
|
952
|
+
"node:fs",
|
|
953
|
+
"node:path",
|
|
954
|
+
"node:module",
|
|
955
|
+
"child_process",
|
|
956
|
+
"node:child_process",
|
|
957
|
+
"crypto",
|
|
958
|
+
"node:crypto"
|
|
959
|
+
]);
|
|
960
|
+
const SERVER_ONLY_PACKAGE_PATTERNS = [
|
|
961
|
+
"@jay-framework/compiler-shared",
|
|
962
|
+
"@jay-framework/stack-server-runtime",
|
|
963
|
+
"yaml"
|
|
964
|
+
// Often used in server config
|
|
965
|
+
];
|
|
966
|
+
function createImportChainTracker(options = {}) {
|
|
967
|
+
const {
|
|
968
|
+
verbose = false,
|
|
969
|
+
additionalServerModules = [],
|
|
970
|
+
additionalServerPatterns = []
|
|
971
|
+
} = options;
|
|
972
|
+
const importChain = /* @__PURE__ */ new Map();
|
|
973
|
+
const detectedServerModules = /* @__PURE__ */ new Set();
|
|
974
|
+
const serverOnlyModules = /* @__PURE__ */ new Set([...SERVER_ONLY_MODULES, ...additionalServerModules]);
|
|
975
|
+
const serverOnlyPatterns = [...SERVER_ONLY_PACKAGE_PATTERNS, ...additionalServerPatterns];
|
|
976
|
+
function isServerOnlyModule(id) {
|
|
977
|
+
if (serverOnlyModules.has(id)) {
|
|
978
|
+
return true;
|
|
979
|
+
}
|
|
980
|
+
for (const pattern of serverOnlyPatterns) {
|
|
981
|
+
if (id.includes(pattern)) {
|
|
982
|
+
return true;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return false;
|
|
986
|
+
}
|
|
987
|
+
function buildImportChain(moduleId) {
|
|
988
|
+
const chain = [moduleId];
|
|
989
|
+
let current = moduleId;
|
|
990
|
+
for (let i = 0; i < 100; i++) {
|
|
991
|
+
const importer = importChain.get(current);
|
|
992
|
+
if (!importer)
|
|
993
|
+
break;
|
|
994
|
+
chain.push(importer);
|
|
995
|
+
current = importer;
|
|
996
|
+
}
|
|
997
|
+
return chain.reverse();
|
|
998
|
+
}
|
|
999
|
+
function formatChain(chain) {
|
|
1000
|
+
return chain.map((id, idx) => {
|
|
1001
|
+
const indent = " ".repeat(idx);
|
|
1002
|
+
const shortId = shortenPath(id);
|
|
1003
|
+
return `${indent}${idx === 0 ? "" : "↳ "}${shortId}`;
|
|
1004
|
+
}).join("\n");
|
|
1005
|
+
}
|
|
1006
|
+
function shortenPath(id) {
|
|
1007
|
+
if (id.includes("node_modules")) {
|
|
1008
|
+
const parts = id.split("node_modules/");
|
|
1009
|
+
return parts[parts.length - 1];
|
|
1010
|
+
}
|
|
1011
|
+
const cwd = process.cwd();
|
|
1012
|
+
if (id.startsWith(cwd)) {
|
|
1013
|
+
return id.slice(cwd.length + 1);
|
|
1014
|
+
}
|
|
1015
|
+
return id;
|
|
1016
|
+
}
|
|
1017
|
+
return {
|
|
1018
|
+
name: "jay-stack:import-chain-tracker",
|
|
1019
|
+
enforce: "pre",
|
|
1020
|
+
buildStart() {
|
|
1021
|
+
importChain.clear();
|
|
1022
|
+
detectedServerModules.clear();
|
|
1023
|
+
if (verbose) {
|
|
1024
|
+
console.log("[import-chain-tracker] Build started, tracking imports...");
|
|
1025
|
+
}
|
|
1026
|
+
},
|
|
1027
|
+
resolveId(source, importer, options2) {
|
|
1028
|
+
if (options2?.ssr) {
|
|
1029
|
+
return null;
|
|
1030
|
+
}
|
|
1031
|
+
if (source.startsWith("\0")) {
|
|
1032
|
+
return null;
|
|
1033
|
+
}
|
|
1034
|
+
if (importer) {
|
|
1035
|
+
if (verbose) {
|
|
1036
|
+
console.log(
|
|
1037
|
+
`[import-chain-tracker] ${shortenPath(importer)} imports ${source}`
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
return null;
|
|
1042
|
+
},
|
|
1043
|
+
load(id) {
|
|
1044
|
+
return null;
|
|
1045
|
+
},
|
|
1046
|
+
transform(code, id, options2) {
|
|
1047
|
+
if (options2?.ssr) {
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
if (isServerOnlyModule(id)) {
|
|
1051
|
+
detectedServerModules.add(id);
|
|
1052
|
+
const chain = buildImportChain(id);
|
|
1053
|
+
console.error(
|
|
1054
|
+
`
|
|
1055
|
+
[import-chain-tracker] ⚠️ Server-only module detected in client build!`
|
|
1056
|
+
);
|
|
1057
|
+
console.error(`Module: ${shortenPath(id)}`);
|
|
1058
|
+
console.error(`Import chain:`);
|
|
1059
|
+
console.error(formatChain(chain));
|
|
1060
|
+
console.error("");
|
|
1061
|
+
}
|
|
1062
|
+
const importRegex = /import\s+(?:(?:\{[^}]*\}|[^{}\s,]+)\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
1063
|
+
let match;
|
|
1064
|
+
while ((match = importRegex.exec(code)) !== null) {
|
|
1065
|
+
const importedModule = match[1];
|
|
1066
|
+
importChain.set(importedModule, id);
|
|
1067
|
+
if (isServerOnlyModule(importedModule)) {
|
|
1068
|
+
if (!detectedServerModules.has(importedModule)) {
|
|
1069
|
+
detectedServerModules.add(importedModule);
|
|
1070
|
+
console.error(
|
|
1071
|
+
`
|
|
1072
|
+
[import-chain-tracker] ⚠️ Server-only import detected in client build!`
|
|
1073
|
+
);
|
|
1074
|
+
console.error(`Module "${importedModule}" imported by: ${shortenPath(id)}`);
|
|
1075
|
+
const chain = buildImportChain(id);
|
|
1076
|
+
chain.push(importedModule);
|
|
1077
|
+
console.error(`Import chain:`);
|
|
1078
|
+
console.error(formatChain(chain));
|
|
1079
|
+
console.error("");
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
return null;
|
|
1084
|
+
},
|
|
1085
|
+
buildEnd() {
|
|
1086
|
+
if (detectedServerModules.size > 0) {
|
|
1087
|
+
console.warn(
|
|
1088
|
+
`
|
|
1089
|
+
[import-chain-tracker] ⚠️ ${detectedServerModules.size} server-only module(s) detected during transform:`
|
|
1090
|
+
);
|
|
1091
|
+
for (const mod of detectedServerModules) {
|
|
1092
|
+
console.warn(` - ${mod}`);
|
|
1093
|
+
}
|
|
1094
|
+
console.warn(
|
|
1095
|
+
"\nNote: These may be stripped by the code-split transform if only used in .withServer()."
|
|
1096
|
+
);
|
|
1097
|
+
console.warn(
|
|
1098
|
+
'If build fails with "not exported" errors, check the import chains above.\n'
|
|
1099
|
+
);
|
|
1100
|
+
} else if (verbose) {
|
|
1101
|
+
console.log(
|
|
1102
|
+
"[import-chain-tracker] ✅ No server-only modules detected in client build"
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
const require2 = createRequire$1(import.meta.url);
|
|
1109
|
+
function createDefaultPluginDetector() {
|
|
1110
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1111
|
+
return {
|
|
1112
|
+
isJayPluginWithClientExport(packageName, projectRoot) {
|
|
1113
|
+
const cacheKey = `${packageName}:${projectRoot}`;
|
|
1114
|
+
if (cache.has(cacheKey)) {
|
|
1115
|
+
return cache.get(cacheKey);
|
|
1116
|
+
}
|
|
1117
|
+
let result = false;
|
|
1118
|
+
try {
|
|
1119
|
+
require2.resolve(`${packageName}/plugin.yaml`, { paths: [projectRoot] });
|
|
1120
|
+
try {
|
|
1121
|
+
require2.resolve(`${packageName}/client`, { paths: [projectRoot] });
|
|
1122
|
+
result = true;
|
|
1123
|
+
} catch {
|
|
1124
|
+
result = false;
|
|
1125
|
+
}
|
|
1126
|
+
} catch {
|
|
1127
|
+
result = false;
|
|
1128
|
+
}
|
|
1129
|
+
cache.set(cacheKey, result);
|
|
1130
|
+
return result;
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
function extractPackageName(source) {
|
|
1135
|
+
if (source.startsWith(".") || source.startsWith("/")) {
|
|
1136
|
+
return null;
|
|
1137
|
+
}
|
|
1138
|
+
if (source.startsWith("@")) {
|
|
1139
|
+
const parts2 = source.split("/");
|
|
1140
|
+
if (parts2.length >= 2) {
|
|
1141
|
+
return `${parts2[0]}/${parts2[1]}`;
|
|
1142
|
+
}
|
|
1143
|
+
return null;
|
|
1144
|
+
}
|
|
1145
|
+
const parts = source.split("/");
|
|
1146
|
+
return parts[0];
|
|
1147
|
+
}
|
|
1148
|
+
function isSubpathImport(source, packageName) {
|
|
1149
|
+
return source.length > packageName.length && source[packageName.length] === "/";
|
|
1150
|
+
}
|
|
1151
|
+
const IMPORT_REGEX = /import\s+(.+?)\s+from\s+(['"])([^'"]+)\2/g;
|
|
1152
|
+
const EXPORT_FROM_REGEX = /export\s+(.+?)\s+from\s+(['"])([^'"]+)\2/g;
|
|
1153
|
+
function transformImports(options) {
|
|
1154
|
+
const { code, projectRoot, filePath, pluginDetector, verbose = false } = options;
|
|
1155
|
+
let hasChanges = false;
|
|
1156
|
+
let result = code;
|
|
1157
|
+
result = result.replace(IMPORT_REGEX, (match, clause, quote, source) => {
|
|
1158
|
+
const packageName = extractPackageName(source);
|
|
1159
|
+
if (!packageName)
|
|
1160
|
+
return match;
|
|
1161
|
+
if (isSubpathImport(source, packageName))
|
|
1162
|
+
return match;
|
|
1163
|
+
if (!pluginDetector.isJayPluginWithClientExport(packageName, projectRoot))
|
|
1164
|
+
return match;
|
|
1165
|
+
hasChanges = true;
|
|
1166
|
+
const newSource = `${packageName}/client`;
|
|
1167
|
+
if (verbose) {
|
|
1168
|
+
console.log(
|
|
1169
|
+
`[plugin-client-import] Rewriting import ${source} -> ${newSource} (in ${path.basename(filePath)})`
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
return `import ${clause} from ${quote}${newSource}${quote}`;
|
|
1173
|
+
});
|
|
1174
|
+
result = result.replace(EXPORT_FROM_REGEX, (match, clause, quote, source) => {
|
|
1175
|
+
const packageName = extractPackageName(source);
|
|
1176
|
+
if (!packageName)
|
|
1177
|
+
return match;
|
|
1178
|
+
if (isSubpathImport(source, packageName))
|
|
1179
|
+
return match;
|
|
1180
|
+
if (!pluginDetector.isJayPluginWithClientExport(packageName, projectRoot))
|
|
1181
|
+
return match;
|
|
1182
|
+
hasChanges = true;
|
|
1183
|
+
const newSource = `${packageName}/client`;
|
|
1184
|
+
if (verbose) {
|
|
1185
|
+
console.log(
|
|
1186
|
+
`[plugin-client-import] Rewriting export ${source} -> ${newSource} (in ${path.basename(filePath)})`
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
return `export ${clause} from ${quote}${newSource}${quote}`;
|
|
1190
|
+
});
|
|
1191
|
+
return { code: result, hasChanges };
|
|
1192
|
+
}
|
|
1193
|
+
function createPluginClientImportResolver(options = {}) {
|
|
1194
|
+
const { verbose = false } = options;
|
|
1195
|
+
let projectRoot = options.projectRoot || process.cwd();
|
|
1196
|
+
let isSSRBuild = false;
|
|
1197
|
+
const pluginDetector = options.pluginDetector || createDefaultPluginDetector();
|
|
1198
|
+
return {
|
|
1199
|
+
name: "jay-stack:plugin-client-import",
|
|
1200
|
+
enforce: "pre",
|
|
1201
|
+
configResolved(config) {
|
|
1202
|
+
projectRoot = config.root || projectRoot;
|
|
1203
|
+
isSSRBuild = !!config.build?.ssr;
|
|
1204
|
+
},
|
|
1205
|
+
transform(code, id, transformOptions) {
|
|
1206
|
+
if (transformOptions?.ssr || isSSRBuild) {
|
|
1207
|
+
return null;
|
|
1208
|
+
}
|
|
1209
|
+
if (!id.endsWith(".ts") && !id.endsWith(".js") && !id.includes(".ts?") && !id.includes(".js?")) {
|
|
1210
|
+
return null;
|
|
1211
|
+
}
|
|
1212
|
+
if (id.includes("node_modules") && !id.includes("@jay-framework")) {
|
|
1213
|
+
return null;
|
|
1214
|
+
}
|
|
1215
|
+
if (!code.includes("@jay-framework/") && !code.includes("from '@") && !code.includes('from "@')) {
|
|
1216
|
+
return null;
|
|
1217
|
+
}
|
|
1218
|
+
const result = transformImports({
|
|
1219
|
+
code,
|
|
1220
|
+
projectRoot,
|
|
1221
|
+
filePath: id,
|
|
1222
|
+
pluginDetector,
|
|
1223
|
+
verbose
|
|
1224
|
+
});
|
|
1225
|
+
if (!result.hasChanges) {
|
|
1226
|
+
return null;
|
|
1227
|
+
}
|
|
1228
|
+
return { code: result.code };
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
function jayStackCompiler(options = {}) {
|
|
1233
|
+
const { trackImports, ...jayOptions } = options;
|
|
1234
|
+
const moduleCache = /* @__PURE__ */ new Map();
|
|
1235
|
+
const shouldTrackImports = trackImports || process.env.DEBUG_IMPORTS === "1";
|
|
1236
|
+
const trackerOptions = typeof trackImports === "object" ? trackImports : { verbose: process.env.DEBUG_IMPORTS === "1" };
|
|
1237
|
+
const plugins = [];
|
|
1238
|
+
if (shouldTrackImports) {
|
|
1239
|
+
plugins.push(createImportChainTracker(trackerOptions));
|
|
1240
|
+
}
|
|
1241
|
+
plugins.push(createPluginClientImportResolver({ verbose: !!shouldTrackImports }));
|
|
1242
|
+
plugins.push(
|
|
868
1243
|
// First: Jay Stack code splitting transformation
|
|
869
1244
|
{
|
|
870
1245
|
name: "jay-stack:code-split",
|
|
871
1246
|
enforce: "pre",
|
|
872
1247
|
// Run before jay:runtime
|
|
873
|
-
transform(code, id) {
|
|
874
|
-
|
|
875
|
-
const isServerBuild = id.includes("?jay-server");
|
|
876
|
-
if (!isClientBuild && !isServerBuild) {
|
|
1248
|
+
transform(code, id, options2) {
|
|
1249
|
+
if (!id.endsWith(".ts") && !id.includes(".ts?")) {
|
|
877
1250
|
return null;
|
|
878
1251
|
}
|
|
879
|
-
const
|
|
880
|
-
|
|
1252
|
+
const hasComponent = code.includes("makeJayStackComponent");
|
|
1253
|
+
const hasInit = code.includes("makeJayInit");
|
|
1254
|
+
if (!hasComponent && !hasInit) {
|
|
881
1255
|
return null;
|
|
882
1256
|
}
|
|
1257
|
+
const environment = options2?.ssr ? "server" : "client";
|
|
883
1258
|
try {
|
|
884
1259
|
return transformJayStackBuilder(code, id, environment);
|
|
885
1260
|
} catch (error) {
|
|
@@ -888,15 +1263,100 @@ function jayStackCompiler(jayOptions = {}) {
|
|
|
888
1263
|
}
|
|
889
1264
|
}
|
|
890
1265
|
},
|
|
891
|
-
// Second:
|
|
1266
|
+
// Second: Action import transformation (client builds only)
|
|
1267
|
+
// Uses resolveId + load to replace action modules with virtual modules
|
|
1268
|
+
// containing createActionCaller calls BEFORE bundling happens.
|
|
1269
|
+
(() => {
|
|
1270
|
+
let isSSRBuild = false;
|
|
1271
|
+
return {
|
|
1272
|
+
name: "jay-stack:action-transform",
|
|
1273
|
+
enforce: "pre",
|
|
1274
|
+
// Track SSR mode from config
|
|
1275
|
+
configResolved(config) {
|
|
1276
|
+
isSSRBuild = config.build?.ssr ?? false;
|
|
1277
|
+
},
|
|
1278
|
+
buildStart() {
|
|
1279
|
+
clearActionMetadataCache();
|
|
1280
|
+
moduleCache.clear();
|
|
1281
|
+
},
|
|
1282
|
+
async resolveId(source, importer, options2) {
|
|
1283
|
+
if (options2?.ssr || isSSRBuild) {
|
|
1284
|
+
return null;
|
|
1285
|
+
}
|
|
1286
|
+
if (!isActionImport(source)) {
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
if (!source.startsWith(".") || !importer) {
|
|
1290
|
+
return null;
|
|
1291
|
+
}
|
|
1292
|
+
const importerDir = path.dirname(importer);
|
|
1293
|
+
let resolvedPath = path.resolve(importerDir, source);
|
|
1294
|
+
if (!resolvedPath.endsWith(".ts") && !resolvedPath.endsWith(".js")) {
|
|
1295
|
+
if (fs.existsSync(resolvedPath + ".ts")) {
|
|
1296
|
+
resolvedPath += ".ts";
|
|
1297
|
+
} else if (fs.existsSync(resolvedPath + ".js")) {
|
|
1298
|
+
resolvedPath += ".js";
|
|
1299
|
+
} else {
|
|
1300
|
+
return null;
|
|
1301
|
+
}
|
|
1302
|
+
} else if (resolvedPath.endsWith(".js") && !fs.existsSync(resolvedPath)) {
|
|
1303
|
+
const tsPath = resolvedPath.slice(0, -3) + ".ts";
|
|
1304
|
+
if (fs.existsSync(tsPath)) {
|
|
1305
|
+
resolvedPath = tsPath;
|
|
1306
|
+
} else {
|
|
1307
|
+
return null;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
return `\0jay-action:${resolvedPath}`;
|
|
1311
|
+
},
|
|
1312
|
+
async load(id) {
|
|
1313
|
+
if (!id.startsWith("\0jay-action:")) {
|
|
1314
|
+
return null;
|
|
1315
|
+
}
|
|
1316
|
+
const actualPath = id.slice("\0jay-action:".length);
|
|
1317
|
+
let code;
|
|
1318
|
+
try {
|
|
1319
|
+
code = await fs.promises.readFile(actualPath, "utf-8");
|
|
1320
|
+
} catch (err) {
|
|
1321
|
+
console.error(`[action-transform] Could not read ${actualPath}:`, err);
|
|
1322
|
+
return null;
|
|
1323
|
+
}
|
|
1324
|
+
const actions = extractActionsFromSource(code, actualPath);
|
|
1325
|
+
if (actions.length === 0) {
|
|
1326
|
+
console.warn(`[action-transform] No actions found in ${actualPath}`);
|
|
1327
|
+
return null;
|
|
1328
|
+
}
|
|
1329
|
+
const lines = [
|
|
1330
|
+
`import { createActionCaller } from '@jay-framework/stack-client-runtime';`,
|
|
1331
|
+
""
|
|
1332
|
+
];
|
|
1333
|
+
for (const action of actions) {
|
|
1334
|
+
lines.push(
|
|
1335
|
+
`export const ${action.exportName} = createActionCaller('${action.actionName}', '${action.method}');`
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
if (code.includes("ActionError")) {
|
|
1339
|
+
lines.push(
|
|
1340
|
+
`export { ActionError } from '@jay-framework/stack-client-runtime';`
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
const result = lines.join("\n");
|
|
1344
|
+
return result;
|
|
1345
|
+
}
|
|
1346
|
+
};
|
|
1347
|
+
})(),
|
|
1348
|
+
// Third: Jay runtime compilation (existing plugin)
|
|
892
1349
|
jayRuntime(jayOptions)
|
|
893
|
-
|
|
1350
|
+
);
|
|
1351
|
+
return plugins;
|
|
894
1352
|
}
|
|
895
1353
|
class ServiceLifecycleManager {
|
|
896
1354
|
constructor(projectRoot, sourceBase = "src") {
|
|
897
|
-
|
|
1355
|
+
/** Path to project's lib/init.ts (makeJayInit pattern) */
|
|
1356
|
+
__publicField(this, "projectInitFilePath", null);
|
|
898
1357
|
__publicField(this, "isInitialized", false);
|
|
899
1358
|
__publicField(this, "viteServer", null);
|
|
1359
|
+
__publicField(this, "pluginsWithInit", []);
|
|
900
1360
|
this.projectRoot = projectRoot;
|
|
901
1361
|
this.sourceBase = sourceBase;
|
|
902
1362
|
}
|
|
@@ -907,14 +1367,13 @@ class ServiceLifecycleManager {
|
|
|
907
1367
|
this.viteServer = viteServer;
|
|
908
1368
|
}
|
|
909
1369
|
/**
|
|
910
|
-
* Finds the
|
|
911
|
-
* Looks in: {projectRoot}/{sourceBase}/
|
|
1370
|
+
* Finds the project init file using makeJayInit pattern.
|
|
1371
|
+
* Looks in: {projectRoot}/{sourceBase}/init.{ts,js}
|
|
912
1372
|
*/
|
|
913
|
-
|
|
914
|
-
const extensions = [".ts", ".js"
|
|
915
|
-
const baseFilename = "jay.init";
|
|
1373
|
+
findProjectInitFile() {
|
|
1374
|
+
const extensions = [".ts", ".js"];
|
|
916
1375
|
for (const ext of extensions) {
|
|
917
|
-
const filePath = path.join(this.projectRoot, this.sourceBase,
|
|
1376
|
+
const filePath = path.join(this.projectRoot, this.sourceBase, "init" + ext);
|
|
918
1377
|
if (fs.existsSync(filePath)) {
|
|
919
1378
|
return filePath;
|
|
920
1379
|
}
|
|
@@ -922,32 +1381,88 @@ class ServiceLifecycleManager {
|
|
|
922
1381
|
return null;
|
|
923
1382
|
}
|
|
924
1383
|
/**
|
|
925
|
-
* Initializes services by
|
|
1384
|
+
* Initializes services by:
|
|
1385
|
+
* 1. Discovering and executing plugin server inits (in dependency order)
|
|
1386
|
+
* 2. Loading and executing project lib/init.ts
|
|
1387
|
+
* 3. Running all registered onInit callbacks
|
|
1388
|
+
* 4. Auto-discovering and registering actions
|
|
926
1389
|
*/
|
|
927
1390
|
async initialize() {
|
|
928
1391
|
if (this.isInitialized) {
|
|
929
1392
|
console.warn("[Services] Already initialized, skipping...");
|
|
930
1393
|
return;
|
|
931
1394
|
}
|
|
932
|
-
this.
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1395
|
+
this.projectInitFilePath = this.findProjectInitFile();
|
|
1396
|
+
const discoveredPlugins = await discoverPluginsWithInit({
|
|
1397
|
+
projectRoot: this.projectRoot,
|
|
1398
|
+
verbose: true
|
|
1399
|
+
});
|
|
1400
|
+
this.pluginsWithInit = sortPluginsByDependencies(discoveredPlugins);
|
|
1401
|
+
if (this.pluginsWithInit.length > 0) {
|
|
1402
|
+
console.log(
|
|
1403
|
+
`[Services] Found ${this.pluginsWithInit.length} plugin(s) with init: ${this.pluginsWithInit.map((p) => p.name).join(", ")}`
|
|
1404
|
+
);
|
|
936
1405
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1406
|
+
await executePluginServerInits(this.pluginsWithInit, this.viteServer ?? void 0, true);
|
|
1407
|
+
if (this.projectInitFilePath) {
|
|
1408
|
+
console.log("[DevServer] Loading project init: src/init.ts");
|
|
1409
|
+
try {
|
|
1410
|
+
if (this.viteServer) {
|
|
1411
|
+
const module = await this.viteServer.ssrLoadModule(this.projectInitFilePath);
|
|
1412
|
+
if (module.init?._serverInit) {
|
|
1413
|
+
console.log("[DevServer] Running server init: project");
|
|
1414
|
+
const { setClientInitData } = await import("@jay-framework/stack-server-runtime");
|
|
1415
|
+
const clientData = await module.init._serverInit();
|
|
1416
|
+
if (clientData !== void 0 && clientData !== null) {
|
|
1417
|
+
setClientInitData("project", clientData);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
} else {
|
|
1421
|
+
const fileUrl = pathToFileURL(this.projectInitFilePath).href;
|
|
1422
|
+
await import(fileUrl);
|
|
1423
|
+
}
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
console.error("[Services] Failed to load project init:", error);
|
|
1426
|
+
throw error;
|
|
944
1427
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1428
|
+
} else {
|
|
1429
|
+
console.log("[Services] No init.ts found, skipping project initialization");
|
|
1430
|
+
}
|
|
1431
|
+
await runInitCallbacks();
|
|
1432
|
+
console.log("[Services] Initialization complete");
|
|
1433
|
+
await this.discoverActions();
|
|
1434
|
+
this.isInitialized = true;
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Auto-discovers and registers actions from project and plugins.
|
|
1438
|
+
*/
|
|
1439
|
+
async discoverActions() {
|
|
1440
|
+
let totalActions = 0;
|
|
1441
|
+
try {
|
|
1442
|
+
const result = await discoverAndRegisterActions({
|
|
1443
|
+
projectRoot: this.projectRoot,
|
|
1444
|
+
actionsDir: path.join(this.sourceBase, "actions"),
|
|
1445
|
+
registry: actionRegistry,
|
|
1446
|
+
verbose: true,
|
|
1447
|
+
viteServer: this.viteServer ?? void 0
|
|
1448
|
+
});
|
|
1449
|
+
totalActions += result.actionCount;
|
|
948
1450
|
} catch (error) {
|
|
949
|
-
console.error("[
|
|
950
|
-
|
|
1451
|
+
console.error("[Actions] Failed to auto-discover project actions:", error);
|
|
1452
|
+
}
|
|
1453
|
+
try {
|
|
1454
|
+
const pluginActions = await discoverAllPluginActions({
|
|
1455
|
+
projectRoot: this.projectRoot,
|
|
1456
|
+
registry: actionRegistry,
|
|
1457
|
+
verbose: true,
|
|
1458
|
+
viteServer: this.viteServer ?? void 0
|
|
1459
|
+
});
|
|
1460
|
+
totalActions += pluginActions.length;
|
|
1461
|
+
} catch (error) {
|
|
1462
|
+
console.error("[Actions] Failed to auto-discover plugin actions:", error);
|
|
1463
|
+
}
|
|
1464
|
+
if (totalActions > 0) {
|
|
1465
|
+
console.log(`[Actions] Auto-registered ${totalActions} action(s) total`);
|
|
951
1466
|
}
|
|
952
1467
|
}
|
|
953
1468
|
/**
|
|
@@ -980,31 +1495,48 @@ class ServiceLifecycleManager {
|
|
|
980
1495
|
* Hot reload: shutdown, clear caches, re-import, and re-initialize
|
|
981
1496
|
*/
|
|
982
1497
|
async reload() {
|
|
983
|
-
if (!this.initFilePath) {
|
|
984
|
-
console.log("[Services] No init file to reload");
|
|
985
|
-
return;
|
|
986
|
-
}
|
|
987
1498
|
console.log("[Services] Reloading services...");
|
|
988
1499
|
await this.shutdown();
|
|
989
1500
|
clearLifecycleCallbacks();
|
|
990
1501
|
clearServiceRegistry();
|
|
991
|
-
|
|
992
|
-
|
|
1502
|
+
clearClientInitData();
|
|
1503
|
+
actionRegistry.clear();
|
|
1504
|
+
if (this.projectInitFilePath && this.viteServer) {
|
|
1505
|
+
const moduleNode = this.viteServer.moduleGraph.getModuleById(this.projectInitFilePath);
|
|
993
1506
|
if (moduleNode) {
|
|
994
1507
|
await this.viteServer.moduleGraph.invalidateModule(moduleNode);
|
|
995
1508
|
}
|
|
996
|
-
} else {
|
|
997
|
-
delete require.cache[require.resolve(this.
|
|
1509
|
+
} else if (this.projectInitFilePath) {
|
|
1510
|
+
delete require.cache[require.resolve(this.projectInitFilePath)];
|
|
998
1511
|
}
|
|
999
1512
|
this.isInitialized = false;
|
|
1000
1513
|
await this.initialize();
|
|
1001
1514
|
console.log("[Services] Reload complete");
|
|
1002
1515
|
}
|
|
1003
1516
|
/**
|
|
1004
|
-
* Returns the path to the init file if found
|
|
1517
|
+
* Returns the path to the init file if found.
|
|
1005
1518
|
*/
|
|
1006
1519
|
getInitFilePath() {
|
|
1007
|
-
return this.
|
|
1520
|
+
return this.projectInitFilePath;
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Returns project init info for client-side embedding.
|
|
1524
|
+
*/
|
|
1525
|
+
getProjectInit() {
|
|
1526
|
+
if (!this.projectInitFilePath) {
|
|
1527
|
+
return null;
|
|
1528
|
+
}
|
|
1529
|
+
return {
|
|
1530
|
+
importPath: this.projectInitFilePath,
|
|
1531
|
+
initExport: "init"
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Returns the discovered plugins with init configurations.
|
|
1536
|
+
* Sorted by dependencies (plugins with no deps first).
|
|
1537
|
+
*/
|
|
1538
|
+
getPluginsWithInit() {
|
|
1539
|
+
return this.pluginsWithInit;
|
|
1008
1540
|
}
|
|
1009
1541
|
/**
|
|
1010
1542
|
* Checks if services are initialized
|
|
@@ -1013,6 +1545,212 @@ class ServiceLifecycleManager {
|
|
|
1013
1545
|
return this.isInitialized;
|
|
1014
1546
|
}
|
|
1015
1547
|
}
|
|
1548
|
+
function deepMergeViewStates(base, overlay, trackByMap, path2 = "") {
|
|
1549
|
+
if (!base && !overlay)
|
|
1550
|
+
return {};
|
|
1551
|
+
if (!base)
|
|
1552
|
+
return overlay || {};
|
|
1553
|
+
if (!overlay)
|
|
1554
|
+
return base || {};
|
|
1555
|
+
const result = {};
|
|
1556
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(overlay)]);
|
|
1557
|
+
for (const key of allKeys) {
|
|
1558
|
+
const baseValue = base[key];
|
|
1559
|
+
const overlayValue = overlay[key];
|
|
1560
|
+
const currentPath = path2 ? `${path2}.${key}` : key;
|
|
1561
|
+
if (overlayValue === void 0) {
|
|
1562
|
+
result[key] = baseValue;
|
|
1563
|
+
} else if (baseValue === void 0) {
|
|
1564
|
+
result[key] = overlayValue;
|
|
1565
|
+
} else if (Array.isArray(baseValue) && Array.isArray(overlayValue)) {
|
|
1566
|
+
const trackByField = trackByMap[currentPath];
|
|
1567
|
+
if (trackByField) {
|
|
1568
|
+
result[key] = mergeArraysByTrackBy(
|
|
1569
|
+
baseValue,
|
|
1570
|
+
overlayValue,
|
|
1571
|
+
trackByField,
|
|
1572
|
+
trackByMap,
|
|
1573
|
+
currentPath
|
|
1574
|
+
);
|
|
1575
|
+
} else {
|
|
1576
|
+
result[key] = overlayValue;
|
|
1577
|
+
}
|
|
1578
|
+
} else if (typeof baseValue === "object" && baseValue !== null && typeof overlayValue === "object" && overlayValue !== null && !Array.isArray(baseValue) && !Array.isArray(overlayValue)) {
|
|
1579
|
+
result[key] = deepMergeViewStates(baseValue, overlayValue, trackByMap, currentPath);
|
|
1580
|
+
} else {
|
|
1581
|
+
result[key] = overlayValue;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
return result;
|
|
1585
|
+
}
|
|
1586
|
+
function mergeArraysByTrackBy(baseArray, overlayArray, trackByField, trackByMap, arrayPath) {
|
|
1587
|
+
const baseByKey = /* @__PURE__ */ new Map();
|
|
1588
|
+
for (const item of baseArray) {
|
|
1589
|
+
const key = item[trackByField];
|
|
1590
|
+
if (key !== void 0 && key !== null) {
|
|
1591
|
+
if (baseByKey.has(key)) {
|
|
1592
|
+
console.warn(
|
|
1593
|
+
`Duplicate trackBy key [${key}] in base array at path [${arrayPath}]. This may cause incorrect merging.`
|
|
1594
|
+
);
|
|
1595
|
+
}
|
|
1596
|
+
baseByKey.set(key, item);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
const overlayByKey = /* @__PURE__ */ new Map();
|
|
1600
|
+
for (const item of overlayArray) {
|
|
1601
|
+
const key = item[trackByField];
|
|
1602
|
+
if (key !== void 0 && key !== null) {
|
|
1603
|
+
overlayByKey.set(key, item);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
return baseArray.map((baseItem) => {
|
|
1607
|
+
const key = baseItem[trackByField];
|
|
1608
|
+
if (key === void 0 || key === null) {
|
|
1609
|
+
return baseItem;
|
|
1610
|
+
}
|
|
1611
|
+
const overlayItem = overlayByKey.get(key);
|
|
1612
|
+
if (overlayItem) {
|
|
1613
|
+
return deepMergeViewStates(baseItem, overlayItem, trackByMap, arrayPath);
|
|
1614
|
+
} else {
|
|
1615
|
+
return baseItem;
|
|
1616
|
+
}
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
const ACTION_ENDPOINT_BASE = "/_jay/actions";
|
|
1620
|
+
function createActionRouter(options) {
|
|
1621
|
+
const registry = options?.registry ?? actionRegistry;
|
|
1622
|
+
return async (req, res) => {
|
|
1623
|
+
const actionName = req.path.slice(1);
|
|
1624
|
+
if (!actionName) {
|
|
1625
|
+
res.status(400).json({
|
|
1626
|
+
success: false,
|
|
1627
|
+
error: {
|
|
1628
|
+
code: "MISSING_ACTION_NAME",
|
|
1629
|
+
message: "Action name is required",
|
|
1630
|
+
isActionError: false
|
|
1631
|
+
}
|
|
1632
|
+
});
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
const action = registry.get(actionName);
|
|
1636
|
+
if (!action) {
|
|
1637
|
+
res.status(404).json({
|
|
1638
|
+
success: false,
|
|
1639
|
+
error: {
|
|
1640
|
+
code: "ACTION_NOT_FOUND",
|
|
1641
|
+
message: `Action '${actionName}' is not registered`,
|
|
1642
|
+
isActionError: false
|
|
1643
|
+
}
|
|
1644
|
+
});
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
const requestMethod = req.method.toUpperCase();
|
|
1648
|
+
if (requestMethod !== action.method) {
|
|
1649
|
+
res.status(405).json({
|
|
1650
|
+
success: false,
|
|
1651
|
+
error: {
|
|
1652
|
+
code: "METHOD_NOT_ALLOWED",
|
|
1653
|
+
message: `Action '${actionName}' expects ${action.method}, got ${requestMethod}`,
|
|
1654
|
+
isActionError: false
|
|
1655
|
+
}
|
|
1656
|
+
});
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
let input;
|
|
1660
|
+
try {
|
|
1661
|
+
if (requestMethod === "GET") {
|
|
1662
|
+
if (req.query._input) {
|
|
1663
|
+
input = JSON.parse(req.query._input);
|
|
1664
|
+
} else {
|
|
1665
|
+
input = { ...req.query };
|
|
1666
|
+
delete input._input;
|
|
1667
|
+
}
|
|
1668
|
+
} else {
|
|
1669
|
+
input = req.body;
|
|
1670
|
+
}
|
|
1671
|
+
} catch (parseError) {
|
|
1672
|
+
res.status(400).json({
|
|
1673
|
+
success: false,
|
|
1674
|
+
error: {
|
|
1675
|
+
code: "INVALID_INPUT",
|
|
1676
|
+
message: "Failed to parse request input",
|
|
1677
|
+
isActionError: false
|
|
1678
|
+
}
|
|
1679
|
+
});
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
const result = await registry.execute(actionName, input);
|
|
1683
|
+
if (requestMethod === "GET" && result.success) {
|
|
1684
|
+
const cacheHeaders = registry.getCacheHeaders(actionName);
|
|
1685
|
+
if (cacheHeaders) {
|
|
1686
|
+
res.set("Cache-Control", cacheHeaders);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
if (result.success) {
|
|
1690
|
+
res.status(200).json({
|
|
1691
|
+
success: true,
|
|
1692
|
+
data: result.data
|
|
1693
|
+
});
|
|
1694
|
+
} else {
|
|
1695
|
+
const statusCode = getStatusCodeForError(result.error.code, result.error.isActionError);
|
|
1696
|
+
res.status(statusCode).json({
|
|
1697
|
+
success: false,
|
|
1698
|
+
error: result.error
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
function getStatusCodeForError(code, isActionError) {
|
|
1704
|
+
if (isActionError) {
|
|
1705
|
+
return 422;
|
|
1706
|
+
}
|
|
1707
|
+
switch (code) {
|
|
1708
|
+
case "ACTION_NOT_FOUND":
|
|
1709
|
+
return 404;
|
|
1710
|
+
case "INVALID_INPUT":
|
|
1711
|
+
case "VALIDATION_ERROR":
|
|
1712
|
+
return 400;
|
|
1713
|
+
case "UNAUTHORIZED":
|
|
1714
|
+
return 401;
|
|
1715
|
+
case "FORBIDDEN":
|
|
1716
|
+
return 403;
|
|
1717
|
+
case "INTERNAL_ERROR":
|
|
1718
|
+
default:
|
|
1719
|
+
return 500;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
function actionBodyParser() {
|
|
1723
|
+
return (req, res, next) => {
|
|
1724
|
+
if (!req.path.startsWith(ACTION_ENDPOINT_BASE)) {
|
|
1725
|
+
next();
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
if (req.method === "GET") {
|
|
1729
|
+
next();
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
let body = "";
|
|
1733
|
+
req.setEncoding("utf8");
|
|
1734
|
+
req.on("data", (chunk) => {
|
|
1735
|
+
body += chunk;
|
|
1736
|
+
});
|
|
1737
|
+
req.on("end", () => {
|
|
1738
|
+
try {
|
|
1739
|
+
req.body = body ? JSON.parse(body) : {};
|
|
1740
|
+
next();
|
|
1741
|
+
} catch (e2) {
|
|
1742
|
+
res.status(400).json({
|
|
1743
|
+
success: false,
|
|
1744
|
+
error: {
|
|
1745
|
+
code: "INVALID_JSON",
|
|
1746
|
+
message: "Invalid JSON in request body",
|
|
1747
|
+
isActionError: false
|
|
1748
|
+
}
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
});
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1016
1754
|
async function initRoutes(pagesBaseFolder) {
|
|
1017
1755
|
return await scanRoutes(pagesBaseFolder, {
|
|
1018
1756
|
jayHtmlFilename: "page.jay-html",
|
|
@@ -1026,11 +1764,13 @@ function defaults(options) {
|
|
|
1026
1764
|
projectRootFolder,
|
|
1027
1765
|
options.pagesRootFolder || "./src/pages"
|
|
1028
1766
|
);
|
|
1767
|
+
const buildFolder = options.buildFolder || path__default.resolve(projectRootFolder, "./build");
|
|
1029
1768
|
const tsConfigFilePath = options.jayRollupConfig.tsConfigFilePath || path__default.resolve(projectRootFolder, "./tsconfig.json");
|
|
1030
1769
|
return {
|
|
1031
1770
|
publicBaseUrlPath,
|
|
1032
1771
|
pagesRootFolder,
|
|
1033
1772
|
projectRootFolder,
|
|
1773
|
+
buildFolder,
|
|
1034
1774
|
dontCacheSlowly: options.dontCacheSlowly,
|
|
1035
1775
|
jayRollupConfig: {
|
|
1036
1776
|
...options.jayRollupConfig || {},
|
|
@@ -1046,59 +1786,96 @@ function handleOtherResponseCodes(res, renderedResult) {
|
|
|
1046
1786
|
else
|
|
1047
1787
|
res.status(renderedResult.status).end("redirect to " + renderedResult.location);
|
|
1048
1788
|
}
|
|
1049
|
-
function
|
|
1050
|
-
const
|
|
1789
|
+
function filterPluginsForPage(allPluginClientInits, allPluginsWithInit, usedPackages) {
|
|
1790
|
+
const pluginsByPackage = /* @__PURE__ */ new Map();
|
|
1791
|
+
for (const plugin of allPluginsWithInit) {
|
|
1792
|
+
pluginsByPackage.set(plugin.packageName, plugin);
|
|
1793
|
+
}
|
|
1794
|
+
const expandedPackages = new Set(usedPackages);
|
|
1795
|
+
const toProcess = [...usedPackages];
|
|
1796
|
+
while (toProcess.length > 0) {
|
|
1797
|
+
const packageName = toProcess.pop();
|
|
1798
|
+
const plugin = pluginsByPackage.get(packageName);
|
|
1799
|
+
if (!plugin)
|
|
1800
|
+
continue;
|
|
1801
|
+
for (const dep of plugin.dependencies) {
|
|
1802
|
+
if (pluginsByPackage.has(dep) && !expandedPackages.has(dep)) {
|
|
1803
|
+
expandedPackages.add(dep);
|
|
1804
|
+
toProcess.push(dep);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
return allPluginClientInits.filter((plugin) => {
|
|
1809
|
+
const pluginInfo = allPluginsWithInit.find((p) => p.name === plugin.name);
|
|
1810
|
+
return pluginInfo && expandedPackages.has(pluginInfo.packageName);
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
function mkRoute(route, vite, slowlyPhase, options, slowRenderCache, projectInit, allPluginsWithInit = [], allPluginClientInits = []) {
|
|
1814
|
+
const routePath = routeToExpressRoute(route);
|
|
1051
1815
|
const handler = async (req, res) => {
|
|
1052
1816
|
try {
|
|
1053
1817
|
const url = req.originalUrl.replace(options.publicBaseUrlPath, "");
|
|
1054
|
-
const pageParams = req.params;
|
|
1818
|
+
const pageParams = { ...route.inferredParams, ...req.params };
|
|
1055
1819
|
const pageProps = {
|
|
1056
1820
|
language: "en",
|
|
1057
1821
|
url
|
|
1058
1822
|
};
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1823
|
+
const useSlowRenderCache = !options.dontCacheSlowly;
|
|
1824
|
+
let cachedEntry = useSlowRenderCache ? slowRenderCache.get(route.jayHtmlPath, pageParams) : void 0;
|
|
1825
|
+
if (cachedEntry) {
|
|
1826
|
+
try {
|
|
1827
|
+
await fs$1.access(cachedEntry.preRenderedPath);
|
|
1828
|
+
} catch {
|
|
1829
|
+
console.log(
|
|
1830
|
+
`[SlowRender] Cached file missing, rebuilding: ${cachedEntry.preRenderedPath}`
|
|
1831
|
+
);
|
|
1832
|
+
await slowRenderCache.invalidate(route.jayHtmlPath);
|
|
1833
|
+
cachedEntry = void 0;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
if (cachedEntry) {
|
|
1837
|
+
await handleCachedRequest(
|
|
1838
|
+
vite,
|
|
1839
|
+
route,
|
|
1840
|
+
options,
|
|
1841
|
+
cachedEntry,
|
|
1068
1842
|
pageParams,
|
|
1069
1843
|
pageProps,
|
|
1070
|
-
|
|
1844
|
+
allPluginClientInits,
|
|
1845
|
+
allPluginsWithInit,
|
|
1846
|
+
projectInit,
|
|
1847
|
+
res,
|
|
1848
|
+
url
|
|
1849
|
+
);
|
|
1850
|
+
} else if (useSlowRenderCache) {
|
|
1851
|
+
await handlePreRenderRequest(
|
|
1852
|
+
vite,
|
|
1853
|
+
route,
|
|
1854
|
+
options,
|
|
1855
|
+
slowlyPhase,
|
|
1856
|
+
slowRenderCache,
|
|
1857
|
+
pageParams,
|
|
1858
|
+
pageProps,
|
|
1859
|
+
allPluginClientInits,
|
|
1860
|
+
allPluginsWithInit,
|
|
1861
|
+
projectInit,
|
|
1862
|
+
res,
|
|
1863
|
+
url
|
|
1071
1864
|
);
|
|
1072
|
-
if (renderedSlowly.kind === "PartialRender") {
|
|
1073
|
-
const renderedFast = await renderFastChangingData(
|
|
1074
|
-
pageParams,
|
|
1075
|
-
pageProps,
|
|
1076
|
-
renderedSlowly.carryForward,
|
|
1077
|
-
pageParts.val
|
|
1078
|
-
);
|
|
1079
|
-
if (renderedFast.kind === "PartialRender") {
|
|
1080
|
-
viewState = { ...renderedSlowly.rendered, ...renderedFast.rendered };
|
|
1081
|
-
carryForward = renderedFast.carryForward;
|
|
1082
|
-
const pageHtml = generateClientScript(
|
|
1083
|
-
viewState,
|
|
1084
|
-
carryForward,
|
|
1085
|
-
pageParts.val,
|
|
1086
|
-
route.jayHtmlPath
|
|
1087
|
-
);
|
|
1088
|
-
const compiledPageHtml = await vite.transformIndexHtml(
|
|
1089
|
-
!!url ? url : "/",
|
|
1090
|
-
pageHtml
|
|
1091
|
-
);
|
|
1092
|
-
res.status(200).set({ "Content-Type": "text/html" }).send(compiledPageHtml);
|
|
1093
|
-
} else {
|
|
1094
|
-
handleOtherResponseCodes(res, renderedFast);
|
|
1095
|
-
}
|
|
1096
|
-
} else if (renderedSlowly.kind === "ClientError") {
|
|
1097
|
-
handleOtherResponseCodes(res, renderedSlowly);
|
|
1098
|
-
}
|
|
1099
1865
|
} else {
|
|
1100
|
-
|
|
1101
|
-
|
|
1866
|
+
await handleDirectRequest(
|
|
1867
|
+
vite,
|
|
1868
|
+
route,
|
|
1869
|
+
options,
|
|
1870
|
+
slowlyPhase,
|
|
1871
|
+
pageParams,
|
|
1872
|
+
pageProps,
|
|
1873
|
+
allPluginClientInits,
|
|
1874
|
+
allPluginsWithInit,
|
|
1875
|
+
projectInit,
|
|
1876
|
+
res,
|
|
1877
|
+
url
|
|
1878
|
+
);
|
|
1102
1879
|
}
|
|
1103
1880
|
} catch (e2) {
|
|
1104
1881
|
vite?.ssrFixStacktrace(e2);
|
|
@@ -1106,13 +1883,262 @@ function mkRoute(route, vite, slowlyPhase, options) {
|
|
|
1106
1883
|
res.status(500).end(e2.stack);
|
|
1107
1884
|
}
|
|
1108
1885
|
};
|
|
1109
|
-
return { path:
|
|
1886
|
+
return { path: routePath, handler, fsRoute: route };
|
|
1887
|
+
}
|
|
1888
|
+
async function handleCachedRequest(vite, route, options, cachedEntry, pageParams, pageProps, allPluginClientInits, allPluginsWithInit, projectInit, res, url) {
|
|
1889
|
+
const pagePartsResult = await loadPageParts(
|
|
1890
|
+
vite,
|
|
1891
|
+
route,
|
|
1892
|
+
options.pagesRootFolder,
|
|
1893
|
+
options.projectRootFolder,
|
|
1894
|
+
options.jayRollupConfig,
|
|
1895
|
+
{ preRenderedPath: cachedEntry.preRenderedPath }
|
|
1896
|
+
);
|
|
1897
|
+
if (!pagePartsResult.val) {
|
|
1898
|
+
console.log(pagePartsResult.validations.join("\n"));
|
|
1899
|
+
res.status(500).end(pagePartsResult.validations.join("\n"));
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
const { parts: pageParts, clientTrackByMap, usedPackages } = pagePartsResult.val;
|
|
1903
|
+
const pluginsForPage = filterPluginsForPage(
|
|
1904
|
+
allPluginClientInits,
|
|
1905
|
+
allPluginsWithInit,
|
|
1906
|
+
usedPackages
|
|
1907
|
+
);
|
|
1908
|
+
const renderedFast = await renderFastChangingData(
|
|
1909
|
+
pageParams,
|
|
1910
|
+
pageProps,
|
|
1911
|
+
cachedEntry.carryForward,
|
|
1912
|
+
pageParts
|
|
1913
|
+
);
|
|
1914
|
+
if (renderedFast.kind !== "PhaseOutput") {
|
|
1915
|
+
handleOtherResponseCodes(res, renderedFast);
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
await sendResponse(
|
|
1919
|
+
vite,
|
|
1920
|
+
res,
|
|
1921
|
+
url,
|
|
1922
|
+
cachedEntry.preRenderedPath,
|
|
1923
|
+
pageParts,
|
|
1924
|
+
renderedFast.rendered,
|
|
1925
|
+
renderedFast.carryForward,
|
|
1926
|
+
clientTrackByMap,
|
|
1927
|
+
projectInit,
|
|
1928
|
+
pluginsForPage,
|
|
1929
|
+
options,
|
|
1930
|
+
cachedEntry.slowViewState
|
|
1931
|
+
);
|
|
1932
|
+
}
|
|
1933
|
+
async function handlePreRenderRequest(vite, route, options, slowlyPhase, slowRenderCache, pageParams, pageProps, allPluginClientInits, allPluginsWithInit, projectInit, res, url) {
|
|
1934
|
+
const initialPartsResult = await loadPageParts(
|
|
1935
|
+
vite,
|
|
1936
|
+
route,
|
|
1937
|
+
options.pagesRootFolder,
|
|
1938
|
+
options.projectRootFolder,
|
|
1939
|
+
options.jayRollupConfig
|
|
1940
|
+
);
|
|
1941
|
+
if (!initialPartsResult.val) {
|
|
1942
|
+
console.log(initialPartsResult.validations.join("\n"));
|
|
1943
|
+
res.status(500).end(initialPartsResult.validations.join("\n"));
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
const renderedSlowly = await slowlyPhase.runSlowlyForPage(
|
|
1947
|
+
pageParams,
|
|
1948
|
+
pageProps,
|
|
1949
|
+
initialPartsResult.val.parts
|
|
1950
|
+
);
|
|
1951
|
+
if (renderedSlowly.kind !== "PhaseOutput") {
|
|
1952
|
+
if (renderedSlowly.kind === "ClientError") {
|
|
1953
|
+
handleOtherResponseCodes(res, renderedSlowly);
|
|
1954
|
+
}
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
const preRenderedContent = await preRenderJayHtml(route, renderedSlowly.rendered);
|
|
1958
|
+
if (!preRenderedContent) {
|
|
1959
|
+
res.status(500).end("Failed to pre-render jay-html");
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
const preRenderedPath = await slowRenderCache.set(
|
|
1963
|
+
route.jayHtmlPath,
|
|
1964
|
+
pageParams,
|
|
1965
|
+
preRenderedContent,
|
|
1966
|
+
renderedSlowly.rendered,
|
|
1967
|
+
renderedSlowly.carryForward
|
|
1968
|
+
);
|
|
1969
|
+
console.log(`[SlowRender] Cached pre-rendered jay-html at ${preRenderedPath}`);
|
|
1970
|
+
const pagePartsResult = await loadPageParts(
|
|
1971
|
+
vite,
|
|
1972
|
+
route,
|
|
1973
|
+
options.pagesRootFolder,
|
|
1974
|
+
options.projectRootFolder,
|
|
1975
|
+
options.jayRollupConfig,
|
|
1976
|
+
{ preRenderedPath }
|
|
1977
|
+
);
|
|
1978
|
+
if (!pagePartsResult.val) {
|
|
1979
|
+
console.log(pagePartsResult.validations.join("\n"));
|
|
1980
|
+
res.status(500).end(pagePartsResult.validations.join("\n"));
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
const { parts: pageParts, clientTrackByMap, usedPackages } = pagePartsResult.val;
|
|
1984
|
+
const pluginsForPage = filterPluginsForPage(
|
|
1985
|
+
allPluginClientInits,
|
|
1986
|
+
allPluginsWithInit,
|
|
1987
|
+
usedPackages
|
|
1988
|
+
);
|
|
1989
|
+
const renderedFast = await renderFastChangingData(
|
|
1990
|
+
pageParams,
|
|
1991
|
+
pageProps,
|
|
1992
|
+
renderedSlowly.carryForward,
|
|
1993
|
+
pageParts
|
|
1994
|
+
);
|
|
1995
|
+
if (renderedFast.kind !== "PhaseOutput") {
|
|
1996
|
+
handleOtherResponseCodes(res, renderedFast);
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
await sendResponse(
|
|
2000
|
+
vite,
|
|
2001
|
+
res,
|
|
2002
|
+
url,
|
|
2003
|
+
preRenderedPath,
|
|
2004
|
+
pageParts,
|
|
2005
|
+
renderedFast.rendered,
|
|
2006
|
+
renderedFast.carryForward,
|
|
2007
|
+
clientTrackByMap,
|
|
2008
|
+
projectInit,
|
|
2009
|
+
pluginsForPage,
|
|
2010
|
+
options,
|
|
2011
|
+
renderedSlowly.rendered
|
|
2012
|
+
);
|
|
2013
|
+
}
|
|
2014
|
+
async function handleDirectRequest(vite, route, options, slowlyPhase, pageParams, pageProps, allPluginClientInits, allPluginsWithInit, projectInit, res, url) {
|
|
2015
|
+
const pagePartsResult = await loadPageParts(
|
|
2016
|
+
vite,
|
|
2017
|
+
route,
|
|
2018
|
+
options.pagesRootFolder,
|
|
2019
|
+
options.projectRootFolder,
|
|
2020
|
+
options.jayRollupConfig
|
|
2021
|
+
);
|
|
2022
|
+
if (!pagePartsResult.val) {
|
|
2023
|
+
console.log(pagePartsResult.validations.join("\n"));
|
|
2024
|
+
res.status(500).end(pagePartsResult.validations.join("\n"));
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
const {
|
|
2028
|
+
parts: pageParts,
|
|
2029
|
+
serverTrackByMap,
|
|
2030
|
+
clientTrackByMap,
|
|
2031
|
+
usedPackages
|
|
2032
|
+
} = pagePartsResult.val;
|
|
2033
|
+
const pluginsForPage = filterPluginsForPage(
|
|
2034
|
+
allPluginClientInits,
|
|
2035
|
+
allPluginsWithInit,
|
|
2036
|
+
usedPackages
|
|
2037
|
+
);
|
|
2038
|
+
const renderedSlowly = await slowlyPhase.runSlowlyForPage(pageParams, pageProps, pageParts);
|
|
2039
|
+
if (renderedSlowly.kind !== "PhaseOutput") {
|
|
2040
|
+
if (renderedSlowly.kind === "ClientError") {
|
|
2041
|
+
handleOtherResponseCodes(res, renderedSlowly);
|
|
2042
|
+
}
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
const renderedFast = await renderFastChangingData(
|
|
2046
|
+
pageParams,
|
|
2047
|
+
pageProps,
|
|
2048
|
+
renderedSlowly.carryForward,
|
|
2049
|
+
pageParts
|
|
2050
|
+
);
|
|
2051
|
+
if (renderedFast.kind !== "PhaseOutput") {
|
|
2052
|
+
handleOtherResponseCodes(res, renderedFast);
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
let viewState;
|
|
2056
|
+
if (serverTrackByMap && Object.keys(serverTrackByMap).length > 0) {
|
|
2057
|
+
viewState = deepMergeViewStates(
|
|
2058
|
+
renderedSlowly.rendered,
|
|
2059
|
+
renderedFast.rendered,
|
|
2060
|
+
serverTrackByMap
|
|
2061
|
+
);
|
|
2062
|
+
} else {
|
|
2063
|
+
viewState = { ...renderedSlowly.rendered, ...renderedFast.rendered };
|
|
2064
|
+
}
|
|
2065
|
+
await sendResponse(
|
|
2066
|
+
vite,
|
|
2067
|
+
res,
|
|
2068
|
+
url,
|
|
2069
|
+
route.jayHtmlPath,
|
|
2070
|
+
pageParts,
|
|
2071
|
+
viewState,
|
|
2072
|
+
renderedFast.carryForward,
|
|
2073
|
+
clientTrackByMap,
|
|
2074
|
+
projectInit,
|
|
2075
|
+
pluginsForPage,
|
|
2076
|
+
options
|
|
2077
|
+
);
|
|
2078
|
+
}
|
|
2079
|
+
async function sendResponse(vite, res, url, jayHtmlPath, pageParts, viewState, carryForward, clientTrackByMap, projectInit, pluginsForPage, options, slowViewState) {
|
|
2080
|
+
const pageHtml = generateClientScript(
|
|
2081
|
+
viewState,
|
|
2082
|
+
carryForward,
|
|
2083
|
+
pageParts,
|
|
2084
|
+
jayHtmlPath,
|
|
2085
|
+
clientTrackByMap,
|
|
2086
|
+
getClientInitData(),
|
|
2087
|
+
projectInit,
|
|
2088
|
+
pluginsForPage,
|
|
2089
|
+
{
|
|
2090
|
+
enableAutomation: !options.disableAutomation,
|
|
2091
|
+
slowViewState
|
|
2092
|
+
}
|
|
2093
|
+
);
|
|
2094
|
+
const compiledPageHtml = await vite.transformIndexHtml(!!url ? url : "/", pageHtml);
|
|
2095
|
+
res.status(200).set({ "Content-Type": "text/html" }).send(compiledPageHtml);
|
|
2096
|
+
}
|
|
2097
|
+
async function preRenderJayHtml(route, slowViewState) {
|
|
2098
|
+
const jayHtmlContent = await fs$1.readFile(route.jayHtmlPath, "utf-8");
|
|
2099
|
+
const contractPath = route.jayHtmlPath.replace(".jay-html", ".jay-contract");
|
|
2100
|
+
let contract;
|
|
2101
|
+
try {
|
|
2102
|
+
const contractContent = await fs$1.readFile(contractPath, "utf-8");
|
|
2103
|
+
const parseResult = parseContract(contractContent, path__default.basename(contractPath));
|
|
2104
|
+
if (parseResult.val) {
|
|
2105
|
+
contract = parseResult.val;
|
|
2106
|
+
} else if (parseResult.validations.length > 0) {
|
|
2107
|
+
console.error(
|
|
2108
|
+
`[SlowRender] Contract parse error for ${contractPath}:`,
|
|
2109
|
+
parseResult.validations
|
|
2110
|
+
);
|
|
2111
|
+
return void 0;
|
|
2112
|
+
}
|
|
2113
|
+
} catch (error) {
|
|
2114
|
+
if (error.code !== "ENOENT") {
|
|
2115
|
+
console.error(`[SlowRender] Error reading contract ${contractPath}:`, error);
|
|
2116
|
+
return void 0;
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
const result = slowRenderTransform({
|
|
2120
|
+
jayHtmlContent,
|
|
2121
|
+
slowViewState,
|
|
2122
|
+
contract,
|
|
2123
|
+
sourceDir: path__default.dirname(route.jayHtmlPath)
|
|
2124
|
+
});
|
|
2125
|
+
if (result.val) {
|
|
2126
|
+
return result.val.preRenderedJayHtml;
|
|
2127
|
+
}
|
|
2128
|
+
if (result.validations.length > 0) {
|
|
2129
|
+
console.error(
|
|
2130
|
+
`[SlowRender] Transform failed for ${route.jayHtmlPath}:`,
|
|
2131
|
+
result.validations
|
|
2132
|
+
);
|
|
2133
|
+
}
|
|
2134
|
+
return void 0;
|
|
1110
2135
|
}
|
|
1111
2136
|
async function mkDevServer(options) {
|
|
1112
2137
|
const {
|
|
1113
2138
|
publicBaseUrlPath,
|
|
1114
2139
|
pagesRootFolder,
|
|
1115
2140
|
projectRootFolder,
|
|
2141
|
+
buildFolder,
|
|
1116
2142
|
jayRollupConfig,
|
|
1117
2143
|
dontCacheSlowly
|
|
1118
2144
|
} = defaults(options);
|
|
@@ -1126,17 +2152,33 @@ async function mkDevServer(options) {
|
|
|
1126
2152
|
root: pagesRootFolder,
|
|
1127
2153
|
ssr: {
|
|
1128
2154
|
// Mark stack-server-runtime as external so Vite uses Node's require
|
|
1129
|
-
// This ensures
|
|
2155
|
+
// This ensures lib/init.ts and dev-server share the same module instance
|
|
1130
2156
|
external: ["@jay-framework/stack-server-runtime"]
|
|
1131
2157
|
}
|
|
1132
2158
|
});
|
|
1133
2159
|
lifecycleManager.setViteServer(vite);
|
|
1134
2160
|
await lifecycleManager.initialize();
|
|
1135
2161
|
setupServiceHotReload(vite, lifecycleManager);
|
|
2162
|
+
setupActionRouter(vite);
|
|
1136
2163
|
const routes = await initRoutes(pagesRootFolder);
|
|
1137
2164
|
const slowlyPhase = new DevSlowlyChangingPhase(dontCacheSlowly);
|
|
2165
|
+
const slowRenderCacheDir = path__default.join(buildFolder, "slow-render-cache");
|
|
2166
|
+
const slowRenderCache = new SlowRenderCache(slowRenderCacheDir, pagesRootFolder);
|
|
2167
|
+
setupSlowRenderCacheInvalidation(vite, slowRenderCache, pagesRootFolder);
|
|
2168
|
+
const projectInit = lifecycleManager.getProjectInit() ?? void 0;
|
|
2169
|
+
const pluginsWithInit = lifecycleManager.getPluginsWithInit();
|
|
2170
|
+
const pluginClientInits = preparePluginClientInits(pluginsWithInit);
|
|
1138
2171
|
const devServerRoutes = routes.map(
|
|
1139
|
-
(route) => mkRoute(
|
|
2172
|
+
(route) => mkRoute(
|
|
2173
|
+
route,
|
|
2174
|
+
vite,
|
|
2175
|
+
slowlyPhase,
|
|
2176
|
+
options,
|
|
2177
|
+
slowRenderCache,
|
|
2178
|
+
projectInit,
|
|
2179
|
+
pluginsWithInit,
|
|
2180
|
+
pluginClientInits
|
|
2181
|
+
)
|
|
1140
2182
|
);
|
|
1141
2183
|
return {
|
|
1142
2184
|
server: vite.middlewares,
|
|
@@ -1163,7 +2205,7 @@ function setupServiceHotReload(vite, lifecycleManager) {
|
|
|
1163
2205
|
vite.watcher.add(initFilePath);
|
|
1164
2206
|
vite.watcher.on("change", async (changedPath) => {
|
|
1165
2207
|
if (changedPath === initFilePath) {
|
|
1166
|
-
console.log("[Services]
|
|
2208
|
+
console.log("[Services] lib/init.ts changed, reloading services...");
|
|
1167
2209
|
try {
|
|
1168
2210
|
await lifecycleManager.reload();
|
|
1169
2211
|
vite.ws.send({
|
|
@@ -1176,6 +2218,42 @@ function setupServiceHotReload(vite, lifecycleManager) {
|
|
|
1176
2218
|
}
|
|
1177
2219
|
});
|
|
1178
2220
|
}
|
|
2221
|
+
function setupActionRouter(vite) {
|
|
2222
|
+
vite.middlewares.use(actionBodyParser());
|
|
2223
|
+
vite.middlewares.use(ACTION_ENDPOINT_BASE, createActionRouter());
|
|
2224
|
+
console.log(`[Actions] Action router mounted at ${ACTION_ENDPOINT_BASE}`);
|
|
2225
|
+
}
|
|
2226
|
+
function setupSlowRenderCacheInvalidation(vite, cache, pagesRootFolder) {
|
|
2227
|
+
vite.watcher.on("change", (changedPath) => {
|
|
2228
|
+
if (!changedPath.startsWith(pagesRootFolder)) {
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
if (changedPath.endsWith(".jay-html")) {
|
|
2232
|
+
cache.invalidate(changedPath).then(() => {
|
|
2233
|
+
console.log(`[SlowRender] Cache invalidated for ${changedPath}`);
|
|
2234
|
+
});
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
if (changedPath.endsWith("page.ts")) {
|
|
2238
|
+
const dir = path__default.dirname(changedPath);
|
|
2239
|
+
const jayHtmlPath = path__default.join(dir, "page.jay-html");
|
|
2240
|
+
cache.invalidate(jayHtmlPath).then(() => {
|
|
2241
|
+
console.log(`[SlowRender] Cache invalidated for ${jayHtmlPath} (page.ts changed)`);
|
|
2242
|
+
});
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
if (changedPath.endsWith(".jay-contract")) {
|
|
2246
|
+
const jayHtmlPath = changedPath.replace(".jay-contract", ".jay-html");
|
|
2247
|
+
cache.invalidate(jayHtmlPath).then(() => {
|
|
2248
|
+
console.log(`[SlowRender] Cache invalidated for ${jayHtmlPath} (contract changed)`);
|
|
2249
|
+
});
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
1179
2254
|
export {
|
|
2255
|
+
ACTION_ENDPOINT_BASE,
|
|
2256
|
+
actionBodyParser,
|
|
2257
|
+
createActionRouter,
|
|
1180
2258
|
mkDevServer
|
|
1181
2259
|
};
|