@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 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
- * @param jayOptions - Configuration for Jay runtime (passed to jay:runtime plugin)
37
- * @returns Array of Vite plugins [codeSplitPlugin, jayRuntimePlugin]
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(jayOptions?: JayRollupConfig): Plugin[];
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
- const SERVER_METHODS = /* @__PURE__ */ new Set([
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 CLIENT_METHODS = /* @__PURE__ */ new Set([
11
- "withInteractive",
12
- "withContexts"
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
- return environment === "client" && SERVER_METHODS.has(methodName) || environment === "server" && CLIENT_METHODS.has(methodName);
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] === "makeJayStackComponent" && isImportModuleVariableRoot(flattened.root) && isStringLiteral(flattened.root.module) && flattened.root.module.text === "@jay-framework/fullstack-component")
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: isFunctionDeclaration$1,
76
- isVariableStatement: isVariableStatement$1,
77
- isInterfaceDeclaration: isInterfaceDeclaration$1,
78
- isTypeAliasDeclaration: isTypeAliasDeclaration$1,
85
+ isFunctionDeclaration,
86
+ isVariableStatement,
87
+ isInterfaceDeclaration,
88
+ isTypeAliasDeclaration,
79
89
  isClassDeclaration,
80
90
  isEnumDeclaration,
81
91
  SyntaxKind
82
92
  } = tsBridge;
83
- function analyzeUnusedStatements(sourceFile, bindingResolver) {
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$1(statement) && statement.name) {
154
+ if (isFunctionDeclaration(statement) && statement.name) {
145
155
  return statement.name.text;
146
156
  }
147
- if (isVariableStatement$1(statement)) {
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$1(statement) && statement.name) {
163
+ if (isInterfaceDeclaration(statement) && statement.name) {
154
164
  return statement.name.text;
155
165
  }
156
- if (isTypeAliasDeclaration$1(statement) && statement.name) {
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, removedVariables } = findBuilderMethodsToRemove(
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(sourceFile, transformVisitor, context);
226
- new SourceFileBindingResolver(transformedSourceFile);
227
- const { statementsToRemove, unusedImports } = analyzeUnusedStatements(
228
- transformedSourceFile
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
- function jayStackCompiler(jayOptions = {}) {
269
- return [
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
- const isClientBuild = id.includes("?jay-client");
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 environment = isClientBuild ? "client" : "server";
282
- if (!id.endsWith(".ts") && !id.includes(".ts?")) {
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: Jay runtime compilation (existing plugin)
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.9.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.9.0",
31
- "@jay-framework/typescript-bridge": "^0.4.0",
32
- "@jay-framework/vite-plugin": "^0.9.0",
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.9.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
  }