@jay-framework/compiler-jay-stack 0.10.0 → 0.12.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
@@ -108,6 +108,93 @@ declare function transformActionImports(code: string, id: string, resolveActionM
108
108
  code: string;
109
109
  } | null>): Promise<TransformResult | null>;
110
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
+
111
198
  interface JayStackCompilerOptions extends JayRollupConfig {
112
199
  /**
113
200
  * Enable import chain tracking for debugging server code leaking into client builds.
@@ -156,4 +243,4 @@ interface JayStackCompilerOptions extends JayRollupConfig {
156
243
  */
157
244
  declare function jayStackCompiler(options?: JayStackCompilerOptions): Plugin[];
158
245
 
159
- export { type ActionMetadata, type BuildEnvironment, type ExtractedActions, type ImportChainTrackerOptions, type JayStackCompilerOptions, clearActionMetadataCache, createImportChainTracker, extractActionsFromSource, isActionImport, jayStackCompiler, transformActionImports, 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,8 +1,10 @@
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
- import * as fs from "node:fs";
4
+ import { getLogger } from "@jay-framework/logger";
5
5
  import * as path from "node:path";
6
+ import { createRequire } from "node:module";
7
+ import * as fs from "node:fs";
6
8
  const COMPONENT_SERVER_METHODS = /* @__PURE__ */ new Set([
7
9
  "withServices",
8
10
  "withLoadParams",
@@ -387,7 +389,7 @@ async function transformActionImports(code, id, resolveActionModule) {
387
389
  for (const imp of actionImports) {
388
390
  const resolved = await resolveActionModule(imp.source, id);
389
391
  if (!resolved) {
390
- console.warn(`[action-transform] Could not resolve action module: ${imp.source}`);
392
+ getLogger().warn(`[action-transform] Could not resolve action module: ${imp.source}`);
391
393
  continue;
392
394
  }
393
395
  const actions = extractActionsFromSource(resolved.code, resolved.path);
@@ -400,7 +402,7 @@ async function transformActionImports(code, id, resolveActionModule) {
400
402
  );
401
403
  needsCreateActionCallerImport = true;
402
404
  } else {
403
- console.warn(
405
+ getLogger().warn(
404
406
  `[action-transform] Export '${importName}' from ${imp.source} is not a recognized action`
405
407
  );
406
408
  }
@@ -504,7 +506,7 @@ function createImportChainTracker(options = {}) {
504
506
  importChain.clear();
505
507
  detectedServerModules.clear();
506
508
  if (verbose) {
507
- console.log("[import-chain-tracker] Build started, tracking imports...");
509
+ getLogger().info("[import-chain-tracker] Build started, tracking imports...");
508
510
  }
509
511
  },
510
512
  resolveId(source, importer, options2) {
@@ -516,7 +518,7 @@ function createImportChainTracker(options = {}) {
516
518
  }
517
519
  if (importer) {
518
520
  if (verbose) {
519
- console.log(
521
+ getLogger().info(
520
522
  `[import-chain-tracker] ${shortenPath(importer)} imports ${source}`
521
523
  );
522
524
  }
@@ -533,14 +535,14 @@ function createImportChainTracker(options = {}) {
533
535
  if (isServerOnlyModule(id)) {
534
536
  detectedServerModules.add(id);
535
537
  const chain = buildImportChain(id);
536
- console.error(
538
+ getLogger().error(
537
539
  `
538
540
  [import-chain-tracker] ⚠️ Server-only module detected in client build!`
539
541
  );
540
- console.error(`Module: ${shortenPath(id)}`);
541
- console.error(`Import chain:`);
542
- console.error(formatChain(chain));
543
- console.error("");
542
+ getLogger().error(`Module: ${shortenPath(id)}`);
543
+ getLogger().error(`Import chain:`);
544
+ getLogger().error(formatChain(chain));
545
+ getLogger().error("");
544
546
  }
545
547
  const importRegex = /import\s+(?:(?:\{[^}]*\}|[^{}\s,]+)\s+from\s+)?['"]([^'"]+)['"]/g;
546
548
  let match;
@@ -550,16 +552,18 @@ function createImportChainTracker(options = {}) {
550
552
  if (isServerOnlyModule(importedModule)) {
551
553
  if (!detectedServerModules.has(importedModule)) {
552
554
  detectedServerModules.add(importedModule);
553
- console.error(
555
+ getLogger().error(
554
556
  `
555
557
  [import-chain-tracker] ⚠️ Server-only import detected in client build!`
556
558
  );
557
- console.error(`Module "${importedModule}" imported by: ${shortenPath(id)}`);
559
+ getLogger().error(
560
+ `Module "${importedModule}" imported by: ${shortenPath(id)}`
561
+ );
558
562
  const chain = buildImportChain(id);
559
563
  chain.push(importedModule);
560
- console.error(`Import chain:`);
561
- console.error(formatChain(chain));
562
- console.error("");
564
+ getLogger().error(`Import chain:`);
565
+ getLogger().error(formatChain(chain));
566
+ getLogger().error("");
563
567
  }
564
568
  }
565
569
  }
@@ -567,27 +571,151 @@ function createImportChainTracker(options = {}) {
567
571
  },
568
572
  buildEnd() {
569
573
  if (detectedServerModules.size > 0) {
570
- console.warn(
574
+ getLogger().warn(
571
575
  `
572
576
  [import-chain-tracker] ⚠️ ${detectedServerModules.size} server-only module(s) detected during transform:`
573
577
  );
574
578
  for (const mod of detectedServerModules) {
575
- console.warn(` - ${mod}`);
579
+ getLogger().warn(` - ${mod}`);
576
580
  }
577
- console.warn(
581
+ getLogger().warn(
578
582
  "\nNote: These may be stripped by the code-split transform if only used in .withServer()."
579
583
  );
580
- console.warn(
584
+ getLogger().warn(
581
585
  'If build fails with "not exported" errors, check the import chains above.\n'
582
586
  );
583
587
  } else if (verbose) {
584
- console.log(
588
+ getLogger().info(
585
589
  "[import-chain-tracker] ✅ No server-only modules detected in client build"
586
590
  );
587
591
  }
588
592
  }
589
593
  };
590
594
  }
595
+ const require2 = createRequire(import.meta.url);
596
+ function createDefaultPluginDetector() {
597
+ const cache = /* @__PURE__ */ new Map();
598
+ return {
599
+ isJayPluginWithClientExport(packageName, projectRoot) {
600
+ const cacheKey = `${packageName}:${projectRoot}`;
601
+ if (cache.has(cacheKey)) {
602
+ return cache.get(cacheKey);
603
+ }
604
+ let result = false;
605
+ try {
606
+ require2.resolve(`${packageName}/plugin.yaml`, { paths: [projectRoot] });
607
+ try {
608
+ require2.resolve(`${packageName}/client`, { paths: [projectRoot] });
609
+ result = true;
610
+ } catch {
611
+ result = false;
612
+ }
613
+ } catch {
614
+ result = false;
615
+ }
616
+ cache.set(cacheKey, result);
617
+ return result;
618
+ }
619
+ };
620
+ }
621
+ function extractPackageName(source) {
622
+ if (source.startsWith(".") || source.startsWith("/")) {
623
+ return null;
624
+ }
625
+ if (source.startsWith("@")) {
626
+ const parts2 = source.split("/");
627
+ if (parts2.length >= 2) {
628
+ return `${parts2[0]}/${parts2[1]}`;
629
+ }
630
+ return null;
631
+ }
632
+ const parts = source.split("/");
633
+ return parts[0];
634
+ }
635
+ function isSubpathImport(source, packageName) {
636
+ return source.length > packageName.length && source[packageName.length] === "/";
637
+ }
638
+ const IMPORT_REGEX = /import\s+(.+?)\s+from\s+(['"])([^'"]+)\2/g;
639
+ const EXPORT_FROM_REGEX = /export\s+(.+?)\s+from\s+(['"])([^'"]+)\2/g;
640
+ function transformImports(options) {
641
+ const { code, projectRoot, filePath, pluginDetector, verbose = false } = options;
642
+ let hasChanges = false;
643
+ let result = code;
644
+ result = result.replace(IMPORT_REGEX, (match, clause, quote, source) => {
645
+ const packageName = extractPackageName(source);
646
+ if (!packageName)
647
+ return match;
648
+ if (isSubpathImport(source, packageName))
649
+ return match;
650
+ if (!pluginDetector.isJayPluginWithClientExport(packageName, projectRoot))
651
+ return match;
652
+ hasChanges = true;
653
+ const newSource = `${packageName}/client`;
654
+ if (verbose) {
655
+ getLogger().info(
656
+ `[plugin-client-import] Rewriting import ${source} -> ${newSource} (in ${path.basename(filePath)})`
657
+ );
658
+ }
659
+ return `import ${clause} from ${quote}${newSource}${quote}`;
660
+ });
661
+ result = result.replace(EXPORT_FROM_REGEX, (match, clause, quote, source) => {
662
+ const packageName = extractPackageName(source);
663
+ if (!packageName)
664
+ return match;
665
+ if (isSubpathImport(source, packageName))
666
+ return match;
667
+ if (!pluginDetector.isJayPluginWithClientExport(packageName, projectRoot))
668
+ return match;
669
+ hasChanges = true;
670
+ const newSource = `${packageName}/client`;
671
+ if (verbose) {
672
+ getLogger().info(
673
+ `[plugin-client-import] Rewriting export ${source} -> ${newSource} (in ${path.basename(filePath)})`
674
+ );
675
+ }
676
+ return `export ${clause} from ${quote}${newSource}${quote}`;
677
+ });
678
+ return { code: result, hasChanges };
679
+ }
680
+ function createPluginClientImportResolver(options = {}) {
681
+ const { verbose = false } = options;
682
+ let projectRoot = options.projectRoot || process.cwd();
683
+ let isSSRBuild = false;
684
+ const pluginDetector = options.pluginDetector || createDefaultPluginDetector();
685
+ return {
686
+ name: "jay-stack:plugin-client-import",
687
+ enforce: "pre",
688
+ configResolved(config) {
689
+ projectRoot = config.root || projectRoot;
690
+ isSSRBuild = !!config.build?.ssr;
691
+ },
692
+ transform(code, id, transformOptions) {
693
+ if (transformOptions?.ssr || isSSRBuild) {
694
+ return null;
695
+ }
696
+ if (!id.endsWith(".ts") && !id.endsWith(".js") && !id.includes(".ts?") && !id.includes(".js?")) {
697
+ return null;
698
+ }
699
+ if (id.includes("node_modules") && !id.includes("@jay-framework")) {
700
+ return null;
701
+ }
702
+ if (!code.includes("@jay-framework/") && !code.includes("from '@") && !code.includes('from "@')) {
703
+ return null;
704
+ }
705
+ const result = transformImports({
706
+ code,
707
+ projectRoot,
708
+ filePath: id,
709
+ pluginDetector,
710
+ verbose
711
+ });
712
+ if (!result.hasChanges) {
713
+ return null;
714
+ }
715
+ return { code: result.code };
716
+ }
717
+ };
718
+ }
591
719
  function jayStackCompiler(options = {}) {
592
720
  const { trackImports, ...jayOptions } = options;
593
721
  const moduleCache = /* @__PURE__ */ new Map();
@@ -597,6 +725,7 @@ function jayStackCompiler(options = {}) {
597
725
  if (shouldTrackImports) {
598
726
  plugins.push(createImportChainTracker(trackerOptions));
599
727
  }
728
+ plugins.push(createPluginClientImportResolver({ verbose: !!shouldTrackImports }));
600
729
  plugins.push(
601
730
  // First: Jay Stack code splitting transformation
602
731
  {
@@ -616,7 +745,7 @@ function jayStackCompiler(options = {}) {
616
745
  try {
617
746
  return transformJayStackBuilder(code, id, environment);
618
747
  } catch (error) {
619
- console.error(`[jay-stack:code-split] Error transforming ${id}:`, error);
748
+ getLogger().error(`[jay-stack:code-split] Error transforming ${id}: ${error}`);
620
749
  return null;
621
750
  }
622
751
  }
@@ -657,6 +786,13 @@ function jayStackCompiler(options = {}) {
657
786
  } else {
658
787
  return null;
659
788
  }
789
+ } else if (resolvedPath.endsWith(".js") && !fs.existsSync(resolvedPath)) {
790
+ const tsPath = resolvedPath.slice(0, -3) + ".ts";
791
+ if (fs.existsSync(tsPath)) {
792
+ resolvedPath = tsPath;
793
+ } else {
794
+ return null;
795
+ }
660
796
  }
661
797
  return `\0jay-action:${resolvedPath}`;
662
798
  },
@@ -669,12 +805,14 @@ function jayStackCompiler(options = {}) {
669
805
  try {
670
806
  code = await fs.promises.readFile(actualPath, "utf-8");
671
807
  } catch (err) {
672
- console.error(`[action-transform] Could not read ${actualPath}:`, err);
808
+ getLogger().error(
809
+ `[action-transform] Could not read ${actualPath}: ${err}`
810
+ );
673
811
  return null;
674
812
  }
675
813
  const actions = extractActionsFromSource(code, actualPath);
676
814
  if (actions.length === 0) {
677
- console.warn(`[action-transform] No actions found in ${actualPath}`);
815
+ getLogger().warn(`[action-transform] No actions found in ${actualPath}`);
678
816
  return null;
679
817
  }
680
818
  const lines = [
@@ -703,10 +841,15 @@ function jayStackCompiler(options = {}) {
703
841
  }
704
842
  export {
705
843
  clearActionMetadataCache,
844
+ createDefaultPluginDetector,
706
845
  createImportChainTracker,
846
+ createPluginClientImportResolver,
707
847
  extractActionsFromSource,
848
+ extractPackageName,
708
849
  isActionImport,
850
+ isSubpathImport,
709
851
  jayStackCompiler,
710
852
  transformActionImports,
853
+ transformImports,
711
854
  transformJayStackBuilder
712
855
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/compiler-jay-stack",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/index.js",
@@ -27,15 +27,16 @@
27
27
  "test:watch": "vitest"
28
28
  },
29
29
  "dependencies": {
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",
30
+ "@jay-framework/compiler": "^0.12.0",
31
+ "@jay-framework/compiler-shared": "^0.12.0",
32
+ "@jay-framework/logger": "^0.12.0",
33
+ "@jay-framework/typescript-bridge": "^0.7.0",
34
+ "@jay-framework/vite-plugin": "^0.12.0",
34
35
  "typescript": "^5.3.3",
35
36
  "vite": "^5.0.11"
36
37
  },
37
38
  "devDependencies": {
38
- "@jay-framework/dev-environment": "^0.10.0",
39
+ "@jay-framework/dev-environment": "^0.12.0",
39
40
  "rimraf": "^5.0.5",
40
41
  "tsup": "^8.0.1",
41
42
  "vitest": "^1.2.1"