@rainfw/core 0.1.2 → 0.2.1

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 (63) hide show
  1. package/dist/client/dom.d.ts +4 -0
  2. package/dist/client/dom.d.ts.map +1 -0
  3. package/dist/client/dom.js +130 -0
  4. package/dist/client/dom.js.map +1 -0
  5. package/dist/client/hooks.d.ts +58 -0
  6. package/dist/client/hooks.d.ts.map +1 -0
  7. package/dist/client/hooks.js +173 -0
  8. package/dist/client/hooks.js.map +1 -0
  9. package/dist/client/hydrate.d.ts +14 -0
  10. package/dist/client/hydrate.d.ts.map +1 -0
  11. package/dist/client/hydrate.js +167 -0
  12. package/dist/client/hydrate.js.map +1 -0
  13. package/dist/client/jsx-runtime.d.ts +6 -0
  14. package/dist/client/jsx-runtime.d.ts.map +1 -0
  15. package/dist/client/jsx-runtime.js +20 -0
  16. package/dist/client/jsx-runtime.js.map +1 -0
  17. package/dist/client/reconciler.d.ts +4 -0
  18. package/dist/client/reconciler.d.ts.map +1 -0
  19. package/dist/client/reconciler.js +238 -0
  20. package/dist/client/reconciler.js.map +1 -0
  21. package/dist/client/runtime.d.ts +6 -0
  22. package/dist/client/runtime.d.ts.map +1 -0
  23. package/dist/client/runtime.js +17 -0
  24. package/dist/client/runtime.js.map +1 -0
  25. package/dist/client/scheduler.d.ts +4 -0
  26. package/dist/client/scheduler.d.ts.map +1 -0
  27. package/dist/client/scheduler.js +44 -0
  28. package/dist/client/scheduler.js.map +1 -0
  29. package/dist/compiler/inject.d.ts +6 -0
  30. package/dist/compiler/inject.d.ts.map +1 -0
  31. package/dist/compiler/inject.js +19 -0
  32. package/dist/compiler/inject.js.map +1 -0
  33. package/dist/compiler/server-action.d.ts +9 -0
  34. package/dist/compiler/server-action.d.ts.map +1 -0
  35. package/dist/compiler/server-action.js +98 -0
  36. package/dist/compiler/server-action.js.map +1 -0
  37. package/dist/context.js +1 -1
  38. package/dist/context.js.map +1 -1
  39. package/dist/index.d.ts +5 -3
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +2 -1
  42. package/dist/index.js.map +1 -1
  43. package/dist/jsx/index.d.ts +3 -2
  44. package/dist/jsx/index.d.ts.map +1 -1
  45. package/dist/jsx/index.js +2 -2
  46. package/dist/jsx/index.js.map +1 -1
  47. package/dist/jsx/render.d.ts +7 -1
  48. package/dist/jsx/render.d.ts.map +1 -1
  49. package/dist/jsx/render.js +96 -14
  50. package/dist/jsx/render.js.map +1 -1
  51. package/dist/jsx/types.d.ts +4 -0
  52. package/dist/jsx/types.d.ts.map +1 -1
  53. package/dist/jsx/types.js +10 -0
  54. package/dist/jsx/types.js.map +1 -1
  55. package/dist/router.d.ts +7 -1
  56. package/dist/router.d.ts.map +1 -1
  57. package/dist/router.js +84 -3
  58. package/dist/router.js.map +1 -1
  59. package/dist/types.d.ts +2 -0
  60. package/dist/types.d.ts.map +1 -1
  61. package/package.json +7 -2
  62. package/scripts/dev.js +28 -1
  63. package/scripts/generate.js +228 -10
@@ -3,6 +3,13 @@ const path = require("node:path");
3
3
  const { execSync } = require("node:child_process");
4
4
  const ts = require("typescript");
5
5
 
6
+ let esbuild;
7
+ try {
8
+ esbuild = require("esbuild");
9
+ } catch (_esbuildOptional) {
10
+ esbuild = null;
11
+ }
12
+
6
13
  const PROJECT_ROOT = process.cwd();
7
14
 
8
15
  function unwrapExpression(node) {
@@ -166,6 +173,111 @@ function middlewarePathToDir(filePath) {
166
173
  .replace(/\/$/, "");
167
174
  }
168
175
 
176
+ function detectUseClientDirective(content) {
177
+ const sourceFile = ts.createSourceFile(
178
+ "file.tsx",
179
+ content,
180
+ ts.ScriptTarget.Latest,
181
+ true,
182
+ );
183
+ const firstStatement = sourceFile.statements[0];
184
+ if (!firstStatement) return false;
185
+ return (
186
+ ts.isExpressionStatement(firstStatement) &&
187
+ ts.isStringLiteral(firstStatement.expression) &&
188
+ firstStatement.expression.text === "use client"
189
+ );
190
+ }
191
+
192
+ function getClientFiles(dir, base = "") {
193
+ if (!fs.existsSync(dir)) return [];
194
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
195
+ const files = [];
196
+
197
+ for (const entry of entries) {
198
+ const fullPath = path.join(dir, entry.name);
199
+ const relativePath = path.join(base, entry.name);
200
+
201
+ if (entry.isDirectory()) {
202
+ if (entry.name === "node_modules" || entry.name === ".rainjs") continue;
203
+ files.push(...getClientFiles(fullPath, relativePath));
204
+ } else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) {
205
+ const content = fs.readFileSync(fullPath, "utf-8");
206
+ if (detectUseClientDirective(content)) {
207
+ files.push(relativePath);
208
+ }
209
+ }
210
+ }
211
+
212
+ return files;
213
+ }
214
+
215
+ function bundleClientFilesSync(clientFiles, srcDir) {
216
+ if (clientFiles.length === 0) return [];
217
+ if (!esbuild) {
218
+ console.warn(
219
+ "[Rain] Warning: esbuild not found.\n" +
220
+ " → Install esbuild to enable client bundling: npm install -D esbuild\n" +
221
+ " → Client-side components will not be bundled.",
222
+ );
223
+ return [];
224
+ }
225
+
226
+ const outDir = path.join(PROJECT_ROOT, "public", "_rain");
227
+ if (!fs.existsSync(outDir)) {
228
+ fs.mkdirSync(outDir, { recursive: true });
229
+ }
230
+
231
+ for (const file of fs.readdirSync(outDir)) {
232
+ if (file.startsWith("island-")) {
233
+ fs.unlinkSync(path.join(outDir, file));
234
+ }
235
+ }
236
+
237
+ const entryPoints = clientFiles.map((f) => path.join(srcDir, f));
238
+
239
+ const result = esbuild.buildSync({
240
+ entryPoints,
241
+ outdir: outDir,
242
+ bundle: true,
243
+ minify: true,
244
+ format: "esm",
245
+ metafile: true,
246
+ entryNames: "island-[hash]",
247
+ write: true,
248
+ treeShaking: true,
249
+ platform: "browser",
250
+ target: ["es2022"],
251
+ jsx: "automatic",
252
+ jsxImportSource: "@rainfw/core",
253
+ alias: {
254
+ "@rainfw/core/jsx-runtime": path.resolve(
255
+ PROJECT_ROOT,
256
+ "src/framework/client/jsx-runtime.ts",
257
+ ),
258
+ },
259
+ loader: { ".ts": "ts", ".tsx": "tsx" },
260
+ });
261
+
262
+ const publicDir = path.join(PROJECT_ROOT, "public");
263
+ const scripts = [];
264
+ for (const [outPath, meta] of Object.entries(result.metafile.outputs)) {
265
+ if (meta.entryPoint) {
266
+ const relPath = path.relative(publicDir, outPath);
267
+ scripts.push(`/${relPath.replace(/\\/g, "/")}`);
268
+ }
269
+ }
270
+
271
+ return scripts;
272
+ }
273
+
274
+ function stripRouteGroupSegments(filePath) {
275
+ return filePath
276
+ .split("/")
277
+ .filter((segment) => !/^\(.+\)$/.test(segment))
278
+ .join("/");
279
+ }
280
+
169
281
  function routePathToDir(filePath) {
170
282
  return filePath
171
283
  .replace(/\\/g, "/")
@@ -180,6 +292,7 @@ function middlewareImportName(filePath) {
180
292
  .replace(/\//g, "_")
181
293
  .replace(/\[/g, "$")
182
294
  .replace(/\]/g, "")
295
+ .replace(/[()]/g, "")
183
296
  .replace(/-/g, "_")
184
297
  .replace(/_+$/, "");
185
298
  return `mw_${base || "root"}`;
@@ -202,6 +315,7 @@ function layoutPathToDir(filePath) {
202
315
  function pageFilePathToUrlPath(filePath) {
203
316
  let urlPath = filePath.replace(/\.tsx?$/, "");
204
317
  urlPath = urlPath.replace(/\\/g, "/");
318
+ urlPath = stripRouteGroupSegments(urlPath);
205
319
  urlPath = urlPath.replace(/\[([^\]]+)\]/g, ":$1");
206
320
  urlPath = urlPath.replace(/\/page$/, "");
207
321
  if (urlPath === "page") urlPath = "";
@@ -217,6 +331,7 @@ function pageFilePathToImportName(filePath) {
217
331
  .replace(/\//g, "_")
218
332
  .replace(/\[/g, "$")
219
333
  .replace(/\]/g, "")
334
+ .replace(/[()]/g, "")
220
335
  .replace(/-/g, "_")
221
336
  );
222
337
  }
@@ -228,6 +343,7 @@ function layoutImportName(filePath) {
228
343
  .replace(/\//g, "_")
229
344
  .replace(/\[/g, "$")
230
345
  .replace(/\]/g, "")
346
+ .replace(/[()]/g, "")
231
347
  .replace(/-/g, "_")
232
348
  .replace(/_+$/, "");
233
349
  return `layout_${base || "root"}`;
@@ -297,6 +413,63 @@ function validateNoPageRouteColocation(routeFiles, pageFiles) {
297
413
  }
298
414
  }
299
415
 
416
+ const routeUrlMap = new Map();
417
+ for (const f of routeFiles) {
418
+ routeUrlMap.set(filePathToUrlPath(f), f);
419
+ }
420
+ for (const pageFile of pageFiles) {
421
+ const url = pageFilePathToUrlPath(pageFile);
422
+ const conflicting = routeUrlMap.get(url);
423
+ if (conflicting) {
424
+ const pageDir = pageFilePathToDir(pageFile);
425
+ if (!routeDirs.has(pageDir)) {
426
+ errors.push(
427
+ `[Rain] Error: page "${pageFile}" and route "${conflicting}" resolve to the same URL path "${url}":\n` +
428
+ " → This conflict occurs because route group folders are stripped from URLs.\n" +
429
+ " → Move one of them to a different URL path.",
430
+ );
431
+ }
432
+ }
433
+ }
434
+
435
+ return errors;
436
+ }
437
+
438
+ function validateNoDuplicateUrls(routeFiles, pageFiles) {
439
+ const errors = [];
440
+
441
+ const routeUrlMap = new Map();
442
+ for (const f of routeFiles) {
443
+ const url = filePathToUrlPath(f);
444
+ if (routeUrlMap.has(url)) {
445
+ errors.push(
446
+ `[Rain] Error: multiple route files resolve to the same URL path "${url}":\n` +
447
+ ` → ${routeUrlMap.get(url)}\n` +
448
+ ` → ${f}\n` +
449
+ " → Route group folders are stripped from URLs.\n" +
450
+ " → Rename one of the routes to avoid the conflict.",
451
+ );
452
+ } else {
453
+ routeUrlMap.set(url, f);
454
+ }
455
+ }
456
+
457
+ const pageUrlMap = new Map();
458
+ for (const f of pageFiles) {
459
+ const url = pageFilePathToUrlPath(f);
460
+ if (pageUrlMap.has(url)) {
461
+ errors.push(
462
+ `[Rain] Error: multiple page files resolve to the same URL path "${url}":\n` +
463
+ ` → ${pageUrlMap.get(url)}\n` +
464
+ ` → ${f}\n` +
465
+ " → Route group folders are stripped from URLs.\n" +
466
+ " → Rename one of the pages to avoid the conflict.",
467
+ );
468
+ } else {
469
+ pageUrlMap.set(url, f);
470
+ }
471
+ }
472
+
300
473
  return errors;
301
474
  }
302
475
 
@@ -324,6 +497,7 @@ function getMiddlewaresForRoute(routeFile, middlewareFiles) {
324
497
  function filePathToUrlPath(filePath) {
325
498
  let urlPath = filePath.replace(/\.tsx?$/, "");
326
499
  urlPath = urlPath.replace(/\\/g, "/");
500
+ urlPath = stripRouteGroupSegments(urlPath);
327
501
  urlPath = urlPath.replace(/\[([^\]]+)\]/g, ":$1");
328
502
  urlPath = urlPath.replace(/\/route$/, "");
329
503
  if (urlPath === "route") urlPath = "";
@@ -339,6 +513,7 @@ function filePathToImportName(filePath) {
339
513
  .replace(/\//g, "_")
340
514
  .replace(/\[/g, "$")
341
515
  .replace(/\]/g, "")
516
+ .replace(/[()]/g, "")
342
517
  .replace(/-/g, "_")
343
518
  );
344
519
  }
@@ -546,13 +721,9 @@ function validateCompatibilityFlags() {
546
721
  if (!fs.existsSync(wranglerPath)) return;
547
722
 
548
723
  const content = fs.readFileSync(wranglerPath, "utf-8");
549
- const flagsMatch = content.match(
550
- /compatibility_flags\s*=\s*\[([^\]]*)\]/,
551
- );
724
+ const flagsMatch = content.match(/compatibility_flags\s*=\s*\[([^\]]*)\]/);
552
725
  const flags = flagsMatch
553
- ? (flagsMatch[1].match(/"([^"]+)"/g) || []).map((s) =>
554
- s.replace(/"/g, ""),
555
- )
726
+ ? (flagsMatch[1].match(/"([^"]+)"/g) || []).map((s) => s.replace(/"/g, ""))
556
727
  : [];
557
728
 
558
729
  if (!(flags.includes("nodejs_compat") || flags.includes("nodejs_als"))) {
@@ -571,6 +742,37 @@ function validateCompatibilityFlags() {
571
742
  }
572
743
  }
573
744
 
745
+ function buildAppInitLine(clientScripts, hasConfig) {
746
+ if (hasConfig) {
747
+ return clientScripts.length > 0
748
+ ? `const app = new Rain({ ...config, clientScripts: ${JSON.stringify(clientScripts)} });`
749
+ : "const app = new Rain(config);";
750
+ }
751
+ return clientScripts.length > 0
752
+ ? `const app = new Rain({ clientScripts: ${JSON.stringify(clientScripts)} });`
753
+ : "const app = new Rain();";
754
+ }
755
+
756
+ function regenerateClient() {
757
+ const srcDir = path.join(PROJECT_ROOT, "src");
758
+ const clientFiles = getClientFiles(srcDir);
759
+ const clientScripts = bundleClientFilesSync(clientFiles, srcDir);
760
+
761
+ if (!fs.existsSync(ENTRY_FILE)) return;
762
+
763
+ const content = fs.readFileSync(ENTRY_FILE, "utf-8");
764
+ const hasConfig = fs.existsSync(CONFIG_FILE);
765
+ const appInit = buildAppInitLine(clientScripts, hasConfig);
766
+ const updated = content.replace(/^const app = new Rain\(.*\);$/m, appInit);
767
+ if (updated !== content) {
768
+ fs.writeFileSync(ENTRY_FILE, updated);
769
+ }
770
+
771
+ const clientMsg =
772
+ clientFiles.length > 0 ? `${clientFiles.length} client` : "0 client";
773
+ console.log(`[gen:client] ${clientMsg} -> .rainjs/entry.ts`);
774
+ }
775
+
574
776
  function generate() {
575
777
  if (!fs.existsSync(ROUTES_DIR)) {
576
778
  console.error(
@@ -618,6 +820,12 @@ function generate() {
618
820
  process.exit(1);
619
821
  }
620
822
 
823
+ const duplicateErrors = validateNoDuplicateUrls(files, pageFiles);
824
+ for (const err of duplicateErrors) {
825
+ console.error(err);
826
+ process.exit(1);
827
+ }
828
+
621
829
  const hasRootLayout = layoutFiles.some((f) => layoutPathToDir(f) === "");
622
830
 
623
831
  processMiddlewares(middlewareFiles, imports);
@@ -632,6 +840,10 @@ function generate() {
632
840
  registrations,
633
841
  );
634
842
 
843
+ const srcDir = path.join(PROJECT_ROOT, "src");
844
+ const clientFiles = getClientFiles(srcDir);
845
+ const clientScripts = bundleClientFilesSync(clientFiles, srcDir);
846
+
635
847
  const hasConfig = fs.existsSync(CONFIG_FILE);
636
848
  const fwPkg = BUILD_CONFIG.frameworkPackage;
637
849
  const frameworkImport =
@@ -647,9 +859,7 @@ function generate() {
647
859
  headerImports.push(`import config from "${configPath}";`);
648
860
  }
649
861
 
650
- const appInit = hasConfig
651
- ? "const app = new Rain(config);"
652
- : "const app = new Rain();";
862
+ const appInit = buildAppInitLine(clientScripts, hasConfig);
653
863
 
654
864
  const content = [
655
865
  ...headerImports,
@@ -665,18 +875,22 @@ function generate() {
665
875
 
666
876
  fs.writeFileSync(ENTRY_FILE, content);
667
877
  const total = files.length + pageFiles.length;
878
+ const clientMsg =
879
+ clientFiles.length > 0 ? `, ${clientFiles.length} client` : "";
668
880
  console.log(
669
- `[gen] ${total} route(s) (${files.length} api, ${pageFiles.length} page, ${layoutFiles.length} layout) -> .rainjs/entry.ts`,
881
+ `[gen] ${total} route(s) (${files.length} api, ${pageFiles.length} page, ${layoutFiles.length} layout${clientMsg}) -> .rainjs/entry.ts`,
670
882
  );
671
883
  }
672
884
 
673
885
  module.exports = {
674
886
  generate,
887
+ regenerateClient,
675
888
  loadBuildConfig,
676
889
  getRouteFiles,
677
890
  getMiddlewareFiles,
678
891
  getPageFiles,
679
892
  getLayoutFiles,
893
+ getClientFiles,
680
894
  getMiddlewaresForRoute,
681
895
  getLayoutsForPage,
682
896
  filePathToUrlPath,
@@ -691,7 +905,11 @@ module.exports = {
691
905
  detectMiddlewareExportFromContent,
692
906
  detectDefaultExport,
693
907
  detectDefaultExportFromContent,
908
+ detectUseClientDirective,
909
+ bundleClientFilesSync,
694
910
  validateNoPageRouteColocation,
911
+ validateNoDuplicateUrls,
912
+ stripRouteGroupSegments,
695
913
  ROUTES_DIR,
696
914
  ENTRY_FILE,
697
915
  HTTP_METHODS,