@jay-framework/dev-server 0.9.0 → 0.10.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 +79 -9
  2. package/dist/index.js +741 -102
  3. package/package.json +13 -12
package/dist/index.d.ts CHANGED
@@ -2,6 +2,8 @@ import { ViteDevServer, Connect } from 'vite';
2
2
  import { JayRoute } from '@jay-framework/stack-route-scanner';
3
3
  import { RequestHandler } from 'express-serve-static-core';
4
4
  import { JayRollupConfig } from '@jay-framework/rollup-plugin';
5
+ import { ProjectClientInitInfo, PluginWithInit, ActionRegistry } from '@jay-framework/stack-server-runtime';
6
+ import { RequestHandler as RequestHandler$1 } from 'express';
5
7
 
6
8
  interface DevServerOptions {
7
9
  publicBaseUrlPath?: string;
@@ -14,30 +16,40 @@ interface DevServerOptions {
14
16
  /**
15
17
  * Service lifecycle management for the Jay Stack dev-server.
16
18
  *
17
- * Handles loading jay.init.ts, running init/shutdown callbacks,
18
- * hot reloading services, and graceful shutdown.
19
+ * Handles loading lib/init.ts, running init/shutdown callbacks,
20
+ * hot reloading services, graceful shutdown, and action auto-discovery.
19
21
  */
20
22
 
21
23
  declare class ServiceLifecycleManager {
22
24
  private projectRoot;
23
25
  private sourceBase;
24
- private initFilePath;
26
+ /** Path to project's lib/init.ts (makeJayInit pattern) */
27
+ private projectInitFilePath;
25
28
  private isInitialized;
26
29
  private viteServer;
30
+ private pluginsWithInit;
27
31
  constructor(projectRoot: string, sourceBase?: string);
28
32
  /**
29
33
  * Set the Vite server instance for SSR module loading
30
34
  */
31
35
  setViteServer(viteServer: ViteDevServer): void;
32
36
  /**
33
- * Finds the jay.init.ts (or .js) file in the source directory.
34
- * Looks in: {projectRoot}/{sourceBase}/jay.init.{ts,js,mts,mjs}
37
+ * Finds the project init file using makeJayInit pattern.
38
+ * Looks in: {projectRoot}/{sourceBase}/init.{ts,js}
35
39
  */
36
- private findInitFile;
40
+ private findProjectInitFile;
37
41
  /**
38
- * Initializes services by loading and executing jay.init.ts
42
+ * Initializes services by:
43
+ * 1. Discovering and executing plugin server inits (in dependency order)
44
+ * 2. Loading and executing project lib/init.ts
45
+ * 3. Running all registered onInit callbacks
46
+ * 4. Auto-discovering and registering actions
39
47
  */
40
48
  initialize(): Promise<void>;
49
+ /**
50
+ * Auto-discovers and registers actions from project and plugins.
51
+ */
52
+ private discoverActions;
41
53
  /**
42
54
  * Shuts down services gracefully with timeout
43
55
  */
@@ -47,9 +59,18 @@ declare class ServiceLifecycleManager {
47
59
  */
48
60
  reload(): Promise<void>;
49
61
  /**
50
- * Returns the path to the init file if found
62
+ * Returns the path to the init file if found.
51
63
  */
52
64
  getInitFilePath(): string | null;
65
+ /**
66
+ * Returns project init info for client-side embedding.
67
+ */
68
+ getProjectInit(): ProjectClientInitInfo | null;
69
+ /**
70
+ * Returns the discovered plugins with init configurations.
71
+ * Sorted by dependencies (plugins with no deps first).
72
+ */
73
+ getPluginsWithInit(): PluginWithInit[];
53
74
  /**
54
75
  * Checks if services are initialized
55
76
  */
@@ -69,4 +90,53 @@ interface DevServer {
69
90
  }
70
91
  declare function mkDevServer(options: DevServerOptions): Promise<DevServer>;
71
92
 
72
- export { type DevServer, type DevServerOptions, type DevServerRoute, mkDevServer };
93
+ /**
94
+ * Action Router for Jay Stack dev server.
95
+ *
96
+ * Handles HTTP requests to /_jay/actions/:actionName
97
+ * and routes them to registered action handlers.
98
+ */
99
+
100
+ /**
101
+ * The base path for action endpoints.
102
+ */
103
+ declare const ACTION_ENDPOINT_BASE = "/_jay/actions";
104
+ /**
105
+ * Options for creating the action router.
106
+ */
107
+ interface ActionRouterOptions {
108
+ /**
109
+ * The action registry to use.
110
+ * Defaults to the global actionRegistry.
111
+ */
112
+ registry?: ActionRegistry;
113
+ }
114
+ /**
115
+ * Creates the action router middleware.
116
+ *
117
+ * Handles requests to /_jay/actions/:actionName
118
+ *
119
+ * For GET requests, input is parsed from query string.
120
+ * For POST/PUT/PATCH/DELETE, input is parsed from request body.
121
+ *
122
+ * @param options - Optional configuration including custom registry for testing
123
+ *
124
+ * @example
125
+ * ```typescript
126
+ * // In dev-server setup (uses default registry)
127
+ * const actionRouter = createActionRouter();
128
+ * app.use(ACTION_ENDPOINT_BASE, actionRouter);
129
+ *
130
+ * // For testing (uses isolated registry)
131
+ * const testRegistry = new ActionRegistry();
132
+ * const actionRouter = createActionRouter({ registry: testRegistry });
133
+ * ```
134
+ */
135
+ declare function createActionRouter(options?: ActionRouterOptions): RequestHandler$1;
136
+ /**
137
+ * Express middleware to parse JSON body for action requests.
138
+ * Should be applied before the action router.
139
+ */
140
+ declare function actionBodyParser(): RequestHandler$1;
141
+
142
+ export { ACTION_ENDPOINT_BASE, type ActionRouterOptions, type DevServer, type DevServerOptions, type DevServerRoute, actionBodyParser, createActionRouter, mkDevServer };
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ 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, preparePluginClientInits, loadPageParts, renderFastChangingData, getClientInitData, generateClientScript } 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";
@@ -365,7 +365,7 @@ const {
365
365
  SyntaxKind: SyntaxKind$4,
366
366
  isStringLiteral: isStringLiteral$7,
367
367
  visitNode: visitNode$4,
368
- isFunctionDeclaration: isFunctionDeclaration$1$1,
368
+ isFunctionDeclaration: isFunctionDeclaration$1,
369
369
  isVariableStatement: isVariableStatement$2,
370
370
  isImportDeclaration: isImportDeclaration$3,
371
371
  isBlock: isBlock$2,
@@ -464,7 +464,7 @@ class SourceFileBindingResolver {
464
464
  return node;
465
465
  };
466
466
  const visitor = (node) => {
467
- if (isFunctionDeclaration$1$1(node))
467
+ if (isFunctionDeclaration$1(node))
468
468
  nbResolversQueue[0].addFunctionDeclaration(node);
469
469
  if (isVariableStatement$2(node))
470
470
  nbResolversQueue[0].addVariableStatement(node);
@@ -599,18 +599,25 @@ function areVariableRootsEqual(root1, root2) {
599
599
  return false;
600
600
  }
601
601
  }
602
- const SERVER_METHODS = /* @__PURE__ */ new Set([
602
+ const COMPONENT_SERVER_METHODS = /* @__PURE__ */ new Set([
603
603
  "withServices",
604
604
  "withLoadParams",
605
605
  "withSlowlyRender",
606
606
  "withFastRender"
607
607
  ]);
608
- const CLIENT_METHODS = /* @__PURE__ */ new Set([
609
- "withInteractive",
610
- "withContexts"
611
- ]);
608
+ const COMPONENT_CLIENT_METHODS = /* @__PURE__ */ new Set(["withInteractive", "withContexts"]);
609
+ const INIT_SERVER_METHODS = /* @__PURE__ */ new Set(["withServer"]);
610
+ const INIT_CLIENT_METHODS = /* @__PURE__ */ new Set(["withClient"]);
612
611
  function shouldRemoveMethod(methodName, environment) {
613
- return environment === "client" && SERVER_METHODS.has(methodName) || environment === "server" && CLIENT_METHODS.has(methodName);
612
+ if (environment === "client" && COMPONENT_SERVER_METHODS.has(methodName))
613
+ return true;
614
+ if (environment === "server" && COMPONENT_CLIENT_METHODS.has(methodName))
615
+ return true;
616
+ if (environment === "client" && INIT_SERVER_METHODS.has(methodName))
617
+ return true;
618
+ if (environment === "server" && INIT_CLIENT_METHODS.has(methodName))
619
+ return true;
620
+ return false;
614
621
  }
615
622
  const { isCallExpression: isCallExpression$1, isPropertyAccessExpression: isPropertyAccessExpression$1, isIdentifier: isIdentifier$2, isStringLiteral } = c$1;
616
623
  function findBuilderMethodsToRemove(sourceFile, bindingResolver, environment) {
@@ -631,6 +638,7 @@ function findBuilderMethodsToRemove(sourceFile, bindingResolver, environment) {
631
638
  sourceFile.forEachChild(visit);
632
639
  return { callsToRemove, removedVariables };
633
640
  }
641
+ const JAY_BUILDER_FUNCTIONS = /* @__PURE__ */ new Set(["makeJayStackComponent", "makeJayInit"]);
634
642
  function isPartOfJayStackChain(callExpr, bindingResolver) {
635
643
  let current = callExpr.expression;
636
644
  while (true) {
@@ -640,7 +648,7 @@ function isPartOfJayStackChain(callExpr, bindingResolver) {
640
648
  if (isIdentifier$2(current.expression)) {
641
649
  const variable = bindingResolver.explain(current.expression);
642
650
  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")
651
+ 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
652
  return true;
645
653
  }
646
654
  if (isPropertyAccessExpression$1(current.expression)) {
@@ -670,15 +678,15 @@ function collectVariablesFromArguments(args, bindingResolver, variables) {
670
678
  const {
671
679
  isIdentifier: isIdentifier$1,
672
680
  isImportDeclaration: isImportDeclaration$1,
673
- isFunctionDeclaration: isFunctionDeclaration$1,
674
- isVariableStatement: isVariableStatement$1,
675
- isInterfaceDeclaration: isInterfaceDeclaration$1,
676
- isTypeAliasDeclaration: isTypeAliasDeclaration$1,
681
+ isFunctionDeclaration,
682
+ isVariableStatement,
683
+ isInterfaceDeclaration,
684
+ isTypeAliasDeclaration,
677
685
  isClassDeclaration,
678
686
  isEnumDeclaration,
679
687
  SyntaxKind
680
688
  } = c$1;
681
- function analyzeUnusedStatements(sourceFile, bindingResolver) {
689
+ function analyzeUnusedStatements(sourceFile) {
682
690
  const statementsToRemove = /* @__PURE__ */ new Set();
683
691
  const collectUsedIdentifiers = () => {
684
692
  const used = /* @__PURE__ */ new Set();
@@ -739,19 +747,19 @@ function isExportStatement(statement) {
739
747
  return false;
740
748
  }
741
749
  function getStatementDefinedName(statement) {
742
- if (isFunctionDeclaration$1(statement) && statement.name) {
750
+ if (isFunctionDeclaration(statement) && statement.name) {
743
751
  return statement.name.text;
744
752
  }
745
- if (isVariableStatement$1(statement)) {
753
+ if (isVariableStatement(statement)) {
746
754
  const firstDecl = statement.declarationList.declarations[0];
747
755
  if (firstDecl && isIdentifier$1(firstDecl.name)) {
748
756
  return firstDecl.name.text;
749
757
  }
750
758
  }
751
- if (isInterfaceDeclaration$1(statement) && statement.name) {
759
+ if (isInterfaceDeclaration(statement) && statement.name) {
752
760
  return statement.name.text;
753
761
  }
754
- if (isTypeAliasDeclaration$1(statement) && statement.name) {
762
+ if (isTypeAliasDeclaration(statement) && statement.name) {
755
763
  return statement.name.text;
756
764
  }
757
765
  if (isClassDeclaration(statement) && statement.name) {
@@ -771,19 +779,10 @@ const {
771
779
  isPropertyAccessExpression,
772
780
  isImportDeclaration,
773
781
  isNamedImports,
774
- isIdentifier,
775
- isFunctionDeclaration,
776
- isVariableStatement,
777
- isInterfaceDeclaration,
778
- isTypeAliasDeclaration
782
+ isIdentifier
779
783
  } = c$1;
780
784
  function transformJayStackBuilder(code, filePath, environment) {
781
- const sourceFile = createSourceFile(
782
- filePath,
783
- code,
784
- ScriptTarget.Latest,
785
- true
786
- );
785
+ const sourceFile = createSourceFile(filePath, code, ScriptTarget.Latest, true);
787
786
  const transformers = [mkTransformer(mkJayStackCodeSplitTransformer, { environment })];
788
787
  const printer = createPrinter();
789
788
  const result = c$1.transform(sourceFile, transformers);
@@ -804,11 +803,7 @@ function mkJayStackCodeSplitTransformer({
804
803
  environment
805
804
  }) {
806
805
  const bindingResolver = new SourceFileBindingResolver(sourceFile);
807
- const { callsToRemove, removedVariables } = findBuilderMethodsToRemove(
808
- sourceFile,
809
- bindingResolver,
810
- environment
811
- );
806
+ const { callsToRemove } = findBuilderMethodsToRemove(sourceFile, bindingResolver, environment);
812
807
  const transformVisitor = (node) => {
813
808
  if (isCallExpression(node) && isPropertyAccessExpression(node.expression)) {
814
809
  const variable = bindingResolver.explain(node.expression);
@@ -820,11 +815,12 @@ function mkJayStackCodeSplitTransformer({
820
815
  }
821
816
  return visitEachChild(node, transformVisitor, context);
822
817
  };
823
- let transformedSourceFile = visitEachChild(sourceFile, transformVisitor, context);
824
- new SourceFileBindingResolver(transformedSourceFile);
825
- const { statementsToRemove, unusedImports } = analyzeUnusedStatements(
826
- transformedSourceFile
818
+ let transformedSourceFile = visitEachChild(
819
+ sourceFile,
820
+ transformVisitor,
821
+ context
827
822
  );
823
+ const { statementsToRemove, unusedImports } = analyzeUnusedStatements(transformedSourceFile);
828
824
  const transformedStatements = transformedSourceFile.statements.map((statement) => {
829
825
  if (statementsToRemove.has(statement)) {
830
826
  return void 0;
@@ -854,32 +850,284 @@ function filterImportDeclaration(statement, unusedImports, factory) {
854
850
  importClause,
855
851
  importClause.isTypeOnly,
856
852
  importClause.name,
857
- factory.updateNamedImports(
858
- importClause.namedBindings,
859
- usedElements
860
- )
853
+ factory.updateNamedImports(importClause.namedBindings, usedElements)
861
854
  ),
862
855
  statement.moduleSpecifier,
863
856
  statement.assertClause
864
857
  );
865
858
  }
866
- function jayStackCompiler(jayOptions = {}) {
867
- return [
859
+ const actionMetadataCache = /* @__PURE__ */ new Map();
860
+ function clearActionMetadataCache() {
861
+ actionMetadataCache.clear();
862
+ }
863
+ function isActionImport(importSource) {
864
+ return importSource.includes(".actions") || importSource.includes("-actions") || importSource.includes("/actions/") || importSource.endsWith("/actions");
865
+ }
866
+ function extractActionsFromSource(sourceCode, filePath) {
867
+ const cached = actionMetadataCache.get(filePath);
868
+ if (cached) {
869
+ return cached;
870
+ }
871
+ const actions = [];
872
+ const sourceFile = c$1.createSourceFile(
873
+ filePath,
874
+ sourceCode,
875
+ c$1.ScriptTarget.Latest,
876
+ true
877
+ );
878
+ function visit(node) {
879
+ if (c$1.isVariableStatement(node)) {
880
+ const hasExport = node.modifiers?.some(
881
+ (m) => m.kind === c$1.SyntaxKind.ExportKeyword
882
+ );
883
+ if (!hasExport) {
884
+ c$1.forEachChild(node, visit);
885
+ return;
886
+ }
887
+ for (const decl of node.declarationList.declarations) {
888
+ if (!c$1.isIdentifier(decl.name) || !decl.initializer) {
889
+ continue;
890
+ }
891
+ const exportName = decl.name.text;
892
+ const actionMeta = extractActionFromExpression(decl.initializer);
893
+ if (actionMeta) {
894
+ actions.push({
895
+ ...actionMeta,
896
+ exportName
897
+ });
898
+ }
899
+ }
900
+ }
901
+ c$1.forEachChild(node, visit);
902
+ }
903
+ visit(sourceFile);
904
+ actionMetadataCache.set(filePath, actions);
905
+ return actions;
906
+ }
907
+ function extractActionFromExpression(node) {
908
+ let current = node;
909
+ let method = "POST";
910
+ let explicitMethod = null;
911
+ while (c$1.isCallExpression(current)) {
912
+ const expr = current.expression;
913
+ if (c$1.isPropertyAccessExpression(expr) && expr.name.text === "withMethod") {
914
+ const arg = current.arguments[0];
915
+ if (arg && c$1.isStringLiteral(arg)) {
916
+ explicitMethod = arg.text;
917
+ }
918
+ current = expr.expression;
919
+ continue;
920
+ }
921
+ if (c$1.isPropertyAccessExpression(expr) && ["withServices", "withCaching", "withHandler", "withTimeout"].includes(expr.name.text)) {
922
+ current = expr.expression;
923
+ continue;
924
+ }
925
+ if (c$1.isIdentifier(expr)) {
926
+ const funcName = expr.text;
927
+ if (funcName === "makeJayAction" || funcName === "makeJayQuery") {
928
+ const nameArg = current.arguments[0];
929
+ if (nameArg && c$1.isStringLiteral(nameArg)) {
930
+ method = funcName === "makeJayQuery" ? "GET" : "POST";
931
+ if (explicitMethod) {
932
+ method = explicitMethod;
933
+ }
934
+ return {
935
+ actionName: nameArg.text,
936
+ method
937
+ };
938
+ }
939
+ }
940
+ }
941
+ break;
942
+ }
943
+ return null;
944
+ }
945
+ const SERVER_ONLY_MODULES = /* @__PURE__ */ new Set([
946
+ "module",
947
+ // createRequire
948
+ "fs",
949
+ "path",
950
+ "node:fs",
951
+ "node:path",
952
+ "node:module",
953
+ "child_process",
954
+ "node:child_process",
955
+ "crypto",
956
+ "node:crypto"
957
+ ]);
958
+ const SERVER_ONLY_PACKAGE_PATTERNS = [
959
+ "@jay-framework/compiler-shared",
960
+ "@jay-framework/stack-server-runtime",
961
+ "yaml"
962
+ // Often used in server config
963
+ ];
964
+ function createImportChainTracker(options = {}) {
965
+ const {
966
+ verbose = false,
967
+ additionalServerModules = [],
968
+ additionalServerPatterns = []
969
+ } = options;
970
+ const importChain = /* @__PURE__ */ new Map();
971
+ const detectedServerModules = /* @__PURE__ */ new Set();
972
+ const serverOnlyModules = /* @__PURE__ */ new Set([...SERVER_ONLY_MODULES, ...additionalServerModules]);
973
+ const serverOnlyPatterns = [...SERVER_ONLY_PACKAGE_PATTERNS, ...additionalServerPatterns];
974
+ function isServerOnlyModule(id) {
975
+ if (serverOnlyModules.has(id)) {
976
+ return true;
977
+ }
978
+ for (const pattern of serverOnlyPatterns) {
979
+ if (id.includes(pattern)) {
980
+ return true;
981
+ }
982
+ }
983
+ return false;
984
+ }
985
+ function buildImportChain(moduleId) {
986
+ const chain = [moduleId];
987
+ let current = moduleId;
988
+ for (let i = 0; i < 100; i++) {
989
+ const importer = importChain.get(current);
990
+ if (!importer)
991
+ break;
992
+ chain.push(importer);
993
+ current = importer;
994
+ }
995
+ return chain.reverse();
996
+ }
997
+ function formatChain(chain) {
998
+ return chain.map((id, idx) => {
999
+ const indent = " ".repeat(idx);
1000
+ const shortId = shortenPath(id);
1001
+ return `${indent}${idx === 0 ? "" : "↳ "}${shortId}`;
1002
+ }).join("\n");
1003
+ }
1004
+ function shortenPath(id) {
1005
+ if (id.includes("node_modules")) {
1006
+ const parts = id.split("node_modules/");
1007
+ return parts[parts.length - 1];
1008
+ }
1009
+ const cwd = process.cwd();
1010
+ if (id.startsWith(cwd)) {
1011
+ return id.slice(cwd.length + 1);
1012
+ }
1013
+ return id;
1014
+ }
1015
+ return {
1016
+ name: "jay-stack:import-chain-tracker",
1017
+ enforce: "pre",
1018
+ buildStart() {
1019
+ importChain.clear();
1020
+ detectedServerModules.clear();
1021
+ if (verbose) {
1022
+ console.log("[import-chain-tracker] Build started, tracking imports...");
1023
+ }
1024
+ },
1025
+ resolveId(source, importer, options2) {
1026
+ if (options2?.ssr) {
1027
+ return null;
1028
+ }
1029
+ if (source.startsWith("\0")) {
1030
+ return null;
1031
+ }
1032
+ if (importer) {
1033
+ if (verbose) {
1034
+ console.log(
1035
+ `[import-chain-tracker] ${shortenPath(importer)} imports ${source}`
1036
+ );
1037
+ }
1038
+ }
1039
+ return null;
1040
+ },
1041
+ load(id) {
1042
+ return null;
1043
+ },
1044
+ transform(code, id, options2) {
1045
+ if (options2?.ssr) {
1046
+ return null;
1047
+ }
1048
+ if (isServerOnlyModule(id)) {
1049
+ detectedServerModules.add(id);
1050
+ const chain = buildImportChain(id);
1051
+ console.error(
1052
+ `
1053
+ [import-chain-tracker] ⚠️ Server-only module detected in client build!`
1054
+ );
1055
+ console.error(`Module: ${shortenPath(id)}`);
1056
+ console.error(`Import chain:`);
1057
+ console.error(formatChain(chain));
1058
+ console.error("");
1059
+ }
1060
+ const importRegex = /import\s+(?:(?:\{[^}]*\}|[^{}\s,]+)\s+from\s+)?['"]([^'"]+)['"]/g;
1061
+ let match;
1062
+ while ((match = importRegex.exec(code)) !== null) {
1063
+ const importedModule = match[1];
1064
+ importChain.set(importedModule, id);
1065
+ if (isServerOnlyModule(importedModule)) {
1066
+ if (!detectedServerModules.has(importedModule)) {
1067
+ detectedServerModules.add(importedModule);
1068
+ console.error(
1069
+ `
1070
+ [import-chain-tracker] ⚠️ Server-only import detected in client build!`
1071
+ );
1072
+ console.error(`Module "${importedModule}" imported by: ${shortenPath(id)}`);
1073
+ const chain = buildImportChain(id);
1074
+ chain.push(importedModule);
1075
+ console.error(`Import chain:`);
1076
+ console.error(formatChain(chain));
1077
+ console.error("");
1078
+ }
1079
+ }
1080
+ }
1081
+ return null;
1082
+ },
1083
+ buildEnd() {
1084
+ if (detectedServerModules.size > 0) {
1085
+ console.warn(
1086
+ `
1087
+ [import-chain-tracker] ⚠️ ${detectedServerModules.size} server-only module(s) detected during transform:`
1088
+ );
1089
+ for (const mod of detectedServerModules) {
1090
+ console.warn(` - ${mod}`);
1091
+ }
1092
+ console.warn(
1093
+ "\nNote: These may be stripped by the code-split transform if only used in .withServer()."
1094
+ );
1095
+ console.warn(
1096
+ 'If build fails with "not exported" errors, check the import chains above.\n'
1097
+ );
1098
+ } else if (verbose) {
1099
+ console.log(
1100
+ "[import-chain-tracker] ✅ No server-only modules detected in client build"
1101
+ );
1102
+ }
1103
+ }
1104
+ };
1105
+ }
1106
+ function jayStackCompiler(options = {}) {
1107
+ const { trackImports, ...jayOptions } = options;
1108
+ const moduleCache = /* @__PURE__ */ new Map();
1109
+ const shouldTrackImports = trackImports || process.env.DEBUG_IMPORTS === "1";
1110
+ const trackerOptions = typeof trackImports === "object" ? trackImports : { verbose: process.env.DEBUG_IMPORTS === "1" };
1111
+ const plugins = [];
1112
+ if (shouldTrackImports) {
1113
+ plugins.push(createImportChainTracker(trackerOptions));
1114
+ }
1115
+ plugins.push(
868
1116
  // First: Jay Stack code splitting transformation
869
1117
  {
870
1118
  name: "jay-stack:code-split",
871
1119
  enforce: "pre",
872
1120
  // 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) {
1121
+ transform(code, id, options2) {
1122
+ if (!id.endsWith(".ts") && !id.includes(".ts?")) {
877
1123
  return null;
878
1124
  }
879
- const environment = isClientBuild ? "client" : "server";
880
- if (!id.endsWith(".ts") && !id.includes(".ts?")) {
1125
+ const hasComponent = code.includes("makeJayStackComponent");
1126
+ const hasInit = code.includes("makeJayInit");
1127
+ if (!hasComponent && !hasInit) {
881
1128
  return null;
882
1129
  }
1130
+ const environment = options2?.ssr ? "server" : "client";
883
1131
  try {
884
1132
  return transformJayStackBuilder(code, id, environment);
885
1133
  } catch (error) {
@@ -888,15 +1136,93 @@ function jayStackCompiler(jayOptions = {}) {
888
1136
  }
889
1137
  }
890
1138
  },
891
- // Second: Jay runtime compilation (existing plugin)
1139
+ // Second: Action import transformation (client builds only)
1140
+ // Uses resolveId + load to replace action modules with virtual modules
1141
+ // containing createActionCaller calls BEFORE bundling happens.
1142
+ (() => {
1143
+ let isSSRBuild = false;
1144
+ return {
1145
+ name: "jay-stack:action-transform",
1146
+ enforce: "pre",
1147
+ // Track SSR mode from config
1148
+ configResolved(config) {
1149
+ isSSRBuild = config.build?.ssr ?? false;
1150
+ },
1151
+ buildStart() {
1152
+ clearActionMetadataCache();
1153
+ moduleCache.clear();
1154
+ },
1155
+ async resolveId(source, importer, options2) {
1156
+ if (options2?.ssr || isSSRBuild) {
1157
+ return null;
1158
+ }
1159
+ if (!isActionImport(source)) {
1160
+ return null;
1161
+ }
1162
+ if (!source.startsWith(".") || !importer) {
1163
+ return null;
1164
+ }
1165
+ const importerDir = path.dirname(importer);
1166
+ let resolvedPath = path.resolve(importerDir, source);
1167
+ if (!resolvedPath.endsWith(".ts") && !resolvedPath.endsWith(".js")) {
1168
+ if (fs.existsSync(resolvedPath + ".ts")) {
1169
+ resolvedPath += ".ts";
1170
+ } else if (fs.existsSync(resolvedPath + ".js")) {
1171
+ resolvedPath += ".js";
1172
+ } else {
1173
+ return null;
1174
+ }
1175
+ }
1176
+ return `\0jay-action:${resolvedPath}`;
1177
+ },
1178
+ async load(id) {
1179
+ if (!id.startsWith("\0jay-action:")) {
1180
+ return null;
1181
+ }
1182
+ const actualPath = id.slice("\0jay-action:".length);
1183
+ let code;
1184
+ try {
1185
+ code = await fs.promises.readFile(actualPath, "utf-8");
1186
+ } catch (err) {
1187
+ console.error(`[action-transform] Could not read ${actualPath}:`, err);
1188
+ return null;
1189
+ }
1190
+ const actions = extractActionsFromSource(code, actualPath);
1191
+ if (actions.length === 0) {
1192
+ console.warn(`[action-transform] No actions found in ${actualPath}`);
1193
+ return null;
1194
+ }
1195
+ const lines = [
1196
+ `import { createActionCaller } from '@jay-framework/stack-client-runtime';`,
1197
+ ""
1198
+ ];
1199
+ for (const action of actions) {
1200
+ lines.push(
1201
+ `export const ${action.exportName} = createActionCaller('${action.actionName}', '${action.method}');`
1202
+ );
1203
+ }
1204
+ if (code.includes("ActionError")) {
1205
+ lines.push(
1206
+ `export { ActionError } from '@jay-framework/stack-client-runtime';`
1207
+ );
1208
+ }
1209
+ const result = lines.join("\n");
1210
+ return result;
1211
+ }
1212
+ };
1213
+ })(),
1214
+ // Third: Jay runtime compilation (existing plugin)
892
1215
  jayRuntime(jayOptions)
893
- ];
1216
+ );
1217
+ return plugins;
894
1218
  }
895
1219
  class ServiceLifecycleManager {
896
1220
  constructor(projectRoot, sourceBase = "src") {
897
- __publicField(this, "initFilePath", null);
1221
+ /** Path to project's lib/init.ts (makeJayInit pattern) */
1222
+ __publicField(this, "projectInitFilePath", null);
898
1223
  __publicField(this, "isInitialized", false);
899
1224
  __publicField(this, "viteServer", null);
1225
+ __publicField(this, "pluginsWithInit", []);
900
1226
  this.projectRoot = projectRoot;
901
1227
  this.sourceBase = sourceBase;
902
1228
  }
@@ -907,14 +1233,13 @@ class ServiceLifecycleManager {
907
1233
  this.viteServer = viteServer;
908
1234
  }
909
1235
  /**
910
- * Finds the jay.init.ts (or .js) file in the source directory.
911
- * Looks in: {projectRoot}/{sourceBase}/jay.init.{ts,js,mts,mjs}
1236
+ * Finds the project init file using makeJayInit pattern.
1237
+ * Looks in: {projectRoot}/{sourceBase}/init.{ts,js}
912
1238
  */
913
- findInitFile() {
914
- const extensions = [".ts", ".js", ".mts", ".mjs"];
915
- const baseFilename = "jay.init";
1239
+ findProjectInitFile() {
1240
+ const extensions = [".ts", ".js"];
916
1241
  for (const ext of extensions) {
917
- const filePath = path.join(this.projectRoot, this.sourceBase, baseFilename + ext);
1242
+ const filePath = path.join(this.projectRoot, this.sourceBase, "init" + ext);
918
1243
  if (fs.existsSync(filePath)) {
919
1244
  return filePath;
920
1245
  }
@@ -922,32 +1247,88 @@ class ServiceLifecycleManager {
922
1247
  return null;
923
1248
  }
924
1249
  /**
925
- * Initializes services by loading and executing jay.init.ts
1250
+ * Initializes services by:
1251
+ * 1. Discovering and executing plugin server inits (in dependency order)
1252
+ * 2. Loading and executing project lib/init.ts
1253
+ * 3. Running all registered onInit callbacks
1254
+ * 4. Auto-discovering and registering actions
926
1255
  */
927
1256
  async initialize() {
928
1257
  if (this.isInitialized) {
929
1258
  console.warn("[Services] Already initialized, skipping...");
930
1259
  return;
931
1260
  }
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;
1261
+ this.projectInitFilePath = this.findProjectInitFile();
1262
+ const discoveredPlugins = await discoverPluginsWithInit({
1263
+ projectRoot: this.projectRoot,
1264
+ verbose: true
1265
+ });
1266
+ this.pluginsWithInit = sortPluginsByDependencies(discoveredPlugins);
1267
+ if (this.pluginsWithInit.length > 0) {
1268
+ console.log(
1269
+ `[Services] Found ${this.pluginsWithInit.length} plugin(s) with init: ${this.pluginsWithInit.map((p) => p.name).join(", ")}`
1270
+ );
936
1271
  }
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);
1272
+ await executePluginServerInits(this.pluginsWithInit, this.viteServer ?? void 0, true);
1273
+ if (this.projectInitFilePath) {
1274
+ console.log("[DevServer] Loading project init: src/init.ts");
1275
+ try {
1276
+ if (this.viteServer) {
1277
+ const module = await this.viteServer.ssrLoadModule(this.projectInitFilePath);
1278
+ if (module.init?._serverInit) {
1279
+ console.log("[DevServer] Running server init: project");
1280
+ const { setClientInitData } = await import("@jay-framework/stack-server-runtime");
1281
+ const clientData = await module.init._serverInit();
1282
+ if (clientData !== void 0 && clientData !== null) {
1283
+ setClientInitData("project", clientData);
1284
+ }
1285
+ }
1286
+ } else {
1287
+ const fileUrl = pathToFileURL(this.projectInitFilePath).href;
1288
+ await import(fileUrl);
1289
+ }
1290
+ } catch (error) {
1291
+ console.error("[Services] Failed to load project init:", error);
1292
+ throw error;
944
1293
  }
945
- await runInitCallbacks();
946
- this.isInitialized = true;
947
- console.log("[Services] Initialization complete");
1294
+ } else {
1295
+ console.log("[Services] No init.ts found, skipping project initialization");
1296
+ }
1297
+ await runInitCallbacks();
1298
+ console.log("[Services] Initialization complete");
1299
+ await this.discoverActions();
1300
+ this.isInitialized = true;
1301
+ }
1302
+ /**
1303
+ * Auto-discovers and registers actions from project and plugins.
1304
+ */
1305
+ async discoverActions() {
1306
+ let totalActions = 0;
1307
+ try {
1308
+ const result = await discoverAndRegisterActions({
1309
+ projectRoot: this.projectRoot,
1310
+ actionsDir: path.join(this.sourceBase, "actions"),
1311
+ registry: actionRegistry,
1312
+ verbose: true,
1313
+ viteServer: this.viteServer ?? void 0
1314
+ });
1315
+ totalActions += result.actionCount;
948
1316
  } catch (error) {
949
- console.error("[Services] Failed to initialize:", error);
950
- throw error;
1317
+ console.error("[Actions] Failed to auto-discover project actions:", error);
1318
+ }
1319
+ try {
1320
+ const pluginActions = await discoverAllPluginActions({
1321
+ projectRoot: this.projectRoot,
1322
+ registry: actionRegistry,
1323
+ verbose: true,
1324
+ viteServer: this.viteServer ?? void 0
1325
+ });
1326
+ totalActions += pluginActions.length;
1327
+ } catch (error) {
1328
+ console.error("[Actions] Failed to auto-discover plugin actions:", error);
1329
+ }
1330
+ if (totalActions > 0) {
1331
+ console.log(`[Actions] Auto-registered ${totalActions} action(s) total`);
951
1332
  }
952
1333
  }
953
1334
  /**
@@ -980,31 +1361,48 @@ class ServiceLifecycleManager {
980
1361
  * Hot reload: shutdown, clear caches, re-import, and re-initialize
981
1362
  */
982
1363
  async reload() {
983
- if (!this.initFilePath) {
984
- console.log("[Services] No init file to reload");
985
- return;
986
- }
987
1364
  console.log("[Services] Reloading services...");
988
1365
  await this.shutdown();
989
1366
  clearLifecycleCallbacks();
990
1367
  clearServiceRegistry();
991
- if (this.viteServer) {
992
- const moduleNode = this.viteServer.moduleGraph.getModuleById(this.initFilePath);
1368
+ clearClientInitData();
1369
+ actionRegistry.clear();
1370
+ if (this.projectInitFilePath && this.viteServer) {
1371
+ const moduleNode = this.viteServer.moduleGraph.getModuleById(this.projectInitFilePath);
993
1372
  if (moduleNode) {
994
1373
  await this.viteServer.moduleGraph.invalidateModule(moduleNode);
995
1374
  }
996
- } else {
997
- delete require.cache[require.resolve(this.initFilePath)];
1375
+ } else if (this.projectInitFilePath) {
1376
+ delete require.cache[require.resolve(this.projectInitFilePath)];
998
1377
  }
999
1378
  this.isInitialized = false;
1000
1379
  await this.initialize();
1001
1380
  console.log("[Services] Reload complete");
1002
1381
  }
1003
1382
  /**
1004
- * Returns the path to the init file if found
1383
+ * Returns the path to the init file if found.
1005
1384
  */
1006
1385
  getInitFilePath() {
1007
- return this.initFilePath;
1386
+ return this.projectInitFilePath;
1387
+ }
1388
+ /**
1389
+ * Returns project init info for client-side embedding.
1390
+ */
1391
+ getProjectInit() {
1392
+ if (!this.projectInitFilePath) {
1393
+ return null;
1394
+ }
1395
+ return {
1396
+ importPath: this.projectInitFilePath,
1397
+ initExport: "init"
1398
+ };
1399
+ }
1400
+ /**
1401
+ * Returns the discovered plugins with init configurations.
1402
+ * Sorted by dependencies (plugins with no deps first).
1403
+ */
1404
+ getPluginsWithInit() {
1405
+ return this.pluginsWithInit;
1008
1406
  }
1009
1407
  /**
1010
1408
  * Checks if services are initialized
@@ -1013,6 +1411,212 @@ class ServiceLifecycleManager {
1013
1411
  return this.isInitialized;
1014
1412
  }
1015
1413
  }
1414
+ function deepMergeViewStates(base, overlay, trackByMap, path2 = "") {
1415
+ if (!base && !overlay)
1416
+ return {};
1417
+ if (!base)
1418
+ return overlay || {};
1419
+ if (!overlay)
1420
+ return base || {};
1421
+ const result = {};
1422
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(overlay)]);
1423
+ for (const key of allKeys) {
1424
+ const baseValue = base[key];
1425
+ const overlayValue = overlay[key];
1426
+ const currentPath = path2 ? `${path2}.${key}` : key;
1427
+ if (overlayValue === void 0) {
1428
+ result[key] = baseValue;
1429
+ } else if (baseValue === void 0) {
1430
+ result[key] = overlayValue;
1431
+ } else if (Array.isArray(baseValue) && Array.isArray(overlayValue)) {
1432
+ const trackByField = trackByMap[currentPath];
1433
+ if (trackByField) {
1434
+ result[key] = mergeArraysByTrackBy(
1435
+ baseValue,
1436
+ overlayValue,
1437
+ trackByField,
1438
+ trackByMap,
1439
+ currentPath
1440
+ );
1441
+ } else {
1442
+ result[key] = overlayValue;
1443
+ }
1444
+ } else if (typeof baseValue === "object" && baseValue !== null && typeof overlayValue === "object" && overlayValue !== null && !Array.isArray(baseValue) && !Array.isArray(overlayValue)) {
1445
+ result[key] = deepMergeViewStates(baseValue, overlayValue, trackByMap, currentPath);
1446
+ } else {
1447
+ result[key] = overlayValue;
1448
+ }
1449
+ }
1450
+ return result;
1451
+ }
1452
+ function mergeArraysByTrackBy(baseArray, overlayArray, trackByField, trackByMap, arrayPath) {
1453
+ const baseByKey = /* @__PURE__ */ new Map();
1454
+ for (const item of baseArray) {
1455
+ const key = item[trackByField];
1456
+ if (key !== void 0 && key !== null) {
1457
+ if (baseByKey.has(key)) {
1458
+ console.warn(
1459
+ `Duplicate trackBy key [${key}] in base array at path [${arrayPath}]. This may cause incorrect merging.`
1460
+ );
1461
+ }
1462
+ baseByKey.set(key, item);
1463
+ }
1464
+ }
1465
+ const overlayByKey = /* @__PURE__ */ new Map();
1466
+ for (const item of overlayArray) {
1467
+ const key = item[trackByField];
1468
+ if (key !== void 0 && key !== null) {
1469
+ overlayByKey.set(key, item);
1470
+ }
1471
+ }
1472
+ return baseArray.map((baseItem) => {
1473
+ const key = baseItem[trackByField];
1474
+ if (key === void 0 || key === null) {
1475
+ return baseItem;
1476
+ }
1477
+ const overlayItem = overlayByKey.get(key);
1478
+ if (overlayItem) {
1479
+ return deepMergeViewStates(baseItem, overlayItem, trackByMap, arrayPath);
1480
+ } else {
1481
+ return baseItem;
1482
+ }
1483
+ });
1484
+ }
1485
+ const ACTION_ENDPOINT_BASE = "/_jay/actions";
1486
+ function createActionRouter(options) {
1487
+ const registry = options?.registry ?? actionRegistry;
1488
+ return async (req, res) => {
1489
+ const actionName = req.path.slice(1);
1490
+ if (!actionName) {
1491
+ res.status(400).json({
1492
+ success: false,
1493
+ error: {
1494
+ code: "MISSING_ACTION_NAME",
1495
+ message: "Action name is required",
1496
+ isActionError: false
1497
+ }
1498
+ });
1499
+ return;
1500
+ }
1501
+ const action = registry.get(actionName);
1502
+ if (!action) {
1503
+ res.status(404).json({
1504
+ success: false,
1505
+ error: {
1506
+ code: "ACTION_NOT_FOUND",
1507
+ message: `Action '${actionName}' is not registered`,
1508
+ isActionError: false
1509
+ }
1510
+ });
1511
+ return;
1512
+ }
1513
+ const requestMethod = req.method.toUpperCase();
1514
+ if (requestMethod !== action.method) {
1515
+ res.status(405).json({
1516
+ success: false,
1517
+ error: {
1518
+ code: "METHOD_NOT_ALLOWED",
1519
+ message: `Action '${actionName}' expects ${action.method}, got ${requestMethod}`,
1520
+ isActionError: false
1521
+ }
1522
+ });
1523
+ return;
1524
+ }
1525
+ let input;
1526
+ try {
1527
+ if (requestMethod === "GET") {
1528
+ if (req.query._input) {
1529
+ input = JSON.parse(req.query._input);
1530
+ } else {
1531
+ input = { ...req.query };
1532
+ delete input._input;
1533
+ }
1534
+ } else {
1535
+ input = req.body;
1536
+ }
1537
+ } catch (parseError) {
1538
+ res.status(400).json({
1539
+ success: false,
1540
+ error: {
1541
+ code: "INVALID_INPUT",
1542
+ message: "Failed to parse request input",
1543
+ isActionError: false
1544
+ }
1545
+ });
1546
+ return;
1547
+ }
1548
+ const result = await registry.execute(actionName, input);
1549
+ if (requestMethod === "GET" && result.success) {
1550
+ const cacheHeaders = registry.getCacheHeaders(actionName);
1551
+ if (cacheHeaders) {
1552
+ res.set("Cache-Control", cacheHeaders);
1553
+ }
1554
+ }
1555
+ if (result.success) {
1556
+ res.status(200).json({
1557
+ success: true,
1558
+ data: result.data
1559
+ });
1560
+ } else {
1561
+ const statusCode = getStatusCodeForError(result.error.code, result.error.isActionError);
1562
+ res.status(statusCode).json({
1563
+ success: false,
1564
+ error: result.error
1565
+ });
1566
+ }
1567
+ };
1568
+ }
1569
+ function getStatusCodeForError(code, isActionError) {
1570
+ if (isActionError) {
1571
+ return 422;
1572
+ }
1573
+ switch (code) {
1574
+ case "ACTION_NOT_FOUND":
1575
+ return 404;
1576
+ case "INVALID_INPUT":
1577
+ case "VALIDATION_ERROR":
1578
+ return 400;
1579
+ case "UNAUTHORIZED":
1580
+ return 401;
1581
+ case "FORBIDDEN":
1582
+ return 403;
1583
+ case "INTERNAL_ERROR":
1584
+ default:
1585
+ return 500;
1586
+ }
1587
+ }
1588
+ function actionBodyParser() {
1589
+ return (req, res, next) => {
1590
+ if (!req.path.startsWith(ACTION_ENDPOINT_BASE)) {
1591
+ next();
1592
+ return;
1593
+ }
1594
+ if (req.method === "GET") {
1595
+ next();
1596
+ return;
1597
+ }
1598
+ let body = "";
1599
+ req.setEncoding("utf8");
1600
+ req.on("data", (chunk) => {
1601
+ body += chunk;
1602
+ });
1603
+ req.on("end", () => {
1604
+ try {
1605
+ req.body = body ? JSON.parse(body) : {};
1606
+ next();
1607
+ } catch (e2) {
1608
+ res.status(400).json({
1609
+ success: false,
1610
+ error: {
1611
+ code: "INVALID_JSON",
1612
+ message: "Invalid JSON in request body",
1613
+ isActionError: false
1614
+ }
1615
+ });
1616
+ }
1617
+ });
1618
+ };
1619
+ }
1016
1620
  async function initRoutes(pagesBaseFolder) {
1017
1621
  return await scanRoutes(pagesBaseFolder, {
1018
1622
  jayHtmlFilename: "page.jay-html",
@@ -1046,7 +1650,7 @@ function handleOtherResponseCodes(res, renderedResult) {
1046
1650
  else
1047
1651
  res.status(renderedResult.status).end("redirect to " + renderedResult.location);
1048
1652
  }
1049
- function mkRoute(route, vite, slowlyPhase, options) {
1653
+ function mkRoute(route, vite, slowlyPhase, options, projectInit, allPluginsWithInit = [], allPluginClientInits = []) {
1050
1654
  const path2 = routeToExpressRoute(route);
1051
1655
  const handler = async (req, res) => {
1052
1656
  try {
@@ -1057,33 +1661,56 @@ function mkRoute(route, vite, slowlyPhase, options) {
1057
1661
  url
1058
1662
  };
1059
1663
  let viewState, carryForward;
1060
- const pageParts = await loadPageParts(
1664
+ const pagePartsResult = await loadPageParts(
1061
1665
  vite,
1062
1666
  route,
1063
1667
  options.pagesRootFolder,
1668
+ options.projectRootFolder,
1064
1669
  options.jayRollupConfig
1065
1670
  );
1066
- if (pageParts.val) {
1671
+ if (pagePartsResult.val) {
1672
+ const {
1673
+ parts: pageParts,
1674
+ serverTrackByMap,
1675
+ clientTrackByMap,
1676
+ usedPackages
1677
+ } = pagePartsResult.val;
1678
+ const pluginsForPage = allPluginClientInits.filter((plugin) => {
1679
+ const pluginInfo = allPluginsWithInit.find((p) => p.name === plugin.name);
1680
+ return pluginInfo && usedPackages.has(pluginInfo.packageName);
1681
+ });
1067
1682
  const renderedSlowly = await slowlyPhase.runSlowlyForPage(
1068
1683
  pageParams,
1069
1684
  pageProps,
1070
- pageParts.val
1685
+ pageParts
1071
1686
  );
1072
- if (renderedSlowly.kind === "PartialRender") {
1687
+ if (renderedSlowly.kind === "PhaseOutput") {
1073
1688
  const renderedFast = await renderFastChangingData(
1074
1689
  pageParams,
1075
1690
  pageProps,
1076
1691
  renderedSlowly.carryForward,
1077
- pageParts.val
1692
+ pageParts
1078
1693
  );
1079
- if (renderedFast.kind === "PartialRender") {
1080
- viewState = { ...renderedSlowly.rendered, ...renderedFast.rendered };
1694
+ if (renderedFast.kind === "PhaseOutput") {
1695
+ if (serverTrackByMap && Object.keys(serverTrackByMap).length > 0) {
1696
+ viewState = deepMergeViewStates(
1697
+ renderedSlowly.rendered,
1698
+ renderedFast.rendered,
1699
+ serverTrackByMap
1700
+ );
1701
+ } else {
1702
+ viewState = { ...renderedSlowly.rendered, ...renderedFast.rendered };
1703
+ }
1081
1704
  carryForward = renderedFast.carryForward;
1082
1705
  const pageHtml = generateClientScript(
1083
1706
  viewState,
1084
1707
  carryForward,
1085
- pageParts.val,
1086
- route.jayHtmlPath
1708
+ pageParts,
1709
+ route.jayHtmlPath,
1710
+ clientTrackByMap,
1711
+ getClientInitData(),
1712
+ projectInit,
1713
+ pluginsForPage
1087
1714
  );
1088
1715
  const compiledPageHtml = await vite.transformIndexHtml(
1089
1716
  !!url ? url : "/",
@@ -1097,8 +1724,8 @@ function mkRoute(route, vite, slowlyPhase, options) {
1097
1724
  handleOtherResponseCodes(res, renderedSlowly);
1098
1725
  }
1099
1726
  } else {
1100
- console.log(pageParts.validations.join("\n"));
1101
- res.status(500).end(pageParts.validations.join("\n"));
1727
+ console.log(pagePartsResult.validations.join("\n"));
1728
+ res.status(500).end(pagePartsResult.validations.join("\n"));
1102
1729
  }
1103
1730
  } catch (e2) {
1104
1731
  vite?.ssrFixStacktrace(e2);
@@ -1126,17 +1753,21 @@ async function mkDevServer(options) {
1126
1753
  root: pagesRootFolder,
1127
1754
  ssr: {
1128
1755
  // 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
1756
+ // This ensures lib/init.ts and dev-server share the same module instance
1130
1757
  external: ["@jay-framework/stack-server-runtime"]
1131
1758
  }
1132
1759
  });
1133
1760
  lifecycleManager.setViteServer(vite);
1134
1761
  await lifecycleManager.initialize();
1135
1762
  setupServiceHotReload(vite, lifecycleManager);
1763
+ setupActionRouter(vite);
1136
1764
  const routes = await initRoutes(pagesRootFolder);
1137
1765
  const slowlyPhase = new DevSlowlyChangingPhase(dontCacheSlowly);
1766
+ const projectInit = lifecycleManager.getProjectInit() ?? void 0;
1767
+ const pluginsWithInit = lifecycleManager.getPluginsWithInit();
1768
+ const pluginClientInits = preparePluginClientInits(pluginsWithInit);
1138
1769
  const devServerRoutes = routes.map(
1139
- (route) => mkRoute(route, vite, slowlyPhase, options)
1770
+ (route) => mkRoute(route, vite, slowlyPhase, options, projectInit, pluginsWithInit, pluginClientInits)
1140
1771
  );
1141
1772
  return {
1142
1773
  server: vite.middlewares,
@@ -1163,7 +1794,7 @@ function setupServiceHotReload(vite, lifecycleManager) {
1163
1794
  vite.watcher.add(initFilePath);
1164
1795
  vite.watcher.on("change", async (changedPath) => {
1165
1796
  if (changedPath === initFilePath) {
1166
- console.log("[Services] jay.init.ts changed, reloading services...");
1797
+ console.log("[Services] lib/init.ts changed, reloading services...");
1167
1798
  try {
1168
1799
  await lifecycleManager.reload();
1169
1800
  vite.ws.send({
@@ -1176,6 +1807,14 @@ function setupServiceHotReload(vite, lifecycleManager) {
1176
1807
  }
1177
1808
  });
1178
1809
  }
1810
+ function setupActionRouter(vite) {
1811
+ vite.middlewares.use(actionBodyParser());
1812
+ vite.middlewares.use(ACTION_ENDPOINT_BASE, createActionRouter());
1813
+ console.log(`[Actions] Action router mounted at ${ACTION_ENDPOINT_BASE}`);
1814
+ }
1179
1815
  export {
1816
+ ACTION_ENDPOINT_BASE,
1817
+ actionBodyParser,
1818
+ createActionRouter,
1180
1819
  mkDevServer
1181
1820
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/dev-server",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/index.js",
@@ -22,20 +22,21 @@
22
22
  "test:watch": "vitest"
23
23
  },
24
24
  "dependencies": {
25
- "@jay-framework/compiler-jay-stack": "^0.9.0",
26
- "@jay-framework/compiler-shared": "^0.9.0",
27
- "@jay-framework/component": "^0.9.0",
28
- "@jay-framework/fullstack-component": "^0.9.0",
29
- "@jay-framework/runtime": "^0.9.0",
30
- "@jay-framework/stack-client-runtime": "^0.9.0",
31
- "@jay-framework/stack-route-scanner": "^0.9.0",
32
- "@jay-framework/stack-server-runtime": "^0.9.0",
25
+ "@jay-framework/compiler-jay-stack": "^0.10.0",
26
+ "@jay-framework/compiler-shared": "^0.10.0",
27
+ "@jay-framework/component": "^0.10.0",
28
+ "@jay-framework/fullstack-component": "^0.10.0",
29
+ "@jay-framework/runtime": "^0.10.0",
30
+ "@jay-framework/stack-client-runtime": "^0.10.0",
31
+ "@jay-framework/stack-route-scanner": "^0.10.0",
32
+ "@jay-framework/stack-server-runtime": "^0.10.0",
33
+ "@jay-framework/view-state-merge": "^0.10.0",
33
34
  "vite": "^5.0.11"
34
35
  },
35
36
  "devDependencies": {
36
- "@jay-framework/dev-environment": "^0.9.0",
37
- "@jay-framework/jay-cli": "^0.9.0",
38
- "@jay-framework/stack-client-runtime": "^0.9.0",
37
+ "@jay-framework/dev-environment": "^0.10.0",
38
+ "@jay-framework/jay-cli": "^0.10.0",
39
+ "@jay-framework/stack-client-runtime": "^0.10.0",
39
40
  "@types/express": "^5.0.2",
40
41
  "@types/node": "^22.15.21",
41
42
  "nodemon": "^3.0.3",