@rainfw/core 0.2.0 → 0.2.2
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/package.json +7 -2
- package/scripts/generate.js +295 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rainfw/core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "A TypeScript web framework for Cloudflare Workers",
|
|
5
5
|
"bin": {
|
|
6
6
|
"rainjs": "./cli/index.js"
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"test:generate": "node --test tests/generate.test.js",
|
|
22
22
|
"bench": "node scripts/bench.js",
|
|
23
23
|
"build:pkg": "tsc -p tsconfig.build.json && node -e \"require('fs').writeFileSync('dist/package.json',JSON.stringify({type:'module'}))\"",
|
|
24
|
-
"prepublishOnly": "npm run build:pkg"
|
|
24
|
+
"prepublishOnly": "npm run build:pkg",
|
|
25
|
+
"release": "npm run ci && npm publish --access public"
|
|
25
26
|
},
|
|
26
27
|
"keywords": [
|
|
27
28
|
"cloudflare-workers",
|
|
@@ -57,6 +58,10 @@
|
|
|
57
58
|
"./db": {
|
|
58
59
|
"types": "./dist/db.d.ts",
|
|
59
60
|
"import": "./dist/db.js"
|
|
61
|
+
},
|
|
62
|
+
"./client": {
|
|
63
|
+
"types": "./dist/client/hooks.d.ts",
|
|
64
|
+
"import": "./dist/client/hooks.js"
|
|
60
65
|
}
|
|
61
66
|
},
|
|
62
67
|
"devDependencies": {
|
package/scripts/generate.js
CHANGED
|
@@ -271,6 +271,13 @@ function bundleClientFilesSync(clientFiles, srcDir) {
|
|
|
271
271
|
return scripts;
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
function stripRouteGroupSegments(filePath) {
|
|
275
|
+
return filePath
|
|
276
|
+
.split("/")
|
|
277
|
+
.filter((segment) => !/^\(.+\)$/.test(segment))
|
|
278
|
+
.join("/");
|
|
279
|
+
}
|
|
280
|
+
|
|
274
281
|
function routePathToDir(filePath) {
|
|
275
282
|
return filePath
|
|
276
283
|
.replace(/\\/g, "/")
|
|
@@ -285,6 +292,7 @@ function middlewareImportName(filePath) {
|
|
|
285
292
|
.replace(/\//g, "_")
|
|
286
293
|
.replace(/\[/g, "$")
|
|
287
294
|
.replace(/\]/g, "")
|
|
295
|
+
.replace(/[()]/g, "")
|
|
288
296
|
.replace(/-/g, "_")
|
|
289
297
|
.replace(/_+$/, "");
|
|
290
298
|
return `mw_${base || "root"}`;
|
|
@@ -307,6 +315,7 @@ function layoutPathToDir(filePath) {
|
|
|
307
315
|
function pageFilePathToUrlPath(filePath) {
|
|
308
316
|
let urlPath = filePath.replace(/\.tsx?$/, "");
|
|
309
317
|
urlPath = urlPath.replace(/\\/g, "/");
|
|
318
|
+
urlPath = stripRouteGroupSegments(urlPath);
|
|
310
319
|
urlPath = urlPath.replace(/\[([^\]]+)\]/g, ":$1");
|
|
311
320
|
urlPath = urlPath.replace(/\/page$/, "");
|
|
312
321
|
if (urlPath === "page") urlPath = "";
|
|
@@ -322,6 +331,7 @@ function pageFilePathToImportName(filePath) {
|
|
|
322
331
|
.replace(/\//g, "_")
|
|
323
332
|
.replace(/\[/g, "$")
|
|
324
333
|
.replace(/\]/g, "")
|
|
334
|
+
.replace(/[()]/g, "")
|
|
325
335
|
.replace(/-/g, "_")
|
|
326
336
|
);
|
|
327
337
|
}
|
|
@@ -333,6 +343,7 @@ function layoutImportName(filePath) {
|
|
|
333
343
|
.replace(/\//g, "_")
|
|
334
344
|
.replace(/\[/g, "$")
|
|
335
345
|
.replace(/\]/g, "")
|
|
346
|
+
.replace(/[()]/g, "")
|
|
336
347
|
.replace(/-/g, "_")
|
|
337
348
|
.replace(/_+$/, "");
|
|
338
349
|
return `layout_${base || "root"}`;
|
|
@@ -402,6 +413,63 @@ function validateNoPageRouteColocation(routeFiles, pageFiles) {
|
|
|
402
413
|
}
|
|
403
414
|
}
|
|
404
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
|
+
|
|
405
473
|
return errors;
|
|
406
474
|
}
|
|
407
475
|
|
|
@@ -429,6 +497,7 @@ function getMiddlewaresForRoute(routeFile, middlewareFiles) {
|
|
|
429
497
|
function filePathToUrlPath(filePath) {
|
|
430
498
|
let urlPath = filePath.replace(/\.tsx?$/, "");
|
|
431
499
|
urlPath = urlPath.replace(/\\/g, "/");
|
|
500
|
+
urlPath = stripRouteGroupSegments(urlPath);
|
|
432
501
|
urlPath = urlPath.replace(/\[([^\]]+)\]/g, ":$1");
|
|
433
502
|
urlPath = urlPath.replace(/\/route$/, "");
|
|
434
503
|
if (urlPath === "route") urlPath = "";
|
|
@@ -444,6 +513,7 @@ function filePathToImportName(filePath) {
|
|
|
444
513
|
.replace(/\//g, "_")
|
|
445
514
|
.replace(/\[/g, "$")
|
|
446
515
|
.replace(/\]/g, "")
|
|
516
|
+
.replace(/[()]/g, "")
|
|
447
517
|
.replace(/-/g, "_")
|
|
448
518
|
);
|
|
449
519
|
}
|
|
@@ -517,6 +587,65 @@ function detectExportedMethods(filePath) {
|
|
|
517
587
|
return detectExportedMethodsFromContent(content);
|
|
518
588
|
}
|
|
519
589
|
|
|
590
|
+
function collectAllExportedNames(node) {
|
|
591
|
+
if (ts.isVariableStatement(node)) {
|
|
592
|
+
return node.declarationList.declarations
|
|
593
|
+
.filter((d) => ts.isIdentifier(d.name))
|
|
594
|
+
.map((d) => d.name.text);
|
|
595
|
+
}
|
|
596
|
+
if (
|
|
597
|
+
(ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) &&
|
|
598
|
+
node.name
|
|
599
|
+
) {
|
|
600
|
+
return [node.name.text];
|
|
601
|
+
}
|
|
602
|
+
return [];
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function collectAllNamedExports(node) {
|
|
606
|
+
if (
|
|
607
|
+
!(
|
|
608
|
+
ts.isExportDeclaration(node) &&
|
|
609
|
+
node.exportClause &&
|
|
610
|
+
ts.isNamedExports(node.exportClause)
|
|
611
|
+
)
|
|
612
|
+
) {
|
|
613
|
+
return [];
|
|
614
|
+
}
|
|
615
|
+
return node.exportClause.elements.map((el) => el.name.text);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function detectAllExportsFromContent(content) {
|
|
619
|
+
const sourceFile = ts.createSourceFile(
|
|
620
|
+
"file.tsx",
|
|
621
|
+
content,
|
|
622
|
+
ts.ScriptTarget.Latest,
|
|
623
|
+
true,
|
|
624
|
+
);
|
|
625
|
+
const named = [];
|
|
626
|
+
let hasDefault = false;
|
|
627
|
+
|
|
628
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
629
|
+
if (ts.isExportAssignment(node) && !node.isExportEquals) {
|
|
630
|
+
hasDefault = true;
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (hasExportKeyword(node)) {
|
|
634
|
+
const modifiers = ts.canHaveModifiers(node)
|
|
635
|
+
? ts.getModifiers(node)
|
|
636
|
+
: undefined;
|
|
637
|
+
if (modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword)) {
|
|
638
|
+
hasDefault = true;
|
|
639
|
+
} else {
|
|
640
|
+
named.push(...collectAllExportedNames(node));
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
named.push(...collectAllNamedExports(node));
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
return { named, hasDefault };
|
|
647
|
+
}
|
|
648
|
+
|
|
520
649
|
function detectMiddlewareExportFromContent(content) {
|
|
521
650
|
return detectExportedNamesFromContent(content, ["onRequest"]).length > 0;
|
|
522
651
|
}
|
|
@@ -703,6 +832,156 @@ function regenerateClient() {
|
|
|
703
832
|
console.log(`[gen:client] ${clientMsg} -> .rainjs/entry.ts`);
|
|
704
833
|
}
|
|
705
834
|
|
|
835
|
+
function clientFileToIslandId(relPath) {
|
|
836
|
+
return relPath
|
|
837
|
+
.replace(/\\/g, "/")
|
|
838
|
+
.replace(/\.tsx?$/, "")
|
|
839
|
+
.replace(/[^a-zA-Z0-9_/]/g, "_");
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function generateIslandProxy(clientRelPath, srcDir, fwImport) {
|
|
843
|
+
const islandDir = path.join(PROJECT_ROOT, BUILD_CONFIG.outDir, "islands");
|
|
844
|
+
if (!fs.existsSync(islandDir)) {
|
|
845
|
+
fs.mkdirSync(islandDir, { recursive: true });
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const fullPath = path.join(srcDir, clientRelPath);
|
|
849
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
850
|
+
const { named, hasDefault } = detectAllExportsFromContent(content);
|
|
851
|
+
const islandId = clientFileToIslandId(clientRelPath);
|
|
852
|
+
|
|
853
|
+
const proxyFile = path.join(
|
|
854
|
+
islandDir,
|
|
855
|
+
clientRelPath.replace(/\\/g, "/").replace(/\.tsx?$/, ".ts"),
|
|
856
|
+
);
|
|
857
|
+
const proxyDir = path.dirname(proxyFile);
|
|
858
|
+
if (!fs.existsSync(proxyDir)) {
|
|
859
|
+
fs.mkdirSync(proxyDir, { recursive: true });
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const proxyEntryDir = path.dirname(proxyFile);
|
|
863
|
+
let relOriginal = path
|
|
864
|
+
.relative(
|
|
865
|
+
proxyEntryDir,
|
|
866
|
+
path.join(srcDir, clientRelPath.replace(/\.tsx?$/, "")),
|
|
867
|
+
)
|
|
868
|
+
.replace(/\\/g, "/");
|
|
869
|
+
if (!relOriginal.startsWith(".")) relOriginal = `./${relOriginal}`;
|
|
870
|
+
|
|
871
|
+
const lines = [];
|
|
872
|
+
lines.push(`import { markAsIsland } from "${fwImport}";`);
|
|
873
|
+
|
|
874
|
+
const importSpecifiers = [];
|
|
875
|
+
if (hasDefault) {
|
|
876
|
+
importSpecifiers.push("default as _default");
|
|
877
|
+
}
|
|
878
|
+
for (const name of named) {
|
|
879
|
+
importSpecifiers.push(`${name} as _${name}`);
|
|
880
|
+
}
|
|
881
|
+
if (importSpecifiers.length > 0) {
|
|
882
|
+
lines.push(
|
|
883
|
+
`import { ${importSpecifiers.join(", ")} } from "${relOriginal}";`,
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
lines.push("");
|
|
887
|
+
|
|
888
|
+
if (hasDefault) {
|
|
889
|
+
lines.push(`export default markAsIsland("${islandId}:default", _default);`);
|
|
890
|
+
}
|
|
891
|
+
for (const name of named) {
|
|
892
|
+
lines.push(
|
|
893
|
+
`export const ${name} = markAsIsland("${islandId}:${name}", _${name});`,
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
lines.push("");
|
|
897
|
+
|
|
898
|
+
fs.writeFileSync(proxyFile, lines.join("\n"));
|
|
899
|
+
return { proxyFile, islandId, named, hasDefault };
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function generateAllIslandProxies(clientFiles, srcDir, fwImport) {
|
|
903
|
+
const islandDir = path.join(PROJECT_ROOT, BUILD_CONFIG.outDir, "islands");
|
|
904
|
+
if (fs.existsSync(islandDir)) {
|
|
905
|
+
fs.rmSync(islandDir, { recursive: true, force: true });
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const proxies = [];
|
|
909
|
+
for (const cf of clientFiles) {
|
|
910
|
+
proxies.push(generateIslandProxy(cf, srcDir, fwImport));
|
|
911
|
+
}
|
|
912
|
+
return proxies;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function updateWranglerAliases(clientFiles, srcDir) {
|
|
916
|
+
const wranglerPath = path.join(PROJECT_ROOT, "wrangler.toml");
|
|
917
|
+
if (!fs.existsSync(wranglerPath)) return;
|
|
918
|
+
|
|
919
|
+
let content = fs.readFileSync(wranglerPath, "utf-8");
|
|
920
|
+
|
|
921
|
+
const markerStart = "# [rain:alias:start]";
|
|
922
|
+
const markerEnd = "# [rain:alias:end]";
|
|
923
|
+
|
|
924
|
+
const startIdx = content.indexOf(markerStart);
|
|
925
|
+
const endIdx = content.indexOf(markerEnd);
|
|
926
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
927
|
+
content =
|
|
928
|
+
content.slice(0, startIdx).trimEnd() +
|
|
929
|
+
"\n" +
|
|
930
|
+
content.slice(endIdx + markerEnd.length).trimStart();
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (clientFiles.length === 0) {
|
|
934
|
+
const cleaned = `${content.trimEnd()}
|
|
935
|
+
`;
|
|
936
|
+
if (cleaned !== fs.readFileSync(wranglerPath, "utf-8")) {
|
|
937
|
+
fs.writeFileSync(wranglerPath, cleaned);
|
|
938
|
+
}
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const islandDir = path.join(PROJECT_ROOT, BUILD_CONFIG.outDir, "islands");
|
|
943
|
+
|
|
944
|
+
const aliasLines = [markerStart];
|
|
945
|
+
const hasExistingAlias = /^\[alias\]/m.test(content);
|
|
946
|
+
if (!hasExistingAlias) {
|
|
947
|
+
aliasLines.push("[alias]");
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
for (const cf of clientFiles) {
|
|
951
|
+
const originalAbs = path.join(srcDir, cf.replace(/\.tsx?$/, ""));
|
|
952
|
+
const proxyAbs = path.join(
|
|
953
|
+
islandDir,
|
|
954
|
+
cf.replace(/\\/g, "/").replace(/\.tsx?$/, ".ts"),
|
|
955
|
+
);
|
|
956
|
+
const relProxy = path.relative(PROJECT_ROOT, proxyAbs).replace(/\\/g, "/");
|
|
957
|
+
const relOriginal = path
|
|
958
|
+
.relative(PROJECT_ROOT, originalAbs)
|
|
959
|
+
.replace(/\\/g, "/");
|
|
960
|
+
aliasLines.push(`"./${relOriginal}" = "./${relProxy}"`);
|
|
961
|
+
}
|
|
962
|
+
aliasLines.push(markerEnd);
|
|
963
|
+
|
|
964
|
+
const aliasBlock = aliasLines.join("\n");
|
|
965
|
+
|
|
966
|
+
if (hasExistingAlias) {
|
|
967
|
+
const aliasIdx = content.search(/^\[alias\]/m);
|
|
968
|
+
let insertAt = content.indexOf("\n", aliasIdx);
|
|
969
|
+
if (insertAt === -1) insertAt = content.length;
|
|
970
|
+
content =
|
|
971
|
+
content.slice(0, insertAt + 1) +
|
|
972
|
+
aliasLines.filter((l) => l !== "[alias]").join("\n") +
|
|
973
|
+
"\n" +
|
|
974
|
+
content.slice(insertAt + 1);
|
|
975
|
+
} else {
|
|
976
|
+
content = `${content.trimEnd()}
|
|
977
|
+
|
|
978
|
+
${aliasBlock}
|
|
979
|
+
`;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
fs.writeFileSync(wranglerPath, content);
|
|
983
|
+
}
|
|
984
|
+
|
|
706
985
|
function generate() {
|
|
707
986
|
if (!fs.existsSync(ROUTES_DIR)) {
|
|
708
987
|
console.error(
|
|
@@ -750,6 +1029,12 @@ function generate() {
|
|
|
750
1029
|
process.exit(1);
|
|
751
1030
|
}
|
|
752
1031
|
|
|
1032
|
+
const duplicateErrors = validateNoDuplicateUrls(files, pageFiles);
|
|
1033
|
+
for (const err of duplicateErrors) {
|
|
1034
|
+
console.error(err);
|
|
1035
|
+
process.exit(1);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
753
1038
|
const hasRootLayout = layoutFiles.some((f) => layoutPathToDir(f) === "");
|
|
754
1039
|
|
|
755
1040
|
processMiddlewares(middlewareFiles, imports);
|
|
@@ -775,6 +1060,9 @@ function generate() {
|
|
|
775
1060
|
? relativeImportPath(path.join(PROJECT_ROOT, fwPkg))
|
|
776
1061
|
: fwPkg;
|
|
777
1062
|
|
|
1063
|
+
generateAllIslandProxies(clientFiles, srcDir, frameworkImport);
|
|
1064
|
+
updateWranglerAliases(clientFiles, srcDir);
|
|
1065
|
+
|
|
778
1066
|
const headerImports = [`import { Rain } from "${frameworkImport}";`];
|
|
779
1067
|
if (hasConfig) {
|
|
780
1068
|
const configPath = relativeImportPath(
|
|
@@ -830,8 +1118,15 @@ module.exports = {
|
|
|
830
1118
|
detectDefaultExport,
|
|
831
1119
|
detectDefaultExportFromContent,
|
|
832
1120
|
detectUseClientDirective,
|
|
1121
|
+
detectAllExportsFromContent,
|
|
1122
|
+
generateIslandProxy,
|
|
1123
|
+
generateAllIslandProxies,
|
|
1124
|
+
updateWranglerAliases,
|
|
1125
|
+
clientFileToIslandId,
|
|
833
1126
|
bundleClientFilesSync,
|
|
834
1127
|
validateNoPageRouteColocation,
|
|
1128
|
+
validateNoDuplicateUrls,
|
|
1129
|
+
stripRouteGroupSegments,
|
|
835
1130
|
ROUTES_DIR,
|
|
836
1131
|
ENTRY_FILE,
|
|
837
1132
|
HTTP_METHODS,
|