@jay-framework/compiler-jay-stack 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,190 @@ 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
+ /**
112
+ * Plugin Client Import Resolver
113
+ *
114
+ * Transforms imports from Jay plugin packages to use their /client subpath
115
+ * when in client build mode.
116
+ *
117
+ * This handles transitive plugin dependencies: when wix-stores imports from
118
+ * wix-server-client, the import should be rewritten to wix-server-client/client
119
+ * in client builds.
120
+ *
121
+ * Uses a `transform` hook instead of `resolveId` to ensure the rewrite happens
122
+ * before rollup's `external` option is evaluated.
123
+ *
124
+ * Detection:
125
+ * 1. Check if the imported package has a plugin.yaml (is a Jay plugin)
126
+ * 2. Check if the package exports a /client subpath
127
+ * 3. If both true, rewrite the import to use /client
128
+ */
129
+
130
+ /**
131
+ * Interface for detecting if a package is a Jay plugin with /client export.
132
+ * Extracted to allow mocking in tests.
133
+ */
134
+ interface PluginDetector {
135
+ /**
136
+ * Checks if a package is a Jay plugin with a /client export.
137
+ * @param packageName - The package name (e.g., '@jay-framework/wix-stores')
138
+ * @param projectRoot - The project root for resolution
139
+ * @returns true if the package should be rewritten to /client
140
+ */
141
+ isJayPluginWithClientExport(packageName: string, projectRoot: string): boolean;
142
+ }
143
+ /**
144
+ * Default implementation using Node's require.resolve.
145
+ */
146
+ declare function createDefaultPluginDetector(): PluginDetector;
147
+ /**
148
+ * Extracts the package name from an import source.
149
+ * Handles scoped packages like @jay-framework/wix-stores.
150
+ */
151
+ declare function extractPackageName(source: string): string | null;
152
+ /**
153
+ * Checks if the import is already using a subpath (not just the main entry).
154
+ */
155
+ declare function isSubpathImport(source: string, packageName: string): boolean;
156
+ interface TransformImportsOptions {
157
+ /** The source code to transform */
158
+ code: string;
159
+ /** Project root for plugin detection */
160
+ projectRoot: string;
161
+ /** File path for logging */
162
+ filePath: string;
163
+ /** Plugin detector (injectable for testing) */
164
+ pluginDetector: PluginDetector;
165
+ /** Enable verbose logging */
166
+ verbose?: boolean;
167
+ }
168
+ interface TransformImportsResult {
169
+ /** The transformed code */
170
+ code: string;
171
+ /** Whether any changes were made */
172
+ hasChanges: boolean;
173
+ }
174
+ /**
175
+ * Transforms import/export declarations in source code.
176
+ * Rewrites plugin package imports to use /client subpath.
177
+ *
178
+ * This is a pure function - all IO is handled by the pluginDetector.
179
+ */
180
+ declare function transformImports(options: TransformImportsOptions): TransformImportsResult;
181
+ interface PluginClientImportResolverOptions {
182
+ /** Project root directory for resolution */
183
+ projectRoot?: string;
184
+ /** Enable verbose logging */
185
+ verbose?: boolean;
186
+ /** Custom plugin detector (for testing) */
187
+ pluginDetector?: PluginDetector;
188
+ }
189
+ /**
190
+ * Creates a Vite plugin that transforms plugin package imports to /client
191
+ * in client builds.
192
+ *
193
+ * Uses the `transform` hook to rewrite import declarations before rollup's
194
+ * external option is evaluated.
195
+ */
196
+ declare function createPluginClientImportResolver(options?: PluginClientImportResolverOptions): Plugin;
197
+
198
+ interface JayStackCompilerOptions extends JayRollupConfig {
199
+ /**
200
+ * Enable import chain tracking for debugging server code leaking into client builds.
201
+ * When enabled, logs the full import chain when server-only modules are detected.
202
+ * @default false (but auto-enabled when DEBUG_IMPORTS=1 env var is set)
203
+ */
204
+ trackImports?: boolean | ImportChainTrackerOptions;
205
+ }
19
206
  /**
20
207
  * Jay Stack Compiler - Handles both Jay runtime compilation and Jay Stack code splitting
21
208
  *
22
209
  * This plugin internally uses the jay:runtime plugin and adds Jay Stack-specific
23
210
  * transformations for client/server code splitting.
24
211
  *
212
+ * Environment detection is based on Vite's `options.ssr`:
213
+ * - `options.ssr = true` → server build (strip client code)
214
+ * - `options.ssr = false/undefined` → client build (strip server code)
215
+ *
216
+ * This works for both:
217
+ * - Dev server: SSR renders with server code, browser hydrates with client code
218
+ * - Package builds: Use `build.ssr = true/false` to control environment
219
+ *
25
220
  * Usage:
26
221
  * ```typescript
27
222
  * import { jayStackCompiler } from '@jay-framework/compiler-jay-stack';
@@ -33,9 +228,19 @@ declare function transformJayStackBuilder(code: string, filePath: string, enviro
33
228
  * });
34
229
  * ```
35
230
  *
36
- * @param jayOptions - Configuration for Jay runtime (passed to jay:runtime plugin)
37
- * @returns Array of Vite plugins [codeSplitPlugin, jayRuntimePlugin]
231
+ * To debug import chain issues (server code leaking to client):
232
+ * ```bash
233
+ * DEBUG_IMPORTS=1 npm run build
234
+ * ```
235
+ *
236
+ * Or enable in config:
237
+ * ```typescript
238
+ * ...jayStackCompiler({ trackImports: true })
239
+ * ```
240
+ *
241
+ * @param options - Configuration for Jay Stack compiler
242
+ * @returns Array of Vite plugins
38
243
  */
39
- declare function jayStackCompiler(jayOptions?: JayRollupConfig): Plugin[];
244
+ declare function jayStackCompiler(options?: JayStackCompilerOptions): Plugin[];
40
245
 
41
- export { type BuildEnvironment, jayStackCompiler, transformJayStackBuilder };
246
+ export { type ActionMetadata, type BuildEnvironment, type ExtractedActions, type ImportChainTrackerOptions, type JayStackCompilerOptions, type PluginClientImportResolverOptions, type PluginDetector, type TransformImportsOptions, type TransformImportsResult, clearActionMetadataCache, createDefaultPluginDetector, createImportChainTracker, createPluginClientImportResolver, extractActionsFromSource, extractPackageName, isActionImport, isSubpathImport, jayStackCompiler, transformActionImports, transformImports, transformJayStackBuilder };
package/dist/index.js CHANGED
@@ -1,18 +1,28 @@
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 path from "node:path";
5
+ import { createRequire } from "node:module";
6
+ import * as fs from "node:fs";
7
+ const COMPONENT_SERVER_METHODS = /* @__PURE__ */ new Set([
5
8
  "withServices",
6
9
  "withLoadParams",
7
10
  "withSlowlyRender",
8
11
  "withFastRender"
9
12
  ]);
10
- const CLIENT_METHODS = /* @__PURE__ */ new Set([
11
- "withInteractive",
12
- "withContexts"
13
- ]);
13
+ const COMPONENT_CLIENT_METHODS = /* @__PURE__ */ new Set(["withInteractive", "withContexts"]);
14
+ const INIT_SERVER_METHODS = /* @__PURE__ */ new Set(["withServer"]);
15
+ const INIT_CLIENT_METHODS = /* @__PURE__ */ new Set(["withClient"]);
14
16
  function shouldRemoveMethod(methodName, environment) {
15
- return environment === "client" && SERVER_METHODS.has(methodName) || environment === "server" && CLIENT_METHODS.has(methodName);
17
+ if (environment === "client" && COMPONENT_SERVER_METHODS.has(methodName))
18
+ return true;
19
+ if (environment === "server" && COMPONENT_CLIENT_METHODS.has(methodName))
20
+ return true;
21
+ if (environment === "client" && INIT_SERVER_METHODS.has(methodName))
22
+ return true;
23
+ if (environment === "server" && INIT_CLIENT_METHODS.has(methodName))
24
+ return true;
25
+ return false;
16
26
  }
17
27
  const { isCallExpression: isCallExpression$1, isPropertyAccessExpression: isPropertyAccessExpression$1, isIdentifier: isIdentifier$2, isStringLiteral } = tsBridge;
18
28
  function findBuilderMethodsToRemove(sourceFile, bindingResolver, environment) {
@@ -33,6 +43,7 @@ function findBuilderMethodsToRemove(sourceFile, bindingResolver, environment) {
33
43
  sourceFile.forEachChild(visit);
34
44
  return { callsToRemove, removedVariables };
35
45
  }
46
+ const JAY_BUILDER_FUNCTIONS = /* @__PURE__ */ new Set(["makeJayStackComponent", "makeJayInit"]);
36
47
  function isPartOfJayStackChain(callExpr, bindingResolver) {
37
48
  let current = callExpr.expression;
38
49
  while (true) {
@@ -42,7 +53,7 @@ function isPartOfJayStackChain(callExpr, bindingResolver) {
42
53
  if (isIdentifier$2(current.expression)) {
43
54
  const variable = bindingResolver.explain(current.expression);
44
55
  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")
56
+ 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
57
  return true;
47
58
  }
48
59
  if (isPropertyAccessExpression$1(current.expression)) {
@@ -72,15 +83,15 @@ function collectVariablesFromArguments(args, bindingResolver, variables) {
72
83
  const {
73
84
  isIdentifier: isIdentifier$1,
74
85
  isImportDeclaration: isImportDeclaration$1,
75
- isFunctionDeclaration: isFunctionDeclaration$1,
76
- isVariableStatement: isVariableStatement$1,
77
- isInterfaceDeclaration: isInterfaceDeclaration$1,
78
- isTypeAliasDeclaration: isTypeAliasDeclaration$1,
86
+ isFunctionDeclaration,
87
+ isVariableStatement,
88
+ isInterfaceDeclaration,
89
+ isTypeAliasDeclaration,
79
90
  isClassDeclaration,
80
91
  isEnumDeclaration,
81
92
  SyntaxKind
82
93
  } = tsBridge;
83
- function analyzeUnusedStatements(sourceFile, bindingResolver) {
94
+ function analyzeUnusedStatements(sourceFile) {
84
95
  const statementsToRemove = /* @__PURE__ */ new Set();
85
96
  const collectUsedIdentifiers = () => {
86
97
  const used = /* @__PURE__ */ new Set();
@@ -141,19 +152,19 @@ function isExportStatement(statement) {
141
152
  return false;
142
153
  }
143
154
  function getStatementDefinedName(statement) {
144
- if (isFunctionDeclaration$1(statement) && statement.name) {
155
+ if (isFunctionDeclaration(statement) && statement.name) {
145
156
  return statement.name.text;
146
157
  }
147
- if (isVariableStatement$1(statement)) {
158
+ if (isVariableStatement(statement)) {
148
159
  const firstDecl = statement.declarationList.declarations[0];
149
160
  if (firstDecl && isIdentifier$1(firstDecl.name)) {
150
161
  return firstDecl.name.text;
151
162
  }
152
163
  }
153
- if (isInterfaceDeclaration$1(statement) && statement.name) {
164
+ if (isInterfaceDeclaration(statement) && statement.name) {
154
165
  return statement.name.text;
155
166
  }
156
- if (isTypeAliasDeclaration$1(statement) && statement.name) {
167
+ if (isTypeAliasDeclaration(statement) && statement.name) {
157
168
  return statement.name.text;
158
169
  }
159
170
  if (isClassDeclaration(statement) && statement.name) {
@@ -173,19 +184,10 @@ const {
173
184
  isPropertyAccessExpression,
174
185
  isImportDeclaration,
175
186
  isNamedImports,
176
- isIdentifier,
177
- isFunctionDeclaration,
178
- isVariableStatement,
179
- isInterfaceDeclaration,
180
- isTypeAliasDeclaration
187
+ isIdentifier
181
188
  } = tsBridge;
182
189
  function transformJayStackBuilder(code, filePath, environment) {
183
- const sourceFile = createSourceFile(
184
- filePath,
185
- code,
186
- ScriptTarget.Latest,
187
- true
188
- );
190
+ const sourceFile = createSourceFile(filePath, code, ScriptTarget.Latest, true);
189
191
  const transformers = [mkTransformer(mkJayStackCodeSplitTransformer, { environment })];
190
192
  const printer = createPrinter();
191
193
  const result = tsBridge.transform(sourceFile, transformers);
@@ -206,11 +208,7 @@ function mkJayStackCodeSplitTransformer({
206
208
  environment
207
209
  }) {
208
210
  const bindingResolver = new SourceFileBindingResolver(sourceFile);
209
- const { callsToRemove, removedVariables } = findBuilderMethodsToRemove(
210
- sourceFile,
211
- bindingResolver,
212
- environment
213
- );
211
+ const { callsToRemove } = findBuilderMethodsToRemove(sourceFile, bindingResolver, environment);
214
212
  const transformVisitor = (node) => {
215
213
  if (isCallExpression(node) && isPropertyAccessExpression(node.expression)) {
216
214
  const variable = bindingResolver.explain(node.expression);
@@ -222,11 +220,12 @@ function mkJayStackCodeSplitTransformer({
222
220
  }
223
221
  return visitEachChild(node, transformVisitor, context);
224
222
  };
225
- let transformedSourceFile = visitEachChild(sourceFile, transformVisitor, context);
226
- new SourceFileBindingResolver(transformedSourceFile);
227
- const { statementsToRemove, unusedImports } = analyzeUnusedStatements(
228
- transformedSourceFile
223
+ let transformedSourceFile = visitEachChild(
224
+ sourceFile,
225
+ transformVisitor,
226
+ context
229
227
  );
228
+ const { statementsToRemove, unusedImports } = analyzeUnusedStatements(transformedSourceFile);
230
229
  const transformedStatements = transformedSourceFile.statements.map((statement) => {
231
230
  if (statementsToRemove.has(statement)) {
232
231
  return void 0;
@@ -256,32 +255,490 @@ function filterImportDeclaration(statement, unusedImports, factory) {
256
255
  importClause,
257
256
  importClause.isTypeOnly,
258
257
  importClause.name,
259
- factory.updateNamedImports(
260
- importClause.namedBindings,
261
- usedElements
262
- )
258
+ factory.updateNamedImports(importClause.namedBindings, usedElements)
263
259
  ),
264
260
  statement.moduleSpecifier,
265
261
  statement.assertClause
266
262
  );
267
263
  }
268
- function jayStackCompiler(jayOptions = {}) {
269
- return [
264
+ const actionMetadataCache = /* @__PURE__ */ new Map();
265
+ function clearActionMetadataCache() {
266
+ actionMetadataCache.clear();
267
+ }
268
+ function isActionImport(importSource) {
269
+ return importSource.includes(".actions") || importSource.includes("-actions") || importSource.includes("/actions/") || importSource.endsWith("/actions");
270
+ }
271
+ function extractActionsFromSource(sourceCode, filePath) {
272
+ const cached = actionMetadataCache.get(filePath);
273
+ if (cached) {
274
+ return cached;
275
+ }
276
+ const actions = [];
277
+ const sourceFile = tsBridge.createSourceFile(
278
+ filePath,
279
+ sourceCode,
280
+ tsBridge.ScriptTarget.Latest,
281
+ true
282
+ );
283
+ function visit(node) {
284
+ if (tsBridge.isVariableStatement(node)) {
285
+ const hasExport = node.modifiers?.some(
286
+ (m) => m.kind === tsBridge.SyntaxKind.ExportKeyword
287
+ );
288
+ if (!hasExport) {
289
+ tsBridge.forEachChild(node, visit);
290
+ return;
291
+ }
292
+ for (const decl of node.declarationList.declarations) {
293
+ if (!tsBridge.isIdentifier(decl.name) || !decl.initializer) {
294
+ continue;
295
+ }
296
+ const exportName = decl.name.text;
297
+ const actionMeta = extractActionFromExpression(decl.initializer);
298
+ if (actionMeta) {
299
+ actions.push({
300
+ ...actionMeta,
301
+ exportName
302
+ });
303
+ }
304
+ }
305
+ }
306
+ tsBridge.forEachChild(node, visit);
307
+ }
308
+ visit(sourceFile);
309
+ actionMetadataCache.set(filePath, actions);
310
+ return actions;
311
+ }
312
+ function extractActionFromExpression(node) {
313
+ let current = node;
314
+ let method = "POST";
315
+ let explicitMethod = null;
316
+ while (tsBridge.isCallExpression(current)) {
317
+ const expr = current.expression;
318
+ if (tsBridge.isPropertyAccessExpression(expr) && expr.name.text === "withMethod") {
319
+ const arg = current.arguments[0];
320
+ if (arg && tsBridge.isStringLiteral(arg)) {
321
+ explicitMethod = arg.text;
322
+ }
323
+ current = expr.expression;
324
+ continue;
325
+ }
326
+ if (tsBridge.isPropertyAccessExpression(expr) && ["withServices", "withCaching", "withHandler", "withTimeout"].includes(expr.name.text)) {
327
+ current = expr.expression;
328
+ continue;
329
+ }
330
+ if (tsBridge.isIdentifier(expr)) {
331
+ const funcName = expr.text;
332
+ if (funcName === "makeJayAction" || funcName === "makeJayQuery") {
333
+ const nameArg = current.arguments[0];
334
+ if (nameArg && tsBridge.isStringLiteral(nameArg)) {
335
+ method = funcName === "makeJayQuery" ? "GET" : "POST";
336
+ if (explicitMethod) {
337
+ method = explicitMethod;
338
+ }
339
+ return {
340
+ actionName: nameArg.text,
341
+ method
342
+ };
343
+ }
344
+ }
345
+ }
346
+ break;
347
+ }
348
+ return null;
349
+ }
350
+ async function transformActionImports(code, id, resolveActionModule) {
351
+ if (!code.includes("import")) {
352
+ return null;
353
+ }
354
+ const sourceFile = tsBridge.createSourceFile(id, code, tsBridge.ScriptTarget.Latest, true);
355
+ const actionImports = [];
356
+ for (const statement of sourceFile.statements) {
357
+ if (!tsBridge.isImportDeclaration(statement)) {
358
+ continue;
359
+ }
360
+ const moduleSpecifier = statement.moduleSpecifier;
361
+ if (!tsBridge.isStringLiteral(moduleSpecifier)) {
362
+ continue;
363
+ }
364
+ const importSource = moduleSpecifier.text;
365
+ if (!isActionImport(importSource)) {
366
+ continue;
367
+ }
368
+ const importClause = statement.importClause;
369
+ if (!importClause?.namedBindings || !tsBridge.isNamedImports(importClause.namedBindings)) {
370
+ continue;
371
+ }
372
+ const namedImports = importClause.namedBindings.elements.map(
373
+ (el) => el.propertyName ? el.propertyName.text : el.name.text
374
+ );
375
+ actionImports.push({
376
+ importDecl: statement,
377
+ source: importSource,
378
+ namedImports,
379
+ start: statement.getStart(),
380
+ end: statement.getEnd()
381
+ });
382
+ }
383
+ if (actionImports.length === 0) {
384
+ return null;
385
+ }
386
+ const replacements = [];
387
+ let needsCreateActionCallerImport = false;
388
+ for (const imp of actionImports) {
389
+ const resolved = await resolveActionModule(imp.source, id);
390
+ if (!resolved) {
391
+ console.warn(`[action-transform] Could not resolve action module: ${imp.source}`);
392
+ continue;
393
+ }
394
+ const actions = extractActionsFromSource(resolved.code, resolved.path);
395
+ const callerDeclarations = [];
396
+ for (const importName of imp.namedImports) {
397
+ const action = actions.find((a) => a.exportName === importName);
398
+ if (action) {
399
+ callerDeclarations.push(
400
+ `const ${importName} = createActionCaller('${action.actionName}', '${action.method}');`
401
+ );
402
+ needsCreateActionCallerImport = true;
403
+ } else {
404
+ console.warn(
405
+ `[action-transform] Export '${importName}' from ${imp.source} is not a recognized action`
406
+ );
407
+ }
408
+ }
409
+ if (callerDeclarations.length > 0) {
410
+ replacements.push({
411
+ start: imp.start,
412
+ end: imp.end,
413
+ replacement: callerDeclarations.join("\n")
414
+ });
415
+ }
416
+ }
417
+ if (replacements.length === 0) {
418
+ return null;
419
+ }
420
+ let result = code;
421
+ for (const rep of replacements.sort((a, b) => b.start - a.start)) {
422
+ result = result.slice(0, rep.start) + rep.replacement + result.slice(rep.end);
423
+ }
424
+ if (needsCreateActionCallerImport) {
425
+ const importStatement = `import { createActionCaller } from '@jay-framework/stack-client-runtime';
426
+ `;
427
+ result = importStatement + result;
428
+ }
429
+ return { code: result };
430
+ }
431
+ const SERVER_ONLY_MODULES = /* @__PURE__ */ new Set([
432
+ "module",
433
+ // createRequire
434
+ "fs",
435
+ "path",
436
+ "node:fs",
437
+ "node:path",
438
+ "node:module",
439
+ "child_process",
440
+ "node:child_process",
441
+ "crypto",
442
+ "node:crypto"
443
+ ]);
444
+ const SERVER_ONLY_PACKAGE_PATTERNS = [
445
+ "@jay-framework/compiler-shared",
446
+ "@jay-framework/stack-server-runtime",
447
+ "yaml"
448
+ // Often used in server config
449
+ ];
450
+ function createImportChainTracker(options = {}) {
451
+ const {
452
+ verbose = false,
453
+ additionalServerModules = [],
454
+ additionalServerPatterns = []
455
+ } = options;
456
+ const importChain = /* @__PURE__ */ new Map();
457
+ const detectedServerModules = /* @__PURE__ */ new Set();
458
+ const serverOnlyModules = /* @__PURE__ */ new Set([...SERVER_ONLY_MODULES, ...additionalServerModules]);
459
+ const serverOnlyPatterns = [...SERVER_ONLY_PACKAGE_PATTERNS, ...additionalServerPatterns];
460
+ function isServerOnlyModule(id) {
461
+ if (serverOnlyModules.has(id)) {
462
+ return true;
463
+ }
464
+ for (const pattern of serverOnlyPatterns) {
465
+ if (id.includes(pattern)) {
466
+ return true;
467
+ }
468
+ }
469
+ return false;
470
+ }
471
+ function buildImportChain(moduleId) {
472
+ const chain = [moduleId];
473
+ let current = moduleId;
474
+ for (let i = 0; i < 100; i++) {
475
+ const importer = importChain.get(current);
476
+ if (!importer)
477
+ break;
478
+ chain.push(importer);
479
+ current = importer;
480
+ }
481
+ return chain.reverse();
482
+ }
483
+ function formatChain(chain) {
484
+ return chain.map((id, idx) => {
485
+ const indent = " ".repeat(idx);
486
+ const shortId = shortenPath(id);
487
+ return `${indent}${idx === 0 ? "" : "↳ "}${shortId}`;
488
+ }).join("\n");
489
+ }
490
+ function shortenPath(id) {
491
+ if (id.includes("node_modules")) {
492
+ const parts = id.split("node_modules/");
493
+ return parts[parts.length - 1];
494
+ }
495
+ const cwd = process.cwd();
496
+ if (id.startsWith(cwd)) {
497
+ return id.slice(cwd.length + 1);
498
+ }
499
+ return id;
500
+ }
501
+ return {
502
+ name: "jay-stack:import-chain-tracker",
503
+ enforce: "pre",
504
+ buildStart() {
505
+ importChain.clear();
506
+ detectedServerModules.clear();
507
+ if (verbose) {
508
+ console.log("[import-chain-tracker] Build started, tracking imports...");
509
+ }
510
+ },
511
+ resolveId(source, importer, options2) {
512
+ if (options2?.ssr) {
513
+ return null;
514
+ }
515
+ if (source.startsWith("\0")) {
516
+ return null;
517
+ }
518
+ if (importer) {
519
+ if (verbose) {
520
+ console.log(
521
+ `[import-chain-tracker] ${shortenPath(importer)} imports ${source}`
522
+ );
523
+ }
524
+ }
525
+ return null;
526
+ },
527
+ load(id) {
528
+ return null;
529
+ },
530
+ transform(code, id, options2) {
531
+ if (options2?.ssr) {
532
+ return null;
533
+ }
534
+ if (isServerOnlyModule(id)) {
535
+ detectedServerModules.add(id);
536
+ const chain = buildImportChain(id);
537
+ console.error(
538
+ `
539
+ [import-chain-tracker] ⚠️ Server-only module detected in client build!`
540
+ );
541
+ console.error(`Module: ${shortenPath(id)}`);
542
+ console.error(`Import chain:`);
543
+ console.error(formatChain(chain));
544
+ console.error("");
545
+ }
546
+ const importRegex = /import\s+(?:(?:\{[^}]*\}|[^{}\s,]+)\s+from\s+)?['"]([^'"]+)['"]/g;
547
+ let match;
548
+ while ((match = importRegex.exec(code)) !== null) {
549
+ const importedModule = match[1];
550
+ importChain.set(importedModule, id);
551
+ if (isServerOnlyModule(importedModule)) {
552
+ if (!detectedServerModules.has(importedModule)) {
553
+ detectedServerModules.add(importedModule);
554
+ console.error(
555
+ `
556
+ [import-chain-tracker] ⚠️ Server-only import detected in client build!`
557
+ );
558
+ console.error(`Module "${importedModule}" imported by: ${shortenPath(id)}`);
559
+ const chain = buildImportChain(id);
560
+ chain.push(importedModule);
561
+ console.error(`Import chain:`);
562
+ console.error(formatChain(chain));
563
+ console.error("");
564
+ }
565
+ }
566
+ }
567
+ return null;
568
+ },
569
+ buildEnd() {
570
+ if (detectedServerModules.size > 0) {
571
+ console.warn(
572
+ `
573
+ [import-chain-tracker] ⚠️ ${detectedServerModules.size} server-only module(s) detected during transform:`
574
+ );
575
+ for (const mod of detectedServerModules) {
576
+ console.warn(` - ${mod}`);
577
+ }
578
+ console.warn(
579
+ "\nNote: These may be stripped by the code-split transform if only used in .withServer()."
580
+ );
581
+ console.warn(
582
+ 'If build fails with "not exported" errors, check the import chains above.\n'
583
+ );
584
+ } else if (verbose) {
585
+ console.log(
586
+ "[import-chain-tracker] ✅ No server-only modules detected in client build"
587
+ );
588
+ }
589
+ }
590
+ };
591
+ }
592
+ const require2 = createRequire(import.meta.url);
593
+ function createDefaultPluginDetector() {
594
+ const cache = /* @__PURE__ */ new Map();
595
+ return {
596
+ isJayPluginWithClientExport(packageName, projectRoot) {
597
+ const cacheKey = `${packageName}:${projectRoot}`;
598
+ if (cache.has(cacheKey)) {
599
+ return cache.get(cacheKey);
600
+ }
601
+ let result = false;
602
+ try {
603
+ require2.resolve(`${packageName}/plugin.yaml`, { paths: [projectRoot] });
604
+ try {
605
+ require2.resolve(`${packageName}/client`, { paths: [projectRoot] });
606
+ result = true;
607
+ } catch {
608
+ result = false;
609
+ }
610
+ } catch {
611
+ result = false;
612
+ }
613
+ cache.set(cacheKey, result);
614
+ return result;
615
+ }
616
+ };
617
+ }
618
+ function extractPackageName(source) {
619
+ if (source.startsWith(".") || source.startsWith("/")) {
620
+ return null;
621
+ }
622
+ if (source.startsWith("@")) {
623
+ const parts2 = source.split("/");
624
+ if (parts2.length >= 2) {
625
+ return `${parts2[0]}/${parts2[1]}`;
626
+ }
627
+ return null;
628
+ }
629
+ const parts = source.split("/");
630
+ return parts[0];
631
+ }
632
+ function isSubpathImport(source, packageName) {
633
+ return source.length > packageName.length && source[packageName.length] === "/";
634
+ }
635
+ const IMPORT_REGEX = /import\s+(.+?)\s+from\s+(['"])([^'"]+)\2/g;
636
+ const EXPORT_FROM_REGEX = /export\s+(.+?)\s+from\s+(['"])([^'"]+)\2/g;
637
+ function transformImports(options) {
638
+ const { code, projectRoot, filePath, pluginDetector, verbose = false } = options;
639
+ let hasChanges = false;
640
+ let result = code;
641
+ result = result.replace(IMPORT_REGEX, (match, clause, quote, source) => {
642
+ const packageName = extractPackageName(source);
643
+ if (!packageName)
644
+ return match;
645
+ if (isSubpathImport(source, packageName))
646
+ return match;
647
+ if (!pluginDetector.isJayPluginWithClientExport(packageName, projectRoot))
648
+ return match;
649
+ hasChanges = true;
650
+ const newSource = `${packageName}/client`;
651
+ if (verbose) {
652
+ console.log(
653
+ `[plugin-client-import] Rewriting import ${source} -> ${newSource} (in ${path.basename(filePath)})`
654
+ );
655
+ }
656
+ return `import ${clause} from ${quote}${newSource}${quote}`;
657
+ });
658
+ result = result.replace(EXPORT_FROM_REGEX, (match, clause, quote, source) => {
659
+ const packageName = extractPackageName(source);
660
+ if (!packageName)
661
+ return match;
662
+ if (isSubpathImport(source, packageName))
663
+ return match;
664
+ if (!pluginDetector.isJayPluginWithClientExport(packageName, projectRoot))
665
+ return match;
666
+ hasChanges = true;
667
+ const newSource = `${packageName}/client`;
668
+ if (verbose) {
669
+ console.log(
670
+ `[plugin-client-import] Rewriting export ${source} -> ${newSource} (in ${path.basename(filePath)})`
671
+ );
672
+ }
673
+ return `export ${clause} from ${quote}${newSource}${quote}`;
674
+ });
675
+ return { code: result, hasChanges };
676
+ }
677
+ function createPluginClientImportResolver(options = {}) {
678
+ const { verbose = false } = options;
679
+ let projectRoot = options.projectRoot || process.cwd();
680
+ let isSSRBuild = false;
681
+ const pluginDetector = options.pluginDetector || createDefaultPluginDetector();
682
+ return {
683
+ name: "jay-stack:plugin-client-import",
684
+ enforce: "pre",
685
+ configResolved(config) {
686
+ projectRoot = config.root || projectRoot;
687
+ isSSRBuild = !!config.build?.ssr;
688
+ },
689
+ transform(code, id, transformOptions) {
690
+ if (transformOptions?.ssr || isSSRBuild) {
691
+ return null;
692
+ }
693
+ if (!id.endsWith(".ts") && !id.endsWith(".js") && !id.includes(".ts?") && !id.includes(".js?")) {
694
+ return null;
695
+ }
696
+ if (id.includes("node_modules") && !id.includes("@jay-framework")) {
697
+ return null;
698
+ }
699
+ if (!code.includes("@jay-framework/") && !code.includes("from '@") && !code.includes('from "@')) {
700
+ return null;
701
+ }
702
+ const result = transformImports({
703
+ code,
704
+ projectRoot,
705
+ filePath: id,
706
+ pluginDetector,
707
+ verbose
708
+ });
709
+ if (!result.hasChanges) {
710
+ return null;
711
+ }
712
+ return { code: result.code };
713
+ }
714
+ };
715
+ }
716
+ function jayStackCompiler(options = {}) {
717
+ const { trackImports, ...jayOptions } = options;
718
+ const moduleCache = /* @__PURE__ */ new Map();
719
+ const shouldTrackImports = trackImports || process.env.DEBUG_IMPORTS === "1";
720
+ const trackerOptions = typeof trackImports === "object" ? trackImports : { verbose: process.env.DEBUG_IMPORTS === "1" };
721
+ const plugins = [];
722
+ if (shouldTrackImports) {
723
+ plugins.push(createImportChainTracker(trackerOptions));
724
+ }
725
+ plugins.push(createPluginClientImportResolver({ verbose: !!shouldTrackImports }));
726
+ plugins.push(
270
727
  // First: Jay Stack code splitting transformation
271
728
  {
272
729
  name: "jay-stack:code-split",
273
730
  enforce: "pre",
274
731
  // 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) {
732
+ transform(code, id, options2) {
733
+ if (!id.endsWith(".ts") && !id.includes(".ts?")) {
279
734
  return null;
280
735
  }
281
- const environment = isClientBuild ? "client" : "server";
282
- if (!id.endsWith(".ts") && !id.includes(".ts?")) {
736
+ const hasComponent = code.includes("makeJayStackComponent");
737
+ const hasInit = code.includes("makeJayInit");
738
+ if (!hasComponent && !hasInit) {
283
739
  return null;
284
740
  }
741
+ const environment = options2?.ssr ? "server" : "client";
285
742
  try {
286
743
  return transformJayStackBuilder(code, id, environment);
287
744
  } catch (error) {
@@ -290,11 +747,104 @@ function jayStackCompiler(jayOptions = {}) {
290
747
  }
291
748
  }
292
749
  },
293
- // Second: Jay runtime compilation (existing plugin)
750
+ // Second: Action import transformation (client builds only)
751
+ // Uses resolveId + load to replace action modules with virtual modules
752
+ // containing createActionCaller calls BEFORE bundling happens.
753
+ (() => {
754
+ let isSSRBuild = false;
755
+ return {
756
+ name: "jay-stack:action-transform",
757
+ enforce: "pre",
758
+ // Track SSR mode from config
759
+ configResolved(config) {
760
+ isSSRBuild = config.build?.ssr ?? false;
761
+ },
762
+ buildStart() {
763
+ clearActionMetadataCache();
764
+ moduleCache.clear();
765
+ },
766
+ async resolveId(source, importer, options2) {
767
+ if (options2?.ssr || isSSRBuild) {
768
+ return null;
769
+ }
770
+ if (!isActionImport(source)) {
771
+ return null;
772
+ }
773
+ if (!source.startsWith(".") || !importer) {
774
+ return null;
775
+ }
776
+ const importerDir = path.dirname(importer);
777
+ let resolvedPath = path.resolve(importerDir, source);
778
+ if (!resolvedPath.endsWith(".ts") && !resolvedPath.endsWith(".js")) {
779
+ if (fs.existsSync(resolvedPath + ".ts")) {
780
+ resolvedPath += ".ts";
781
+ } else if (fs.existsSync(resolvedPath + ".js")) {
782
+ resolvedPath += ".js";
783
+ } else {
784
+ return null;
785
+ }
786
+ } else if (resolvedPath.endsWith(".js") && !fs.existsSync(resolvedPath)) {
787
+ const tsPath = resolvedPath.slice(0, -3) + ".ts";
788
+ if (fs.existsSync(tsPath)) {
789
+ resolvedPath = tsPath;
790
+ } else {
791
+ return null;
792
+ }
793
+ }
794
+ return `\0jay-action:${resolvedPath}`;
795
+ },
796
+ async load(id) {
797
+ if (!id.startsWith("\0jay-action:")) {
798
+ return null;
799
+ }
800
+ const actualPath = id.slice("\0jay-action:".length);
801
+ let code;
802
+ try {
803
+ code = await fs.promises.readFile(actualPath, "utf-8");
804
+ } catch (err) {
805
+ console.error(`[action-transform] Could not read ${actualPath}:`, err);
806
+ return null;
807
+ }
808
+ const actions = extractActionsFromSource(code, actualPath);
809
+ if (actions.length === 0) {
810
+ console.warn(`[action-transform] No actions found in ${actualPath}`);
811
+ return null;
812
+ }
813
+ const lines = [
814
+ `import { createActionCaller } from '@jay-framework/stack-client-runtime';`,
815
+ ""
816
+ ];
817
+ for (const action of actions) {
818
+ lines.push(
819
+ `export const ${action.exportName} = createActionCaller('${action.actionName}', '${action.method}');`
820
+ );
821
+ }
822
+ if (code.includes("ActionError")) {
823
+ lines.push(
824
+ `export { ActionError } from '@jay-framework/stack-client-runtime';`
825
+ );
826
+ }
827
+ const result = lines.join("\n");
828
+ return result;
829
+ }
830
+ };
831
+ })(),
832
+ // Third: Jay runtime compilation (existing plugin)
294
833
  jayRuntime(jayOptions)
295
- ];
834
+ );
835
+ return plugins;
296
836
  }
297
837
  export {
838
+ clearActionMetadataCache,
839
+ createDefaultPluginDetector,
840
+ createImportChainTracker,
841
+ createPluginClientImportResolver,
842
+ extractActionsFromSource,
843
+ extractPackageName,
844
+ isActionImport,
845
+ isSubpathImport,
298
846
  jayStackCompiler,
847
+ transformActionImports,
848
+ transformImports,
299
849
  transformJayStackBuilder
300
850
  };
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.11.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.11.0",
31
+ "@jay-framework/compiler-shared": "^0.11.0",
32
+ "@jay-framework/typescript-bridge": "^0.6.0",
33
+ "@jay-framework/vite-plugin": "^0.11.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.11.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
  }