@jay-framework/compiler-jay-stack 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 +122 -4
- package/dist/index.js +462 -50
- package/package.json +7 -6
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,23 @@ import { Plugin } from 'vite';
|
|
|
2
2
|
import { JayRollupConfig } from '@jay-framework/vite-plugin';
|
|
3
3
|
export { JayRollupConfig } from '@jay-framework/vite-plugin';
|
|
4
4
|
|
|
5
|
+
interface ImportChainTrackerOptions {
|
|
6
|
+
/** Enable verbose logging */
|
|
7
|
+
verbose?: boolean;
|
|
8
|
+
/** Additional modules to treat as server-only */
|
|
9
|
+
additionalServerModules?: string[];
|
|
10
|
+
/** Additional package patterns to treat as server-only */
|
|
11
|
+
additionalServerPatterns?: string[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Creates a Vite plugin that tracks import chains and logs when
|
|
15
|
+
* server-only modules are imported in client builds.
|
|
16
|
+
*
|
|
17
|
+
* This helps debug issues where server code is accidentally pulled
|
|
18
|
+
* into client bundles.
|
|
19
|
+
*/
|
|
20
|
+
declare function createImportChainTracker(options?: ImportChainTrackerOptions): Plugin;
|
|
21
|
+
|
|
5
22
|
type BuildEnvironment = 'client' | 'server';
|
|
6
23
|
/**
|
|
7
24
|
* Transform Jay Stack component builder chains to strip environment-specific code
|
|
@@ -16,12 +33,103 @@ declare function transformJayStackBuilder(code: string, filePath: string, enviro
|
|
|
16
33
|
map?: any;
|
|
17
34
|
};
|
|
18
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Transform action imports for client builds.
|
|
38
|
+
*
|
|
39
|
+
* On the server, action imports remain unchanged (handlers are executed directly).
|
|
40
|
+
* On the client, action imports are replaced with createActionCaller() calls.
|
|
41
|
+
*
|
|
42
|
+
* Example:
|
|
43
|
+
* ```typescript
|
|
44
|
+
* // Source
|
|
45
|
+
* import { addToCart, searchProducts } from '../actions/cart.actions';
|
|
46
|
+
*
|
|
47
|
+
* // Client build output
|
|
48
|
+
* import { createActionCaller } from '@jay-framework/stack-client-runtime';
|
|
49
|
+
* const addToCart = createActionCaller('cart.addToCart', 'POST');
|
|
50
|
+
* const searchProducts = createActionCaller('products.search', 'GET');
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* Metadata for a discovered action.
|
|
55
|
+
*/
|
|
56
|
+
interface ActionMetadata {
|
|
57
|
+
/** Unique action name (e.g., 'cart.addToCart') */
|
|
58
|
+
actionName: string;
|
|
59
|
+
/** HTTP method */
|
|
60
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
61
|
+
/** Export name in the source module */
|
|
62
|
+
exportName: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Result of extracting actions from a module.
|
|
66
|
+
*/
|
|
67
|
+
interface ExtractedActions {
|
|
68
|
+
/** Path to the action module */
|
|
69
|
+
modulePath: string;
|
|
70
|
+
/** Actions exported from this module */
|
|
71
|
+
actions: ActionMetadata[];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Clears the action metadata cache (useful for testing/dev reload).
|
|
75
|
+
*/
|
|
76
|
+
declare function clearActionMetadataCache(): void;
|
|
77
|
+
/**
|
|
78
|
+
* Checks if an import source refers to an action module.
|
|
79
|
+
*/
|
|
80
|
+
declare function isActionImport(importSource: string): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Extracts action metadata from source code by parsing makeJayAction/makeJayQuery calls.
|
|
83
|
+
*
|
|
84
|
+
* @param sourceCode - The TypeScript source code
|
|
85
|
+
* @param filePath - Path to the file (for error messages)
|
|
86
|
+
* @returns Array of action metadata
|
|
87
|
+
*/
|
|
88
|
+
declare function extractActionsFromSource(sourceCode: string, filePath: string): ActionMetadata[];
|
|
89
|
+
/**
|
|
90
|
+
* Transform result with source map support.
|
|
91
|
+
*/
|
|
92
|
+
interface TransformResult {
|
|
93
|
+
code: string;
|
|
94
|
+
map?: any;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Transforms action imports in client builds.
|
|
98
|
+
*
|
|
99
|
+
* Replaces imports from action modules with createActionCaller() calls.
|
|
100
|
+
*
|
|
101
|
+
* @param code - Source code to transform
|
|
102
|
+
* @param id - Module ID (file path)
|
|
103
|
+
* @param resolveActionModule - Function to resolve and load action module source
|
|
104
|
+
* @returns Transformed code or null if no transform needed
|
|
105
|
+
*/
|
|
106
|
+
declare function transformActionImports(code: string, id: string, resolveActionModule: (importSource: string, importer: string) => Promise<{
|
|
107
|
+
path: string;
|
|
108
|
+
code: string;
|
|
109
|
+
} | null>): Promise<TransformResult | null>;
|
|
110
|
+
|
|
111
|
+
interface JayStackCompilerOptions extends JayRollupConfig {
|
|
112
|
+
/**
|
|
113
|
+
* Enable import chain tracking for debugging server code leaking into client builds.
|
|
114
|
+
* When enabled, logs the full import chain when server-only modules are detected.
|
|
115
|
+
* @default false (but auto-enabled when DEBUG_IMPORTS=1 env var is set)
|
|
116
|
+
*/
|
|
117
|
+
trackImports?: boolean | ImportChainTrackerOptions;
|
|
118
|
+
}
|
|
19
119
|
/**
|
|
20
120
|
* Jay Stack Compiler - Handles both Jay runtime compilation and Jay Stack code splitting
|
|
21
121
|
*
|
|
22
122
|
* This plugin internally uses the jay:runtime plugin and adds Jay Stack-specific
|
|
23
123
|
* transformations for client/server code splitting.
|
|
24
124
|
*
|
|
125
|
+
* Environment detection is based on Vite's `options.ssr`:
|
|
126
|
+
* - `options.ssr = true` → server build (strip client code)
|
|
127
|
+
* - `options.ssr = false/undefined` → client build (strip server code)
|
|
128
|
+
*
|
|
129
|
+
* This works for both:
|
|
130
|
+
* - Dev server: SSR renders with server code, browser hydrates with client code
|
|
131
|
+
* - Package builds: Use `build.ssr = true/false` to control environment
|
|
132
|
+
*
|
|
25
133
|
* Usage:
|
|
26
134
|
* ```typescript
|
|
27
135
|
* import { jayStackCompiler } from '@jay-framework/compiler-jay-stack';
|
|
@@ -33,9 +141,19 @@ declare function transformJayStackBuilder(code: string, filePath: string, enviro
|
|
|
33
141
|
* });
|
|
34
142
|
* ```
|
|
35
143
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
144
|
+
* To debug import chain issues (server code leaking to client):
|
|
145
|
+
* ```bash
|
|
146
|
+
* DEBUG_IMPORTS=1 npm run build
|
|
147
|
+
* ```
|
|
148
|
+
*
|
|
149
|
+
* Or enable in config:
|
|
150
|
+
* ```typescript
|
|
151
|
+
* ...jayStackCompiler({ trackImports: true })
|
|
152
|
+
* ```
|
|
153
|
+
*
|
|
154
|
+
* @param options - Configuration for Jay Stack compiler
|
|
155
|
+
* @returns Array of Vite plugins
|
|
38
156
|
*/
|
|
39
|
-
declare function jayStackCompiler(
|
|
157
|
+
declare function jayStackCompiler(options?: JayStackCompilerOptions): Plugin[];
|
|
40
158
|
|
|
41
|
-
export { type BuildEnvironment, jayStackCompiler, transformJayStackBuilder };
|
|
159
|
+
export { type ActionMetadata, type BuildEnvironment, type ExtractedActions, type ImportChainTrackerOptions, type JayStackCompilerOptions, clearActionMetadataCache, createImportChainTracker, extractActionsFromSource, isActionImport, jayStackCompiler, transformActionImports, transformJayStackBuilder };
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
import { jayRuntime } from "@jay-framework/vite-plugin";
|
|
2
2
|
import tsBridge from "@jay-framework/typescript-bridge";
|
|
3
3
|
import { flattenVariable, isImportModuleVariableRoot, mkTransformer, SourceFileBindingResolver, areFlattenedAccessChainsEqual } from "@jay-framework/compiler";
|
|
4
|
-
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
const COMPONENT_SERVER_METHODS = /* @__PURE__ */ new Set([
|
|
5
7
|
"withServices",
|
|
6
8
|
"withLoadParams",
|
|
7
9
|
"withSlowlyRender",
|
|
8
10
|
"withFastRender"
|
|
9
11
|
]);
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
]);
|
|
12
|
+
const COMPONENT_CLIENT_METHODS = /* @__PURE__ */ new Set(["withInteractive", "withContexts"]);
|
|
13
|
+
const INIT_SERVER_METHODS = /* @__PURE__ */ new Set(["withServer"]);
|
|
14
|
+
const INIT_CLIENT_METHODS = /* @__PURE__ */ new Set(["withClient"]);
|
|
14
15
|
function shouldRemoveMethod(methodName, environment) {
|
|
15
|
-
|
|
16
|
+
if (environment === "client" && COMPONENT_SERVER_METHODS.has(methodName))
|
|
17
|
+
return true;
|
|
18
|
+
if (environment === "server" && COMPONENT_CLIENT_METHODS.has(methodName))
|
|
19
|
+
return true;
|
|
20
|
+
if (environment === "client" && INIT_SERVER_METHODS.has(methodName))
|
|
21
|
+
return true;
|
|
22
|
+
if (environment === "server" && INIT_CLIENT_METHODS.has(methodName))
|
|
23
|
+
return true;
|
|
24
|
+
return false;
|
|
16
25
|
}
|
|
17
26
|
const { isCallExpression: isCallExpression$1, isPropertyAccessExpression: isPropertyAccessExpression$1, isIdentifier: isIdentifier$2, isStringLiteral } = tsBridge;
|
|
18
27
|
function findBuilderMethodsToRemove(sourceFile, bindingResolver, environment) {
|
|
@@ -33,6 +42,7 @@ function findBuilderMethodsToRemove(sourceFile, bindingResolver, environment) {
|
|
|
33
42
|
sourceFile.forEachChild(visit);
|
|
34
43
|
return { callsToRemove, removedVariables };
|
|
35
44
|
}
|
|
45
|
+
const JAY_BUILDER_FUNCTIONS = /* @__PURE__ */ new Set(["makeJayStackComponent", "makeJayInit"]);
|
|
36
46
|
function isPartOfJayStackChain(callExpr, bindingResolver) {
|
|
37
47
|
let current = callExpr.expression;
|
|
38
48
|
while (true) {
|
|
@@ -42,7 +52,7 @@ function isPartOfJayStackChain(callExpr, bindingResolver) {
|
|
|
42
52
|
if (isIdentifier$2(current.expression)) {
|
|
43
53
|
const variable = bindingResolver.explain(current.expression);
|
|
44
54
|
const flattened = flattenVariable(variable);
|
|
45
|
-
if (flattened.path.length === 1 && flattened.path[0]
|
|
55
|
+
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")
|
|
46
56
|
return true;
|
|
47
57
|
}
|
|
48
58
|
if (isPropertyAccessExpression$1(current.expression)) {
|
|
@@ -72,15 +82,15 @@ function collectVariablesFromArguments(args, bindingResolver, variables) {
|
|
|
72
82
|
const {
|
|
73
83
|
isIdentifier: isIdentifier$1,
|
|
74
84
|
isImportDeclaration: isImportDeclaration$1,
|
|
75
|
-
isFunctionDeclaration
|
|
76
|
-
isVariableStatement
|
|
77
|
-
isInterfaceDeclaration
|
|
78
|
-
isTypeAliasDeclaration
|
|
85
|
+
isFunctionDeclaration,
|
|
86
|
+
isVariableStatement,
|
|
87
|
+
isInterfaceDeclaration,
|
|
88
|
+
isTypeAliasDeclaration,
|
|
79
89
|
isClassDeclaration,
|
|
80
90
|
isEnumDeclaration,
|
|
81
91
|
SyntaxKind
|
|
82
92
|
} = tsBridge;
|
|
83
|
-
function analyzeUnusedStatements(sourceFile
|
|
93
|
+
function analyzeUnusedStatements(sourceFile) {
|
|
84
94
|
const statementsToRemove = /* @__PURE__ */ new Set();
|
|
85
95
|
const collectUsedIdentifiers = () => {
|
|
86
96
|
const used = /* @__PURE__ */ new Set();
|
|
@@ -141,19 +151,19 @@ function isExportStatement(statement) {
|
|
|
141
151
|
return false;
|
|
142
152
|
}
|
|
143
153
|
function getStatementDefinedName(statement) {
|
|
144
|
-
if (isFunctionDeclaration
|
|
154
|
+
if (isFunctionDeclaration(statement) && statement.name) {
|
|
145
155
|
return statement.name.text;
|
|
146
156
|
}
|
|
147
|
-
if (isVariableStatement
|
|
157
|
+
if (isVariableStatement(statement)) {
|
|
148
158
|
const firstDecl = statement.declarationList.declarations[0];
|
|
149
159
|
if (firstDecl && isIdentifier$1(firstDecl.name)) {
|
|
150
160
|
return firstDecl.name.text;
|
|
151
161
|
}
|
|
152
162
|
}
|
|
153
|
-
if (isInterfaceDeclaration
|
|
163
|
+
if (isInterfaceDeclaration(statement) && statement.name) {
|
|
154
164
|
return statement.name.text;
|
|
155
165
|
}
|
|
156
|
-
if (isTypeAliasDeclaration
|
|
166
|
+
if (isTypeAliasDeclaration(statement) && statement.name) {
|
|
157
167
|
return statement.name.text;
|
|
158
168
|
}
|
|
159
169
|
if (isClassDeclaration(statement) && statement.name) {
|
|
@@ -173,19 +183,10 @@ const {
|
|
|
173
183
|
isPropertyAccessExpression,
|
|
174
184
|
isImportDeclaration,
|
|
175
185
|
isNamedImports,
|
|
176
|
-
isIdentifier
|
|
177
|
-
isFunctionDeclaration,
|
|
178
|
-
isVariableStatement,
|
|
179
|
-
isInterfaceDeclaration,
|
|
180
|
-
isTypeAliasDeclaration
|
|
186
|
+
isIdentifier
|
|
181
187
|
} = tsBridge;
|
|
182
188
|
function transformJayStackBuilder(code, filePath, environment) {
|
|
183
|
-
const sourceFile = createSourceFile(
|
|
184
|
-
filePath,
|
|
185
|
-
code,
|
|
186
|
-
ScriptTarget.Latest,
|
|
187
|
-
true
|
|
188
|
-
);
|
|
189
|
+
const sourceFile = createSourceFile(filePath, code, ScriptTarget.Latest, true);
|
|
189
190
|
const transformers = [mkTransformer(mkJayStackCodeSplitTransformer, { environment })];
|
|
190
191
|
const printer = createPrinter();
|
|
191
192
|
const result = tsBridge.transform(sourceFile, transformers);
|
|
@@ -206,11 +207,7 @@ function mkJayStackCodeSplitTransformer({
|
|
|
206
207
|
environment
|
|
207
208
|
}) {
|
|
208
209
|
const bindingResolver = new SourceFileBindingResolver(sourceFile);
|
|
209
|
-
const { callsToRemove
|
|
210
|
-
sourceFile,
|
|
211
|
-
bindingResolver,
|
|
212
|
-
environment
|
|
213
|
-
);
|
|
210
|
+
const { callsToRemove } = findBuilderMethodsToRemove(sourceFile, bindingResolver, environment);
|
|
214
211
|
const transformVisitor = (node) => {
|
|
215
212
|
if (isCallExpression(node) && isPropertyAccessExpression(node.expression)) {
|
|
216
213
|
const variable = bindingResolver.explain(node.expression);
|
|
@@ -222,11 +219,12 @@ function mkJayStackCodeSplitTransformer({
|
|
|
222
219
|
}
|
|
223
220
|
return visitEachChild(node, transformVisitor, context);
|
|
224
221
|
};
|
|
225
|
-
let transformedSourceFile = visitEachChild(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
222
|
+
let transformedSourceFile = visitEachChild(
|
|
223
|
+
sourceFile,
|
|
224
|
+
transformVisitor,
|
|
225
|
+
context
|
|
229
226
|
);
|
|
227
|
+
const { statementsToRemove, unusedImports } = analyzeUnusedStatements(transformedSourceFile);
|
|
230
228
|
const transformedStatements = transformedSourceFile.statements.map((statement) => {
|
|
231
229
|
if (statementsToRemove.has(statement)) {
|
|
232
230
|
return void 0;
|
|
@@ -256,32 +254,365 @@ function filterImportDeclaration(statement, unusedImports, factory) {
|
|
|
256
254
|
importClause,
|
|
257
255
|
importClause.isTypeOnly,
|
|
258
256
|
importClause.name,
|
|
259
|
-
factory.updateNamedImports(
|
|
260
|
-
importClause.namedBindings,
|
|
261
|
-
usedElements
|
|
262
|
-
)
|
|
257
|
+
factory.updateNamedImports(importClause.namedBindings, usedElements)
|
|
263
258
|
),
|
|
264
259
|
statement.moduleSpecifier,
|
|
265
260
|
statement.assertClause
|
|
266
261
|
);
|
|
267
262
|
}
|
|
268
|
-
|
|
269
|
-
|
|
263
|
+
const actionMetadataCache = /* @__PURE__ */ new Map();
|
|
264
|
+
function clearActionMetadataCache() {
|
|
265
|
+
actionMetadataCache.clear();
|
|
266
|
+
}
|
|
267
|
+
function isActionImport(importSource) {
|
|
268
|
+
return importSource.includes(".actions") || importSource.includes("-actions") || importSource.includes("/actions/") || importSource.endsWith("/actions");
|
|
269
|
+
}
|
|
270
|
+
function extractActionsFromSource(sourceCode, filePath) {
|
|
271
|
+
const cached = actionMetadataCache.get(filePath);
|
|
272
|
+
if (cached) {
|
|
273
|
+
return cached;
|
|
274
|
+
}
|
|
275
|
+
const actions = [];
|
|
276
|
+
const sourceFile = tsBridge.createSourceFile(
|
|
277
|
+
filePath,
|
|
278
|
+
sourceCode,
|
|
279
|
+
tsBridge.ScriptTarget.Latest,
|
|
280
|
+
true
|
|
281
|
+
);
|
|
282
|
+
function visit(node) {
|
|
283
|
+
if (tsBridge.isVariableStatement(node)) {
|
|
284
|
+
const hasExport = node.modifiers?.some(
|
|
285
|
+
(m) => m.kind === tsBridge.SyntaxKind.ExportKeyword
|
|
286
|
+
);
|
|
287
|
+
if (!hasExport) {
|
|
288
|
+
tsBridge.forEachChild(node, visit);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
for (const decl of node.declarationList.declarations) {
|
|
292
|
+
if (!tsBridge.isIdentifier(decl.name) || !decl.initializer) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const exportName = decl.name.text;
|
|
296
|
+
const actionMeta = extractActionFromExpression(decl.initializer);
|
|
297
|
+
if (actionMeta) {
|
|
298
|
+
actions.push({
|
|
299
|
+
...actionMeta,
|
|
300
|
+
exportName
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
tsBridge.forEachChild(node, visit);
|
|
306
|
+
}
|
|
307
|
+
visit(sourceFile);
|
|
308
|
+
actionMetadataCache.set(filePath, actions);
|
|
309
|
+
return actions;
|
|
310
|
+
}
|
|
311
|
+
function extractActionFromExpression(node) {
|
|
312
|
+
let current = node;
|
|
313
|
+
let method = "POST";
|
|
314
|
+
let explicitMethod = null;
|
|
315
|
+
while (tsBridge.isCallExpression(current)) {
|
|
316
|
+
const expr = current.expression;
|
|
317
|
+
if (tsBridge.isPropertyAccessExpression(expr) && expr.name.text === "withMethod") {
|
|
318
|
+
const arg = current.arguments[0];
|
|
319
|
+
if (arg && tsBridge.isStringLiteral(arg)) {
|
|
320
|
+
explicitMethod = arg.text;
|
|
321
|
+
}
|
|
322
|
+
current = expr.expression;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (tsBridge.isPropertyAccessExpression(expr) && ["withServices", "withCaching", "withHandler", "withTimeout"].includes(expr.name.text)) {
|
|
326
|
+
current = expr.expression;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (tsBridge.isIdentifier(expr)) {
|
|
330
|
+
const funcName = expr.text;
|
|
331
|
+
if (funcName === "makeJayAction" || funcName === "makeJayQuery") {
|
|
332
|
+
const nameArg = current.arguments[0];
|
|
333
|
+
if (nameArg && tsBridge.isStringLiteral(nameArg)) {
|
|
334
|
+
method = funcName === "makeJayQuery" ? "GET" : "POST";
|
|
335
|
+
if (explicitMethod) {
|
|
336
|
+
method = explicitMethod;
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
actionName: nameArg.text,
|
|
340
|
+
method
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
async function transformActionImports(code, id, resolveActionModule) {
|
|
350
|
+
if (!code.includes("import")) {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
const sourceFile = tsBridge.createSourceFile(id, code, tsBridge.ScriptTarget.Latest, true);
|
|
354
|
+
const actionImports = [];
|
|
355
|
+
for (const statement of sourceFile.statements) {
|
|
356
|
+
if (!tsBridge.isImportDeclaration(statement)) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const moduleSpecifier = statement.moduleSpecifier;
|
|
360
|
+
if (!tsBridge.isStringLiteral(moduleSpecifier)) {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
const importSource = moduleSpecifier.text;
|
|
364
|
+
if (!isActionImport(importSource)) {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const importClause = statement.importClause;
|
|
368
|
+
if (!importClause?.namedBindings || !tsBridge.isNamedImports(importClause.namedBindings)) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const namedImports = importClause.namedBindings.elements.map(
|
|
372
|
+
(el) => el.propertyName ? el.propertyName.text : el.name.text
|
|
373
|
+
);
|
|
374
|
+
actionImports.push({
|
|
375
|
+
importDecl: statement,
|
|
376
|
+
source: importSource,
|
|
377
|
+
namedImports,
|
|
378
|
+
start: statement.getStart(),
|
|
379
|
+
end: statement.getEnd()
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
if (actionImports.length === 0) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
const replacements = [];
|
|
386
|
+
let needsCreateActionCallerImport = false;
|
|
387
|
+
for (const imp of actionImports) {
|
|
388
|
+
const resolved = await resolveActionModule(imp.source, id);
|
|
389
|
+
if (!resolved) {
|
|
390
|
+
console.warn(`[action-transform] Could not resolve action module: ${imp.source}`);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
const actions = extractActionsFromSource(resolved.code, resolved.path);
|
|
394
|
+
const callerDeclarations = [];
|
|
395
|
+
for (const importName of imp.namedImports) {
|
|
396
|
+
const action = actions.find((a) => a.exportName === importName);
|
|
397
|
+
if (action) {
|
|
398
|
+
callerDeclarations.push(
|
|
399
|
+
`const ${importName} = createActionCaller('${action.actionName}', '${action.method}');`
|
|
400
|
+
);
|
|
401
|
+
needsCreateActionCallerImport = true;
|
|
402
|
+
} else {
|
|
403
|
+
console.warn(
|
|
404
|
+
`[action-transform] Export '${importName}' from ${imp.source} is not a recognized action`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (callerDeclarations.length > 0) {
|
|
409
|
+
replacements.push({
|
|
410
|
+
start: imp.start,
|
|
411
|
+
end: imp.end,
|
|
412
|
+
replacement: callerDeclarations.join("\n")
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (replacements.length === 0) {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
let result = code;
|
|
420
|
+
for (const rep of replacements.sort((a, b) => b.start - a.start)) {
|
|
421
|
+
result = result.slice(0, rep.start) + rep.replacement + result.slice(rep.end);
|
|
422
|
+
}
|
|
423
|
+
if (needsCreateActionCallerImport) {
|
|
424
|
+
const importStatement = `import { createActionCaller } from '@jay-framework/stack-client-runtime';
|
|
425
|
+
`;
|
|
426
|
+
result = importStatement + result;
|
|
427
|
+
}
|
|
428
|
+
return { code: result };
|
|
429
|
+
}
|
|
430
|
+
const SERVER_ONLY_MODULES = /* @__PURE__ */ new Set([
|
|
431
|
+
"module",
|
|
432
|
+
// createRequire
|
|
433
|
+
"fs",
|
|
434
|
+
"path",
|
|
435
|
+
"node:fs",
|
|
436
|
+
"node:path",
|
|
437
|
+
"node:module",
|
|
438
|
+
"child_process",
|
|
439
|
+
"node:child_process",
|
|
440
|
+
"crypto",
|
|
441
|
+
"node:crypto"
|
|
442
|
+
]);
|
|
443
|
+
const SERVER_ONLY_PACKAGE_PATTERNS = [
|
|
444
|
+
"@jay-framework/compiler-shared",
|
|
445
|
+
"@jay-framework/stack-server-runtime",
|
|
446
|
+
"yaml"
|
|
447
|
+
// Often used in server config
|
|
448
|
+
];
|
|
449
|
+
function createImportChainTracker(options = {}) {
|
|
450
|
+
const {
|
|
451
|
+
verbose = false,
|
|
452
|
+
additionalServerModules = [],
|
|
453
|
+
additionalServerPatterns = []
|
|
454
|
+
} = options;
|
|
455
|
+
const importChain = /* @__PURE__ */ new Map();
|
|
456
|
+
const detectedServerModules = /* @__PURE__ */ new Set();
|
|
457
|
+
const serverOnlyModules = /* @__PURE__ */ new Set([...SERVER_ONLY_MODULES, ...additionalServerModules]);
|
|
458
|
+
const serverOnlyPatterns = [...SERVER_ONLY_PACKAGE_PATTERNS, ...additionalServerPatterns];
|
|
459
|
+
function isServerOnlyModule(id) {
|
|
460
|
+
if (serverOnlyModules.has(id)) {
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
for (const pattern of serverOnlyPatterns) {
|
|
464
|
+
if (id.includes(pattern)) {
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
function buildImportChain(moduleId) {
|
|
471
|
+
const chain = [moduleId];
|
|
472
|
+
let current = moduleId;
|
|
473
|
+
for (let i = 0; i < 100; i++) {
|
|
474
|
+
const importer = importChain.get(current);
|
|
475
|
+
if (!importer)
|
|
476
|
+
break;
|
|
477
|
+
chain.push(importer);
|
|
478
|
+
current = importer;
|
|
479
|
+
}
|
|
480
|
+
return chain.reverse();
|
|
481
|
+
}
|
|
482
|
+
function formatChain(chain) {
|
|
483
|
+
return chain.map((id, idx) => {
|
|
484
|
+
const indent = " ".repeat(idx);
|
|
485
|
+
const shortId = shortenPath(id);
|
|
486
|
+
return `${indent}${idx === 0 ? "" : "↳ "}${shortId}`;
|
|
487
|
+
}).join("\n");
|
|
488
|
+
}
|
|
489
|
+
function shortenPath(id) {
|
|
490
|
+
if (id.includes("node_modules")) {
|
|
491
|
+
const parts = id.split("node_modules/");
|
|
492
|
+
return parts[parts.length - 1];
|
|
493
|
+
}
|
|
494
|
+
const cwd = process.cwd();
|
|
495
|
+
if (id.startsWith(cwd)) {
|
|
496
|
+
return id.slice(cwd.length + 1);
|
|
497
|
+
}
|
|
498
|
+
return id;
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
name: "jay-stack:import-chain-tracker",
|
|
502
|
+
enforce: "pre",
|
|
503
|
+
buildStart() {
|
|
504
|
+
importChain.clear();
|
|
505
|
+
detectedServerModules.clear();
|
|
506
|
+
if (verbose) {
|
|
507
|
+
console.log("[import-chain-tracker] Build started, tracking imports...");
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
resolveId(source, importer, options2) {
|
|
511
|
+
if (options2?.ssr) {
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
if (source.startsWith("\0")) {
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
if (importer) {
|
|
518
|
+
if (verbose) {
|
|
519
|
+
console.log(
|
|
520
|
+
`[import-chain-tracker] ${shortenPath(importer)} imports ${source}`
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return null;
|
|
525
|
+
},
|
|
526
|
+
load(id) {
|
|
527
|
+
return null;
|
|
528
|
+
},
|
|
529
|
+
transform(code, id, options2) {
|
|
530
|
+
if (options2?.ssr) {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
if (isServerOnlyModule(id)) {
|
|
534
|
+
detectedServerModules.add(id);
|
|
535
|
+
const chain = buildImportChain(id);
|
|
536
|
+
console.error(
|
|
537
|
+
`
|
|
538
|
+
[import-chain-tracker] ⚠️ Server-only module detected in client build!`
|
|
539
|
+
);
|
|
540
|
+
console.error(`Module: ${shortenPath(id)}`);
|
|
541
|
+
console.error(`Import chain:`);
|
|
542
|
+
console.error(formatChain(chain));
|
|
543
|
+
console.error("");
|
|
544
|
+
}
|
|
545
|
+
const importRegex = /import\s+(?:(?:\{[^}]*\}|[^{}\s,]+)\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
546
|
+
let match;
|
|
547
|
+
while ((match = importRegex.exec(code)) !== null) {
|
|
548
|
+
const importedModule = match[1];
|
|
549
|
+
importChain.set(importedModule, id);
|
|
550
|
+
if (isServerOnlyModule(importedModule)) {
|
|
551
|
+
if (!detectedServerModules.has(importedModule)) {
|
|
552
|
+
detectedServerModules.add(importedModule);
|
|
553
|
+
console.error(
|
|
554
|
+
`
|
|
555
|
+
[import-chain-tracker] ⚠️ Server-only import detected in client build!`
|
|
556
|
+
);
|
|
557
|
+
console.error(`Module "${importedModule}" imported by: ${shortenPath(id)}`);
|
|
558
|
+
const chain = buildImportChain(id);
|
|
559
|
+
chain.push(importedModule);
|
|
560
|
+
console.error(`Import chain:`);
|
|
561
|
+
console.error(formatChain(chain));
|
|
562
|
+
console.error("");
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return null;
|
|
567
|
+
},
|
|
568
|
+
buildEnd() {
|
|
569
|
+
if (detectedServerModules.size > 0) {
|
|
570
|
+
console.warn(
|
|
571
|
+
`
|
|
572
|
+
[import-chain-tracker] ⚠️ ${detectedServerModules.size} server-only module(s) detected during transform:`
|
|
573
|
+
);
|
|
574
|
+
for (const mod of detectedServerModules) {
|
|
575
|
+
console.warn(` - ${mod}`);
|
|
576
|
+
}
|
|
577
|
+
console.warn(
|
|
578
|
+
"\nNote: These may be stripped by the code-split transform if only used in .withServer()."
|
|
579
|
+
);
|
|
580
|
+
console.warn(
|
|
581
|
+
'If build fails with "not exported" errors, check the import chains above.\n'
|
|
582
|
+
);
|
|
583
|
+
} else if (verbose) {
|
|
584
|
+
console.log(
|
|
585
|
+
"[import-chain-tracker] ✅ No server-only modules detected in client build"
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
function jayStackCompiler(options = {}) {
|
|
592
|
+
const { trackImports, ...jayOptions } = options;
|
|
593
|
+
const moduleCache = /* @__PURE__ */ new Map();
|
|
594
|
+
const shouldTrackImports = trackImports || process.env.DEBUG_IMPORTS === "1";
|
|
595
|
+
const trackerOptions = typeof trackImports === "object" ? trackImports : { verbose: process.env.DEBUG_IMPORTS === "1" };
|
|
596
|
+
const plugins = [];
|
|
597
|
+
if (shouldTrackImports) {
|
|
598
|
+
plugins.push(createImportChainTracker(trackerOptions));
|
|
599
|
+
}
|
|
600
|
+
plugins.push(
|
|
270
601
|
// First: Jay Stack code splitting transformation
|
|
271
602
|
{
|
|
272
603
|
name: "jay-stack:code-split",
|
|
273
604
|
enforce: "pre",
|
|
274
605
|
// Run before jay:runtime
|
|
275
|
-
transform(code, id) {
|
|
276
|
-
|
|
277
|
-
const isServerBuild = id.includes("?jay-server");
|
|
278
|
-
if (!isClientBuild && !isServerBuild) {
|
|
606
|
+
transform(code, id, options2) {
|
|
607
|
+
if (!id.endsWith(".ts") && !id.includes(".ts?")) {
|
|
279
608
|
return null;
|
|
280
609
|
}
|
|
281
|
-
const
|
|
282
|
-
|
|
610
|
+
const hasComponent = code.includes("makeJayStackComponent");
|
|
611
|
+
const hasInit = code.includes("makeJayInit");
|
|
612
|
+
if (!hasComponent && !hasInit) {
|
|
283
613
|
return null;
|
|
284
614
|
}
|
|
615
|
+
const environment = options2?.ssr ? "server" : "client";
|
|
285
616
|
try {
|
|
286
617
|
return transformJayStackBuilder(code, id, environment);
|
|
287
618
|
} catch (error) {
|
|
@@ -290,11 +621,92 @@ function jayStackCompiler(jayOptions = {}) {
|
|
|
290
621
|
}
|
|
291
622
|
}
|
|
292
623
|
},
|
|
293
|
-
// Second:
|
|
624
|
+
// Second: Action import transformation (client builds only)
|
|
625
|
+
// Uses resolveId + load to replace action modules with virtual modules
|
|
626
|
+
// containing createActionCaller calls BEFORE bundling happens.
|
|
627
|
+
(() => {
|
|
628
|
+
let isSSRBuild = false;
|
|
629
|
+
return {
|
|
630
|
+
name: "jay-stack:action-transform",
|
|
631
|
+
enforce: "pre",
|
|
632
|
+
// Track SSR mode from config
|
|
633
|
+
configResolved(config) {
|
|
634
|
+
isSSRBuild = config.build?.ssr ?? false;
|
|
635
|
+
},
|
|
636
|
+
buildStart() {
|
|
637
|
+
clearActionMetadataCache();
|
|
638
|
+
moduleCache.clear();
|
|
639
|
+
},
|
|
640
|
+
async resolveId(source, importer, options2) {
|
|
641
|
+
if (options2?.ssr || isSSRBuild) {
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
if (!isActionImport(source)) {
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
if (!source.startsWith(".") || !importer) {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
const importerDir = path.dirname(importer);
|
|
651
|
+
let resolvedPath = path.resolve(importerDir, source);
|
|
652
|
+
if (!resolvedPath.endsWith(".ts") && !resolvedPath.endsWith(".js")) {
|
|
653
|
+
if (fs.existsSync(resolvedPath + ".ts")) {
|
|
654
|
+
resolvedPath += ".ts";
|
|
655
|
+
} else if (fs.existsSync(resolvedPath + ".js")) {
|
|
656
|
+
resolvedPath += ".js";
|
|
657
|
+
} else {
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return `\0jay-action:${resolvedPath}`;
|
|
662
|
+
},
|
|
663
|
+
async load(id) {
|
|
664
|
+
if (!id.startsWith("\0jay-action:")) {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
const actualPath = id.slice("\0jay-action:".length);
|
|
668
|
+
let code;
|
|
669
|
+
try {
|
|
670
|
+
code = await fs.promises.readFile(actualPath, "utf-8");
|
|
671
|
+
} catch (err) {
|
|
672
|
+
console.error(`[action-transform] Could not read ${actualPath}:`, err);
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
const actions = extractActionsFromSource(code, actualPath);
|
|
676
|
+
if (actions.length === 0) {
|
|
677
|
+
console.warn(`[action-transform] No actions found in ${actualPath}`);
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
const lines = [
|
|
681
|
+
`import { createActionCaller } from '@jay-framework/stack-client-runtime';`,
|
|
682
|
+
""
|
|
683
|
+
];
|
|
684
|
+
for (const action of actions) {
|
|
685
|
+
lines.push(
|
|
686
|
+
`export const ${action.exportName} = createActionCaller('${action.actionName}', '${action.method}');`
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
if (code.includes("ActionError")) {
|
|
690
|
+
lines.push(
|
|
691
|
+
`export { ActionError } from '@jay-framework/stack-client-runtime';`
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
const result = lines.join("\n");
|
|
695
|
+
return result;
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
})(),
|
|
699
|
+
// Third: Jay runtime compilation (existing plugin)
|
|
294
700
|
jayRuntime(jayOptions)
|
|
295
|
-
|
|
701
|
+
);
|
|
702
|
+
return plugins;
|
|
296
703
|
}
|
|
297
704
|
export {
|
|
705
|
+
clearActionMetadataCache,
|
|
706
|
+
createImportChainTracker,
|
|
707
|
+
extractActionsFromSource,
|
|
708
|
+
isActionImport,
|
|
298
709
|
jayStackCompiler,
|
|
710
|
+
transformActionImports,
|
|
299
711
|
transformJayStackBuilder
|
|
300
712
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jay-framework/compiler-jay-stack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -27,16 +27,17 @@
|
|
|
27
27
|
"test:watch": "vitest"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@jay-framework/compiler": "^0.
|
|
31
|
-
"@jay-framework/
|
|
32
|
-
"@jay-framework/
|
|
30
|
+
"@jay-framework/compiler": "^0.10.0",
|
|
31
|
+
"@jay-framework/compiler-shared": "^0.10.0",
|
|
32
|
+
"@jay-framework/typescript-bridge": "^0.5.0",
|
|
33
|
+
"@jay-framework/vite-plugin": "^0.10.0",
|
|
34
|
+
"typescript": "^5.3.3",
|
|
33
35
|
"vite": "^5.0.11"
|
|
34
36
|
},
|
|
35
37
|
"devDependencies": {
|
|
36
|
-
"@jay-framework/dev-environment": "^0.
|
|
38
|
+
"@jay-framework/dev-environment": "^0.10.0",
|
|
37
39
|
"rimraf": "^5.0.5",
|
|
38
40
|
"tsup": "^8.0.1",
|
|
39
|
-
"typescript": "^5.3.3",
|
|
40
41
|
"vitest": "^1.2.1"
|
|
41
42
|
}
|
|
42
43
|
}
|