rwsdk 0.1.0-alpha.8 → 0.1.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.
Files changed (89) hide show
  1. package/dist/runtime/client.d.ts +3 -1
  2. package/dist/runtime/client.js +16 -13
  3. package/dist/runtime/clientNavigation.d.ts +3 -0
  4. package/dist/runtime/clientNavigation.js +43 -0
  5. package/dist/runtime/entries/client.d.ts +1 -0
  6. package/dist/runtime/entries/client.js +1 -0
  7. package/dist/runtime/entries/worker.d.ts +2 -0
  8. package/dist/runtime/entries/worker.js +2 -0
  9. package/dist/runtime/imports/ClientOnly.d.ts +3 -0
  10. package/dist/runtime/imports/ClientOnly.js +8 -0
  11. package/dist/runtime/imports/NoSSRStub.d.ts +1 -0
  12. package/dist/runtime/imports/NoSSRStub.js +4 -0
  13. package/dist/runtime/imports/client.js +15 -2
  14. package/dist/runtime/imports/worker.d.ts +1 -1
  15. package/dist/runtime/imports/worker.js +7 -5
  16. package/dist/runtime/lib/db/DOWorkerDialect.d.ts +29 -0
  17. package/dist/runtime/lib/db/DOWorkerDialect.js +66 -0
  18. package/dist/runtime/lib/db/SqliteDurableObject.d.ts +14 -0
  19. package/dist/runtime/lib/db/SqliteDurableObject.js +42 -0
  20. package/dist/runtime/lib/db/create.d.ts +3 -0
  21. package/dist/runtime/lib/db/create.js +36 -0
  22. package/dist/runtime/lib/db/createDb.d.ts +2 -0
  23. package/dist/runtime/lib/db/createDb.js +27 -0
  24. package/dist/runtime/lib/db/index.d.ts +3 -0
  25. package/dist/runtime/lib/db/index.js +3 -0
  26. package/dist/runtime/lib/db/logger.d.ts +2 -0
  27. package/dist/runtime/lib/db/logger.js +41 -0
  28. package/dist/runtime/lib/db/migrations.d.ts +23 -0
  29. package/dist/runtime/lib/db/migrations.js +34 -0
  30. package/dist/runtime/lib/db/types.d.ts +0 -0
  31. package/dist/runtime/lib/db/types.js +1 -0
  32. package/dist/runtime/lib/debug.d.ts +2 -0
  33. package/dist/runtime/lib/debug.js +36 -0
  34. package/dist/runtime/lib/realtime/worker.d.ts +1 -1
  35. package/dist/runtime/lib/router.d.ts +31 -26
  36. package/dist/runtime/lib/router.js +9 -2
  37. package/dist/runtime/register/ssr.d.ts +2 -0
  38. package/dist/runtime/register/ssr.js +14 -1
  39. package/dist/runtime/register/worker.d.ts +1 -1
  40. package/dist/runtime/register/worker.js +5 -2
  41. package/dist/runtime/render/renderRscThenableToHtmlStream.d.ts +2 -1
  42. package/dist/runtime/render/renderRscThenableToHtmlStream.js +17 -3
  43. package/dist/runtime/render/renderToStream.d.ts +9 -0
  44. package/dist/runtime/render/renderToStream.js +26 -0
  45. package/dist/runtime/render/renderToString.d.ts +7 -0
  46. package/dist/runtime/render/renderToString.js +26 -0
  47. package/dist/runtime/render/transformRscToHtmlStream.js +1 -0
  48. package/dist/runtime/worker.d.ts +2 -1
  49. package/dist/runtime/worker.js +21 -14
  50. package/dist/scripts/debug-sync.mjs +8 -6
  51. package/dist/scripts/worker-run.mjs +1 -0
  52. package/dist/vite/configPlugin.mjs +8 -17
  53. package/dist/vite/createDirectiveLookupPlugin.d.mts +1 -0
  54. package/dist/vite/createDirectiveLookupPlugin.mjs +88 -49
  55. package/dist/vite/devServerTimingPlugin.d.mts +2 -0
  56. package/dist/vite/devServerTimingPlugin.mjs +24 -0
  57. package/dist/vite/directivesPlugin.mjs +168 -70
  58. package/dist/vite/findImportSpecifiers.d.mts +16 -0
  59. package/dist/vite/findImportSpecifiers.mjs +152 -0
  60. package/dist/vite/findImportSpecifiers.test.d.mts +1 -0
  61. package/dist/vite/findImportSpecifiers.test.mjs +73 -0
  62. package/dist/vite/findSpecifiers.d.mts +31 -0
  63. package/dist/vite/findSpecifiers.mjs +230 -0
  64. package/dist/vite/hasDirective.d.mts +7 -0
  65. package/dist/vite/hasDirective.mjs +54 -0
  66. package/dist/vite/hasOwnCloudflareVitePlugin.d.mts +3 -0
  67. package/dist/vite/hasOwnCloudflareVitePlugin.mjs +14 -0
  68. package/dist/vite/invalidateModule.d.mts +2 -0
  69. package/dist/vite/invalidateModule.mjs +14 -0
  70. package/dist/vite/miniflareHMRPlugin.d.mts +8 -0
  71. package/dist/vite/miniflareHMRPlugin.mjs +133 -0
  72. package/dist/vite/normalizeModulePath.mjs +12 -1
  73. package/dist/vite/redwoodPlugin.d.mts +1 -0
  74. package/dist/vite/redwoodPlugin.mjs +19 -4
  75. package/dist/vite/resolveModuleId.d.mts +6 -0
  76. package/dist/vite/resolveModuleId.mjs +14 -0
  77. package/dist/vite/ssrBridgePlugin.d.mts +5 -1
  78. package/dist/vite/ssrBridgePlugin.mjs +4 -43
  79. package/dist/vite/transformClientComponents.d.mts +1 -0
  80. package/dist/vite/transformClientComponents.mjs +61 -125
  81. package/dist/vite/transformJsxScriptTagsPlugin.mjs +14 -3
  82. package/dist/vite/transformServerFunctions.d.mts +11 -3
  83. package/dist/vite/transformServerFunctions.mjs +256 -171
  84. package/dist/vite/transformServerFunctions.test.mjs +22 -3
  85. package/dist/vite/useClientLookupPlugin.mjs +1 -0
  86. package/dist/vite/useServerLookupPlugin.mjs +1 -0
  87. package/dist/vite/useServerPlugin.d.mts +1 -1
  88. package/dist/vite/useServerPlugin.mjs +1 -1
  89. package/package.json +14 -3
@@ -0,0 +1,133 @@
1
+ import { resolve } from "node:path";
2
+ import colors from "picocolors";
3
+ import { readFile } from "node:fs/promises";
4
+ import { getShortName } from "../lib/getShortName.mjs";
5
+ import { pathExists } from "fs-extra";
6
+ const hasEntryAsAncestor = (module, entryFile, seen = new Set()) => {
7
+ // Prevent infinite recursion
8
+ if (seen.has(module))
9
+ return false;
10
+ seen.add(module);
11
+ // Check direct importers
12
+ for (const importer of module.importers) {
13
+ if (importer.file === entryFile)
14
+ return true;
15
+ // Recursively check importers
16
+ if (hasEntryAsAncestor(importer, entryFile, seen))
17
+ return true;
18
+ }
19
+ return false;
20
+ };
21
+ // Cache for "use client" status results
22
+ const useClientCache = new Map();
23
+ // Function to invalidate cache for a file
24
+ const invalidateUseClientCache = (file) => {
25
+ useClientCache.delete(file);
26
+ };
27
+ const isUseClientModule = async (ctx, file, seen = new Set()) => {
28
+ // Prevent infinite recursion
29
+ if (seen.has(file))
30
+ return false;
31
+ seen.add(file);
32
+ try {
33
+ // Check cache first
34
+ if (useClientCache.has(file)) {
35
+ return useClientCache.get(file);
36
+ }
37
+ // Read and check the file
38
+ const content = (await pathExists(file))
39
+ ? await readFile(file, "utf-8")
40
+ : "";
41
+ const hasUseClient = content.includes("'use client'") || content.includes('"use client"');
42
+ if (hasUseClient) {
43
+ useClientCache.set(file, true);
44
+ return true;
45
+ }
46
+ // Get the module from the module graph to find importers
47
+ const module = ctx.server.moduleGraph.getModuleById(file);
48
+ if (!module) {
49
+ useClientCache.set(file, false);
50
+ return false;
51
+ }
52
+ // Check all importers recursively
53
+ for (const importer of module.importers) {
54
+ if (await isUseClientModule(ctx, importer.url, seen)) {
55
+ useClientCache.set(file, true);
56
+ return true;
57
+ }
58
+ }
59
+ useClientCache.set(file, false);
60
+ return false;
61
+ }
62
+ catch (error) {
63
+ useClientCache.set(file, false);
64
+ return false;
65
+ }
66
+ };
67
+ export const miniflareHMRPlugin = (givenOptions) => [
68
+ {
69
+ name: "rwsdk:miniflare-hmr",
70
+ async hotUpdate(ctx) {
71
+ const environment = givenOptions.viteEnvironment.name;
72
+ const entry = givenOptions.workerEntryPathname;
73
+ if (!["client", environment].includes(this.environment.name)) {
74
+ return;
75
+ }
76
+ // todo(justinvdm, 12 Dec 2024): Skip client references
77
+ const modules = Array.from(ctx.server.environments[environment].moduleGraph.getModulesByFile(ctx.file) ?? []);
78
+ const isWorkerUpdate = ctx.file === entry ||
79
+ modules.some((module) => hasEntryAsAncestor(module, entry));
80
+ // The worker doesnt need an update
81
+ // => Short circuit HMR
82
+ if (!isWorkerUpdate) {
83
+ return [];
84
+ }
85
+ // The worker needs an update, but this is the client environment
86
+ // => Notify for HMR update of any css files imported by in worker, that are also in the client module graph
87
+ // Why: There may have been changes to css classes referenced, which might css modules to change
88
+ if (this.environment.name === "client") {
89
+ const cssModules = [];
90
+ for (const [_, module] of ctx.server.environments[environment]
91
+ .moduleGraph.idToModuleMap) {
92
+ // todo(justinvdm, 13 Dec 2024): We check+update _all_ css files in worker module graph,
93
+ // but it could just be a subset of css files that are actually affected, depending
94
+ // on the importers and imports of the changed file. We should be smarter about this.
95
+ if (module.file && module.file.endsWith(".css")) {
96
+ const clientModules = ctx.server.environments.client.moduleGraph.getModulesByFile(module.file);
97
+ if (clientModules) {
98
+ cssModules.push(...clientModules.values());
99
+ }
100
+ }
101
+ }
102
+ invalidateUseClientCache(ctx.file);
103
+ return (await isUseClientModule(ctx, ctx.file))
104
+ ? [...ctx.modules, ...cssModules]
105
+ : cssModules;
106
+ }
107
+ // The worker needs an update, and the hot check is for the worker environment
108
+ // => Notify for custom RSC-based HMR update, then short circuit HMR
109
+ if (isWorkerUpdate && this.environment.name === environment) {
110
+ const shortName = getShortName(ctx.file, ctx.server.config.root);
111
+ this.environment.logger.info(`${colors.green(`worker update`)} ${colors.dim(shortName)}`, {
112
+ clear: true,
113
+ timestamp: true,
114
+ });
115
+ const m = ctx.server.environments.client.moduleGraph
116
+ .getModulesByFile(resolve(givenOptions.rootDir, "src", "app", "style.css"))
117
+ ?.values()
118
+ .next().value;
119
+ if (m) {
120
+ ctx.server.environments.client.moduleGraph.invalidateModule(m, new Set(), ctx.timestamp, true);
121
+ }
122
+ ctx.server.environments.client.hot.send({
123
+ type: "custom",
124
+ event: "rsc:update",
125
+ data: {
126
+ file: ctx.file,
127
+ },
128
+ });
129
+ return [];
130
+ }
131
+ },
132
+ },
133
+ ];
@@ -1,2 +1,13 @@
1
1
  import path from "node:path";
2
- export const normalizeModulePath = (projectRootDir, modulePath) => "/" + path.relative(projectRootDir, modulePath);
2
+ export const normalizeModulePath = (projectRootDir, modulePath) => {
3
+ // /Users/path/to/project/src/foo/bar.ts -> /src/foo/bar.ts
4
+ if (modulePath.startsWith(projectRootDir)) {
5
+ return "/" + path.relative(projectRootDir, modulePath);
6
+ }
7
+ // /src/foo/bar.ts -> /src/foo/bar.ts
8
+ if (modulePath.startsWith("/")) {
9
+ return modulePath;
10
+ }
11
+ // src/foo/bar.ts -> /src/foo/bar.ts
12
+ return "/" + modulePath;
13
+ };
@@ -3,6 +3,7 @@ export type RedwoodPluginOptions = {
3
3
  silent?: boolean;
4
4
  rootDir?: string;
5
5
  mode?: "development" | "production";
6
+ includeCloudflarePlugin?: boolean;
6
7
  configPath?: string;
7
8
  entry?: {
8
9
  client?: string;
@@ -1,11 +1,13 @@
1
1
  import { resolve } from "node:path";
2
+ import { cloudflare } from "@cloudflare/vite-plugin";
3
+ import { hasOwnCloudflareVitePlugin } from "./hasOwnCloudflareVitePlugin.mjs";
2
4
  import reactPlugin from "@vitejs/plugin-react";
3
5
  import tsconfigPaths from "vite-tsconfig-paths";
4
6
  import { transformJsxScriptTagsPlugin } from "./transformJsxScriptTagsPlugin.mjs";
5
7
  import { directivesPlugin } from "./directivesPlugin.mjs";
6
8
  import { useClientLookupPlugin } from "./useClientLookupPlugin.mjs";
7
9
  import { useServerLookupPlugin } from "./useServerLookupPlugin.mjs";
8
- import { miniflarePlugin } from "./miniflarePlugin.mjs";
10
+ import { miniflareHMRPlugin } from "./miniflareHMRPlugin.mjs";
9
11
  import { moveStaticAssetsPlugin } from "./moveStaticAssetsPlugin.mjs";
10
12
  import { configPlugin } from "./configPlugin.mjs";
11
13
  import { $ } from "../lib/$.mjs";
@@ -17,6 +19,7 @@ import { vitePreamblePlugin } from "./vitePreamblePlugin.mjs";
17
19
  import { prismaPlugin } from "./prismaPlugin.mjs";
18
20
  import { ssrBridgePlugin } from "./ssrBridgePlugin.mjs";
19
21
  import { hasPkgScript } from "../lib/hasPkgScript.mjs";
22
+ import { devServerTimingPlugin } from "./devServerTimingPlugin.mjs";
20
23
  export const redwoodPlugin = async (options = {}) => {
21
24
  const projectRootDir = process.cwd();
22
25
  const mode = options.mode ??
@@ -25,6 +28,8 @@ export const redwoodPlugin = async (options = {}) => {
25
28
  const workerEntryPathname = resolve(projectRootDir, options?.entry?.worker ?? "src/worker.tsx");
26
29
  const clientFiles = new Set();
27
30
  const serverFiles = new Set();
31
+ const shouldIncludeCloudflarePlugin = options.includeCloudflarePlugin ??
32
+ !(await hasOwnCloudflareVitePlugin({ rootProjectDir: projectRootDir }));
28
33
  // context(justinvdm, 31 Mar 2025): We assume that if there is no .wrangler directory,
29
34
  // then this is fresh install, and we run `npm run dev:init` here.
30
35
  if (process.env.RWSDK_WORKER_RUN !== "1" &&
@@ -39,6 +44,7 @@ export const redwoodPlugin = async (options = {}) => {
39
44
  }) `npm run dev:init`;
40
45
  }
41
46
  return [
47
+ devServerTimingPlugin(),
42
48
  configPlugin({
43
49
  mode,
44
50
  silent: options.silent ?? false,
@@ -46,14 +52,23 @@ export const redwoodPlugin = async (options = {}) => {
46
52
  clientEntryPathname,
47
53
  workerEntryPathname,
48
54
  }),
49
- ssrBridgePlugin(),
55
+ ssrBridgePlugin({
56
+ clientFiles,
57
+ serverFiles,
58
+ projectRootDir,
59
+ }),
50
60
  reactConditionsResolverPlugin(),
51
61
  tsconfigPaths({ root: projectRootDir }),
52
- miniflarePlugin({
62
+ shouldIncludeCloudflarePlugin
63
+ ? cloudflare({
64
+ viteEnvironment: { name: "worker" },
65
+ configPath: options.configPath ?? (await findWranglerConfig(projectRootDir)),
66
+ })
67
+ : [],
68
+ miniflareHMRPlugin({
53
69
  rootDir: projectRootDir,
54
70
  viteEnvironment: { name: "worker" },
55
71
  workerEntryPathname,
56
- configPath: options.configPath ?? (await findWranglerConfig(projectRootDir)),
57
72
  }),
58
73
  reactPlugin(),
59
74
  directivesPlugin({
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Resolves a module ID consistently across client and server transformations.
3
+ * For node modules, uses the /rwsdk:kind/relativePath format.
4
+ * For regular files, uses the raw ID.
5
+ */
6
+ export declare function resolveModuleId(id: string, kind: "client" | "server", projectRootDir: string): string;
@@ -0,0 +1,14 @@
1
+ import { normalizeModulePath } from "./normalizeModulePath.mjs";
2
+ /**
3
+ * Resolves a module ID consistently across client and server transformations.
4
+ * For node modules, uses the /rwsdk:kind/relativePath format.
5
+ * For regular files, uses the raw ID.
6
+ */
7
+ export function resolveModuleId(id, kind, projectRootDir) {
8
+ const modulePath = normalizeModulePath(projectRootDir, id);
9
+ return modulePath.includes("node_modules")
10
+ ? modulePath.includes("__rwsdk_kind")
11
+ ? modulePath
12
+ : `${modulePath}?__rwsdk_kind=${kind}`
13
+ : modulePath;
14
+ }
@@ -1,3 +1,7 @@
1
1
  import type { Plugin } from "vite";
2
2
  export declare const VIRTUAL_SSR_PREFIX = "virtual:rwsdk:ssr:";
3
- export declare const ssrBridgePlugin: () => Plugin;
3
+ export declare const ssrBridgePlugin: ({ clientFiles, serverFiles, }: {
4
+ clientFiles: Set<string>;
5
+ serverFiles: Set<string>;
6
+ projectRootDir: string;
7
+ }) => Plugin;
@@ -1,40 +1,13 @@
1
1
  import debug from "debug";
2
2
  import { SSR_BRIDGE_PATH } from "../lib/constants.mjs";
3
+ import { invalidateModule } from "./invalidateModule.mjs";
3
4
  const log = debug("rwsdk:vite:ssr-bridge-plugin");
4
5
  const verboseLog = debug("verbose:rwsdk:vite:ssr-bridge-plugin");
5
6
  export const VIRTUAL_SSR_PREFIX = "virtual:rwsdk:ssr:";
6
- export const ssrBridgePlugin = () => {
7
+ export const ssrBridgePlugin = ({ clientFiles, serverFiles, }) => {
7
8
  log("Initializing SSR bridge plugin with SSR_BRIDGE_PATH=%s", SSR_BRIDGE_PATH);
8
9
  let devServer;
9
10
  let isDev = false;
10
- let promisedSSRWarmup;
11
- const ensureWarmupSSRModules = async () => {
12
- if (promisedSSRWarmup) {
13
- log("SSR warmup already in progress");
14
- return promisedSSRWarmup;
15
- }
16
- promisedSSRWarmup = doWarmupSSRModules();
17
- return promisedSSRWarmup;
18
- };
19
- const doWarmupSSRModules = async () => {
20
- log("Warming up SSR modules");
21
- const files = [
22
- "virtual:use-server-lookup",
23
- "virtual:use-client-lookup",
24
- "rwsdk/__ssr",
25
- "rwsdk/__ssr_bridge",
26
- ];
27
- for (const file of files) {
28
- log("Warming up SSR file: %s", file);
29
- await devServer.environments.ssr.warmupRequest(file);
30
- log("Waiting for SSR requests to idle");
31
- await devServer.environments.ssr.waitForRequestsIdle();
32
- log("Deps optimizer scan processing");
33
- await devServer.environments.ssr.depsOptimizer?.scanProcessing;
34
- log("Deps optimizer scan processing complete");
35
- }
36
- log("SSR warmup complete");
37
- };
38
11
  const ssrBridgePlugin = {
39
12
  name: "rwsdk:ssr-bridge",
40
13
  enforce: "pre",
@@ -112,11 +85,10 @@ export const ssrBridgePlugin = () => {
112
85
  verboseLog("Loading id=%s, isDev=%s, environment=%s", id, isDev, this.environment.name);
113
86
  if (id.startsWith(VIRTUAL_SSR_PREFIX) &&
114
87
  this.environment.name === "worker") {
115
- await ensureWarmupSSRModules();
116
88
  const realId = id.slice(VIRTUAL_SSR_PREFIX.length);
117
89
  log("Virtual SSR module load: id=%s, realId=%s", id, realId);
118
90
  if (isDev) {
119
- log("Dev mode: warming up and fetching SSR module for realPath=%s", realId);
91
+ log("Dev mode: fetching SSR module for realPath=%s", realId);
120
92
  const result = await devServer?.environments.ssr.fetchModule(realId);
121
93
  verboseLog("Fetch module result: id=%s, result=%O", realId, result);
122
94
  if (!result) {
@@ -129,7 +101,7 @@ export const ssrBridgePlugin = () => {
129
101
  await (async function(__vite_ssr_import__, __vite_ssr_dynamic_import__) {${code}})((id) => __vite_ssr_import__('/@id/${VIRTUAL_SSR_PREFIX}'+id), (id) => __vite_ssr_dynamic_import__('/@id/${VIRTUAL_SSR_PREFIX}'+id));
130
102
  `;
131
103
  log("Transformed SSR module code length: %d", transformedCode.length);
132
- log("Transformed SSR module code: %s", transformedCode);
104
+ verboseLog("Transformed SSR module code for realId=%s: %s", realId, transformedCode);
133
105
  return transformedCode;
134
106
  }
135
107
  }
@@ -138,14 +110,3 @@ await (async function(__vite_ssr_import__, __vite_ssr_dynamic_import__) {${code}
138
110
  };
139
111
  return ssrBridgePlugin;
140
112
  };
141
- const invalidateModule = (devServer, environment, id) => {
142
- const [rawId, _query] = id.split("?");
143
- log("Invalidating module: id=%s, environment=%s", id, environment);
144
- const moduleNode = devServer?.environments[environment]?.moduleGraph.idToModuleMap.get(rawId);
145
- if (moduleNode) {
146
- devServer?.environments[environment]?.moduleGraph.invalidateModule(moduleNode);
147
- }
148
- else {
149
- verboseLog("Module not found: id=%s, environment=%s", id, environment);
150
- }
151
- };
@@ -2,6 +2,7 @@ interface TransformContext {
2
2
  environmentName: string;
3
3
  clientFiles?: Set<string>;
4
4
  isEsbuild?: boolean;
5
+ addClientModule?: (environment: string, id: string) => void;
5
6
  }
6
7
  interface TransformResult {
7
8
  code: string;
@@ -1,5 +1,7 @@
1
- import { Project, SyntaxKind, Node } from "ts-morph";
1
+ import MagicString from "magic-string";
2
2
  import debug from "debug";
3
+ import { hasDirective } from "./hasDirective.mjs";
4
+ import { findExports } from "./findSpecifiers.mjs";
3
5
  const logVite = debug("rwsdk:vite:transform-client-components:vite");
4
6
  const logEsbuild = debug("rwsdk:vite:transform-client-components:esbuild");
5
7
  const verboseLogVite = debug("verbose:rwsdk:vite:transform-client-components:vite");
@@ -7,170 +9,104 @@ const verboseLogEsbuild = debug("verbose:rwsdk:vite:transform-client-components:
7
9
  export async function transformClientComponents(code, normalizedId, ctx) {
8
10
  const log = ctx.isEsbuild ? logEsbuild : logVite;
9
11
  const verboseLog = ctx.isEsbuild ? verboseLogEsbuild : verboseLogVite;
10
- log("Called transformClientComponents for id: id=%s, ctx: %O", normalizedId, ctx);
11
- function extractSourceMapFromEmit(sourceFile) {
12
- const emitOutput = sourceFile.getEmitOutput();
13
- let sourceMap;
14
- const outputFiles = emitOutput.getOutputFiles();
15
- log(":isEsbuild=%s: EmitOutput files for %s (%s) - %d files:", !!ctx.isEsbuild, normalizedId, ctx.environmentName, outputFiles.length);
16
- for (const outputFile of outputFiles) {
17
- log(":isEsbuild=%s: - %s (%s)", !!ctx.isEsbuild, outputFile.getFilePath(), ctx.environmentName);
18
- if (outputFile.getFilePath().endsWith(".js.map")) {
19
- sourceMap = JSON.parse(outputFile.getText());
20
- }
21
- }
22
- return sourceMap;
23
- }
24
- // 2. Only transform files that start with 'use client'
25
- const cleanCode = code.trimStart();
26
- const hasUseClient = cleanCode.startsWith('"use client"') ||
27
- cleanCode.startsWith("'use client'");
28
- if (!hasUseClient) {
12
+ log("Called transformClientComponents for id: id=%s", normalizedId);
13
+ if (!hasDirective(code, "use client")) {
29
14
  log("Skipping: no 'use client' directive in id=%s", normalizedId);
30
15
  verboseLog(":VERBOSE: Returning code unchanged for id=%s:\n%s", normalizedId, code);
31
16
  return;
32
17
  }
33
18
  log("Processing 'use client' module: id=%s", normalizedId);
34
- ctx.clientFiles?.add(normalizedId);
35
- // Use ts-morph to collect all export info and perform transformations
36
- const project = new Project({
37
- useInMemoryFileSystem: true,
38
- compilerOptions: {
39
- sourceMap: true,
40
- inlineSourceMap: false,
41
- allowJs: true,
42
- checkJs: true,
43
- target: 2, // ES6
44
- module: 1, // CommonJS
45
- jsx: 2, // React
46
- },
47
- });
48
- const sourceFile = project.createSourceFile(normalizedId + ".ts", code);
49
- const exportInfos = [];
19
+ ctx.addClientModule?.(ctx.environmentName, normalizedId);
20
+ // Parse exports using the findExports helper
21
+ const exportInfos = findExports(normalizedId, code, verboseLog);
22
+ const processedExports = [];
50
23
  let defaultExportInfo;
51
24
  // Helper to get the computed local name (with alias suffix if present)
52
25
  function getComputedLocalName(info) {
53
26
  return `${info.local}${info.alias ? `_${info.alias}` : ""}`;
54
27
  }
55
- // Helper to add export info
56
- function addExport(local, exported, isDefault, statementIdx, alias) {
57
- if (isDefault) {
58
- defaultExportInfo = { local, exported, isDefault, statementIdx };
28
+ // Convert ExportInfo to ProcessedExportInfo
29
+ exportInfos.forEach((exportInfo, idx) => {
30
+ if (exportInfo.isDefault) {
31
+ defaultExportInfo = {
32
+ local: exportInfo.alias || "default",
33
+ exported: "default",
34
+ isDefault: true,
35
+ statementIdx: idx,
36
+ };
59
37
  }
60
38
  else {
61
- exportInfos.push({ local, exported, isDefault, statementIdx, alias });
62
- }
63
- }
64
- // Walk through statements in order to collect export information
65
- const statements = sourceFile.getStatements();
66
- statements.forEach((stmt, idx) => {
67
- // export default function ...
68
- if (Node.isFunctionDeclaration(stmt) &&
69
- stmt.hasModifier(SyntaxKind.ExportKeyword) &&
70
- stmt.hasModifier(SyntaxKind.DefaultKeyword)) {
71
- addExport("default", "default", true, idx);
72
- return;
73
- }
74
- // export default ... (assignment)
75
- if (Node.isExportAssignment(stmt)) {
76
- const expr = stmt.getExpression();
77
- if (Node.isIdentifier(expr)) {
78
- addExport(expr.getText(), "default", true, idx);
79
- }
80
- else {
81
- addExport("default", "default", true, idx);
82
- }
83
- return;
84
- }
85
- // export const foo = ...
86
- if (Node.isVariableStatement(stmt) &&
87
- stmt.hasModifier(SyntaxKind.ExportKeyword)) {
88
- stmt
89
- .getDeclarationList()
90
- .getDeclarations()
91
- .forEach((decl) => {
92
- const name = decl.getName();
93
- addExport(name, name, false, idx);
39
+ // For aliases like "export { MyComponent as CustomName }", we need:
40
+ // - local: "MyComponent" (the original name)
41
+ // - exported: "CustomName" (the alias name)
42
+ // - alias: "CustomName" (to generate MyComponent_CustomName)
43
+ const hasAlias = exportInfo.alias && exportInfo.originalName;
44
+ processedExports.push({
45
+ local: exportInfo.originalName || exportInfo.name, // Use originalName if available
46
+ exported: exportInfo.name, // The exported name (alias if present)
47
+ isDefault: false,
48
+ statementIdx: idx,
49
+ alias: hasAlias ? exportInfo.alias : undefined,
94
50
  });
95
- return;
96
- }
97
- // export function foo() ...
98
- if (Node.isFunctionDeclaration(stmt) &&
99
- stmt.hasModifier(SyntaxKind.ExportKeyword)) {
100
- if (!stmt.hasModifier(SyntaxKind.DefaultKeyword)) {
101
- const name = stmt.getName();
102
- if (name) {
103
- addExport(name, name, false, idx);
104
- }
105
- }
106
- return;
107
- }
108
- // export { ... } or export { ... } from ...
109
- if (Node.isExportDeclaration(stmt)) {
110
- const namedExports = stmt.getNamedExports();
111
- if (namedExports.length > 0) {
112
- namedExports.forEach((exp) => {
113
- const alias = exp.getAliasNode()?.getText();
114
- const local = alias ? exp.getNameNode().getText() : exp.getName();
115
- const exported = alias ? alias : exp.getName();
116
- addExport(local, exported, exported === "default", idx, alias);
117
- });
118
- }
119
- return;
120
51
  }
121
52
  });
122
53
  // 3. Client/SSR files: just remove the directive
123
54
  if (ctx.environmentName === "ssr" || ctx.environmentName === "client") {
124
55
  log(":isEsbuild=%s: Handling SSR virtual module: %s", !!ctx.isEsbuild, normalizedId);
125
- // Remove 'use client' directive using ts-morph
126
- sourceFile
127
- .getDescendantsOfKind(SyntaxKind.StringLiteral)
128
- .forEach((node) => {
129
- if (node.getText() === "'use client'" ||
130
- node.getText() === '"use client"') {
131
- const parentExpr = node.getFirstAncestorByKind(SyntaxKind.ExpressionStatement);
132
- if (parentExpr) {
133
- parentExpr.remove();
134
- }
135
- }
56
+ // Remove 'use client' directive using magic-string
57
+ const s = new MagicString(code);
58
+ // Find and remove "use client" directives
59
+ const directiveRegex = /^(\s*)(['"]use client['"])\s*;?\s*\n?/gm;
60
+ let match;
61
+ while ((match = directiveRegex.exec(code)) !== null) {
62
+ const start = match.index;
63
+ const end = match.index + match[0].length;
64
+ s.remove(start, end);
65
+ verboseLog("Removed 'use client' directive from normalizedId=%s", normalizedId);
66
+ break; // Only remove the first one
67
+ }
68
+ const sourceMap = s.generateMap({
69
+ source: normalizedId,
70
+ includeContent: true,
71
+ hires: true,
136
72
  });
137
- const sourceMap = extractSourceMapFromEmit(sourceFile);
138
- verboseLog(":VERBOSE: SSR transformed code for %s:\n%s", normalizedId, sourceFile.getFullText());
73
+ verboseLog(":VERBOSE: SSR transformed code for %s:\n%s", normalizedId, s.toString());
139
74
  return {
140
- code: sourceFile.getFullText(),
75
+ code: s.toString(),
141
76
  map: sourceMap,
142
77
  };
143
78
  }
144
79
  // 4. Non-SSR files: replace all implementation with registerClientReference logic
145
- // Clear the source file and rebuild it
146
- sourceFile.removeText();
80
+ // Generate completely new code for worker/client environments
81
+ const s = new MagicString("");
147
82
  // Add import declaration
148
- sourceFile.addImportDeclaration({
149
- moduleSpecifier: "rwsdk/worker",
150
- namedImports: [{ name: "registerClientReference" }],
151
- });
83
+ s.append('import { registerClientReference } from "rwsdk/worker";\n');
152
84
  // Compute unique computed local names first
153
- const computedLocalNames = new Map(exportInfos.map((info) => [getComputedLocalName(info), info]));
85
+ const computedLocalNames = new Map(processedExports.map((info) => [getComputedLocalName(info), info]));
154
86
  // Add registerClientReference assignments for unique names
155
87
  for (const [computedLocalName, correspondingInfo] of computedLocalNames) {
156
88
  log(":isEsbuild=%s: Registering client reference for named export: %s as %s", !!ctx.isEsbuild, correspondingInfo.local, correspondingInfo.exported);
157
- sourceFile.addStatements(`const ${computedLocalName} = registerClientReference("${normalizedId}", "${correspondingInfo.exported}");`);
89
+ s.append(`const ${computedLocalName} = registerClientReference("${normalizedId}", "${correspondingInfo.exported}");\n`);
158
90
  }
159
91
  // Add grouped export statement for named exports (preserving order and alias)
160
- if (exportInfos.length > 0) {
92
+ if (processedExports.length > 0) {
161
93
  const exportNames = Array.from(computedLocalNames.entries()).map(([computedLocalName, correspondingInfo]) => correspondingInfo.local === correspondingInfo.exported
162
94
  ? computedLocalName
163
95
  : `${computedLocalName} as ${correspondingInfo.exported}`);
164
96
  log(":isEsbuild=%s: Exporting named exports: %O", !!ctx.isEsbuild, exportNames);
165
- sourceFile.addStatements(`export { ${exportNames.join(", ")} };`);
97
+ s.append(`export { ${exportNames.join(", ")} };\n`);
166
98
  }
167
99
  // Add default export if present
168
100
  if (defaultExportInfo) {
169
101
  log(":isEsbuild=%s: Registering client reference for default export: %s", !!ctx.isEsbuild, defaultExportInfo.exported);
170
- sourceFile.addStatements(`export default registerClientReference("${normalizedId}", "${defaultExportInfo.exported}");`);
102
+ s.append(`export default registerClientReference("${normalizedId}", "${defaultExportInfo.exported}");\n`);
171
103
  }
172
- const sourceMap = extractSourceMapFromEmit(sourceFile);
173
- const finalResult = sourceFile.getFullText();
104
+ const sourceMap = s.generateMap({
105
+ source: normalizedId,
106
+ includeContent: true,
107
+ hires: true,
108
+ });
109
+ const finalResult = s.toString();
174
110
  verboseLog(":VERBOSE: Transformed code (env=%s, normalizedId=%s):\n%s", normalizedId, ctx.environmentName, finalResult);
175
111
  return {
176
112
  code: finalResult,
@@ -12,9 +12,19 @@ const readManifest = async (manifestPath) => {
12
12
  }
13
13
  return manifestCache;
14
14
  };
15
- // Check if a string includes any jsx function calls
16
15
  function hasJsxFunctions(text) {
17
- return (text.includes("jsx(") || text.includes("jsxs(") || text.includes("jsxDEV("));
16
+ return (text.includes('jsx("script"') ||
17
+ text.includes("jsx('script'") ||
18
+ text.includes('jsx("link"') ||
19
+ text.includes("jsx('link'") ||
20
+ text.includes('jsxs("script"') ||
21
+ text.includes("jsxs('script'") ||
22
+ text.includes('jsxs("link"') ||
23
+ text.includes("jsxs('link'") ||
24
+ text.includes('jsxDEV("script"') ||
25
+ text.includes("jsxDEV('script'") ||
26
+ text.includes('jsxDEV("link"') ||
27
+ text.includes("jsxDEV('link'"));
18
28
  }
19
29
  // Transform import statements in script content using ts-morph
20
30
  function transformScriptImports(scriptContent, manifest) {
@@ -68,7 +78,8 @@ function transformScriptImports(scriptContent, manifest) {
68
78
  }
69
79
  }
70
80
  export async function transformJsxScriptTagsCode(code, manifest = {}) {
71
- // Quick heuristic check if there's JSX in the code
81
+ // context(justinvdm, 15 Jun 2025): Optimization to exit early
82
+ // to avoidunnecessary ts-morph parsing
72
83
  if (!hasJsxFunctions(code)) {
73
84
  return;
74
85
  }
@@ -1,8 +1,16 @@
1
- import { SourceFile } from "ts-morph";
2
1
  interface TransformResult {
3
2
  code: string;
4
3
  map?: any;
5
4
  }
6
- export declare const findExportedFunctions: (sourceFile: SourceFile) => Set<string>;
7
- export declare const transformServerFunctions: (code: string, normalizedId: string, environment: "client" | "worker" | "ssr", serverFiles?: Set<string>) => TransformResult | undefined;
5
+ type ExportInfoCompat = {
6
+ localFunctions: Set<string>;
7
+ reExports: Array<{
8
+ localName: string;
9
+ originalName: string;
10
+ moduleSpecifier: string;
11
+ }>;
12
+ };
13
+ export declare const findExportedFunctions: (code: string, normalizedId?: string) => Set<string>;
14
+ export declare const findExportInfo: (code: string, normalizedId?: string) => ExportInfoCompat;
15
+ export declare const transformServerFunctions: (code: string, normalizedId: string, environment: "client" | "worker" | "ssr", serverFiles?: Set<string>, addServerModule?: (environment: string, id: string) => void) => TransformResult | undefined;
8
16
  export type { TransformResult };