@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.
Files changed (3) hide show
  1. package/dist/index.d.ts +91 -9
  2. package/dist/index.js +1212 -134
  3. 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$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$1(node))
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 SERVER_METHODS = /* @__PURE__ */ new Set([
604
+ const COMPONENT_SERVER_METHODS = /* @__PURE__ */ new Set([
603
605
  "withServices",
604
606
  "withLoadParams",
605
607
  "withSlowlyRender",
606
608
  "withFastRender"
607
609
  ]);
608
- const CLIENT_METHODS = /* @__PURE__ */ new Set([
609
- "withInteractive",
610
- "withContexts"
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
- return environment === "client" && SERVER_METHODS.has(methodName) || environment === "server" && CLIENT_METHODS.has(methodName);
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] === "makeJayStackComponent" && isImportModuleVariableRoot(flattened.root) && isStringLiteral(flattened.root.module) && flattened.root.module.text === "@jay-framework/fullstack-component")
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: isFunctionDeclaration$1,
674
- isVariableStatement: isVariableStatement$1,
675
- isInterfaceDeclaration: isInterfaceDeclaration$1,
676
- isTypeAliasDeclaration: isTypeAliasDeclaration$1,
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, bindingResolver) {
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$1(statement) && statement.name) {
752
+ if (isFunctionDeclaration(statement) && statement.name) {
743
753
  return statement.name.text;
744
754
  }
745
- if (isVariableStatement$1(statement)) {
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$1(statement) && statement.name) {
761
+ if (isInterfaceDeclaration(statement) && statement.name) {
752
762
  return statement.name.text;
753
763
  }
754
- if (isTypeAliasDeclaration$1(statement) && statement.name) {
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, removedVariables } = findBuilderMethodsToRemove(
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(sourceFile, transformVisitor, context);
824
- new SourceFileBindingResolver(transformedSourceFile);
825
- const { statementsToRemove, unusedImports } = analyzeUnusedStatements(
826
- transformedSourceFile
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
- function jayStackCompiler(jayOptions = {}) {
867
- return [
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
- const isClientBuild = id.includes("?jay-client");
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 environment = isClientBuild ? "client" : "server";
880
- if (!id.endsWith(".ts") && !id.includes(".ts?")) {
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: Jay runtime compilation (existing plugin)
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
- __publicField(this, "initFilePath", null);
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 jay.init.ts (or .js) file in the source directory.
911
- * Looks in: {projectRoot}/{sourceBase}/jay.init.{ts,js,mts,mjs}
1370
+ * Finds the project init file using makeJayInit pattern.
1371
+ * Looks in: {projectRoot}/{sourceBase}/init.{ts,js}
912
1372
  */
913
- findInitFile() {
914
- const extensions = [".ts", ".js", ".mts", ".mjs"];
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, baseFilename + ext);
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 loading and executing jay.init.ts
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.initFilePath = this.findInitFile();
933
- if (!this.initFilePath) {
934
- console.log("[Services] No jay.init.ts found in src/, skipping service initialization");
935
- return;
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
- console.log(`[Services] Loading initialization file: ${this.initFilePath}`);
938
- try {
939
- if (this.viteServer) {
940
- await this.viteServer.ssrLoadModule(this.initFilePath);
941
- } else {
942
- const fileUrl = pathToFileURL(this.initFilePath).href;
943
- await import(fileUrl);
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
- await runInitCallbacks();
946
- this.isInitialized = true;
947
- console.log("[Services] Initialization complete");
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("[Services] Failed to initialize:", error);
950
- throw error;
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
- if (this.viteServer) {
992
- const moduleNode = this.viteServer.moduleGraph.getModuleById(this.initFilePath);
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.initFilePath)];
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.initFilePath;
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 mkRoute(route, vite, slowlyPhase, options) {
1050
- const path2 = routeToExpressRoute(route);
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
- let viewState, carryForward;
1060
- const pageParts = await loadPageParts(
1061
- vite,
1062
- route,
1063
- options.pagesRootFolder,
1064
- options.jayRollupConfig
1065
- );
1066
- if (pageParts.val) {
1067
- const renderedSlowly = await slowlyPhase.runSlowlyForPage(
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
- pageParts.val
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
- console.log(pageParts.validations.join("\n"));
1101
- res.status(500).end(pageParts.validations.join("\n"));
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: path2, handler, fsRoute: route };
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 jay.init.ts and dev-server share the same module instance
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(route, vite, slowlyPhase, options)
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] jay.init.ts changed, reloading 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
  };