@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.
- package/dist/index.d.ts +79 -9
- package/dist/index.js +741 -102
- 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
|
|
18
|
-
* hot reloading services, and
|
|
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
|
-
|
|
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
|
|
34
|
-
* Looks in: {projectRoot}/{sourceBase}/
|
|
37
|
+
* Finds the project init file using makeJayInit pattern.
|
|
38
|
+
* Looks in: {projectRoot}/{sourceBase}/init.{ts,js}
|
|
35
39
|
*/
|
|
36
|
-
private
|
|
40
|
+
private findProjectInitFile;
|
|
37
41
|
/**
|
|
38
|
-
* Initializes services by
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
602
|
+
const COMPONENT_SERVER_METHODS = /* @__PURE__ */ new Set([
|
|
603
603
|
"withServices",
|
|
604
604
|
"withLoadParams",
|
|
605
605
|
"withSlowlyRender",
|
|
606
606
|
"withFastRender"
|
|
607
607
|
]);
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
|
674
|
-
isVariableStatement
|
|
675
|
-
isInterfaceDeclaration
|
|
676
|
-
isTypeAliasDeclaration
|
|
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
|
|
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
|
|
750
|
+
if (isFunctionDeclaration(statement) && statement.name) {
|
|
743
751
|
return statement.name.text;
|
|
744
752
|
}
|
|
745
|
-
if (isVariableStatement
|
|
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
|
|
759
|
+
if (isInterfaceDeclaration(statement) && statement.name) {
|
|
752
760
|
return statement.name.text;
|
|
753
761
|
}
|
|
754
|
-
if (isTypeAliasDeclaration
|
|
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
|
|
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(
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
-
|
|
867
|
-
|
|
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
|
-
|
|
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
|
|
880
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
911
|
-
* Looks in: {projectRoot}/{sourceBase}/
|
|
1236
|
+
* Finds the project init file using makeJayInit pattern.
|
|
1237
|
+
* Looks in: {projectRoot}/{sourceBase}/init.{ts,js}
|
|
912
1238
|
*/
|
|
913
|
-
|
|
914
|
-
const extensions = [".ts", ".js"
|
|
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,
|
|
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
|
|
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.
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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("[
|
|
950
|
-
|
|
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
|
-
|
|
992
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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 (
|
|
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
|
|
1685
|
+
pageParts
|
|
1071
1686
|
);
|
|
1072
|
-
if (renderedSlowly.kind === "
|
|
1687
|
+
if (renderedSlowly.kind === "PhaseOutput") {
|
|
1073
1688
|
const renderedFast = await renderFastChangingData(
|
|
1074
1689
|
pageParams,
|
|
1075
1690
|
pageProps,
|
|
1076
1691
|
renderedSlowly.carryForward,
|
|
1077
|
-
pageParts
|
|
1692
|
+
pageParts
|
|
1078
1693
|
);
|
|
1079
|
-
if (renderedFast.kind === "
|
|
1080
|
-
|
|
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
|
|
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(
|
|
1101
|
-
res.status(500).end(
|
|
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
|
|
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]
|
|
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.
|
|
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.
|
|
26
|
-
"@jay-framework/compiler-shared": "^0.
|
|
27
|
-
"@jay-framework/component": "^0.
|
|
28
|
-
"@jay-framework/fullstack-component": "^0.
|
|
29
|
-
"@jay-framework/runtime": "^0.
|
|
30
|
-
"@jay-framework/stack-client-runtime": "^0.
|
|
31
|
-
"@jay-framework/stack-route-scanner": "^0.
|
|
32
|
-
"@jay-framework/stack-server-runtime": "^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.
|
|
37
|
-
"@jay-framework/jay-cli": "^0.
|
|
38
|
-
"@jay-framework/stack-client-runtime": "^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",
|