@mandujs/core 0.19.2 → 0.20.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/core",
3
- "version": "0.19.2",
3
+ "version": "0.20.1",
4
4
  "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -11,11 +11,67 @@ import type {
11
11
  BundleManifest,
12
12
  BundleStats,
13
13
  BundlerOptions,
14
+ IslandFileEntry,
14
15
  } from "./types";
15
16
  import { HYDRATION } from "../constants";
16
17
  import path from "path";
17
18
  import fs from "fs/promises";
18
19
 
20
+ /** Scan for *.island.tsx / *.island.ts files across hydrated route directories. */
21
+ async function scanIslandFiles(routes: RouteSpec[], rootDir: string): Promise<IslandFileEntry[]> {
22
+ const entries: IslandFileEntry[] = [];
23
+ const seenDirs = new Set<string>();
24
+
25
+ for (const route of routes) {
26
+ const dir = path.dirname(path.join(rootDir, route.componentModule ?? route.module));
27
+ if (seenDirs.has(dir)) continue;
28
+ seenDirs.add(dir);
29
+
30
+ let files: string[];
31
+ try { files = await fs.readdir(dir); } catch { continue; }
32
+
33
+ const priority = getRouteHydration(route)?.priority || HYDRATION.DEFAULT_PRIORITY;
34
+ for (const file of files) {
35
+ if (/\.island\.tsx?$/.test(file)) {
36
+ entries.push({
37
+ name: file.replace(/\.island\.tsx?$/, ""),
38
+ filePath: path.join(dir, file),
39
+ routeId: route.id,
40
+ priority,
41
+ });
42
+ }
43
+ }
44
+ }
45
+ return entries;
46
+ }
47
+
48
+ /** Build a single per-island bundle. */
49
+ async function buildPerIslandBundle(
50
+ entry: IslandFileEntry, outDir: string, options: BundlerOptions
51
+ ): Promise<{ name: string; js: string; route: string; priority: IslandFileEntry["priority"] }> {
52
+ const entryPath = path.join(outDir, `_entry_island_${entry.name}.js`);
53
+ const outputName = `${entry.name}.island.js`;
54
+ try {
55
+ await Bun.write(entryPath, generateIslandEntry(entry.name, entry.filePath));
56
+ const result = await Bun.build({
57
+ entrypoints: [entryPath],
58
+ outdir: outDir,
59
+ naming: outputName,
60
+ minify: options.minify ?? process.env.NODE_ENV === "production",
61
+ sourcemap: options.sourcemap ? "external" : "none",
62
+ target: "browser",
63
+ external: ["react", "react-dom", "react-dom/client", ...(options.external || [])],
64
+ define: { "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development"), ...options.define },
65
+ });
66
+ await fs.unlink(entryPath).catch(() => {});
67
+ if (!result.success) throw new Error(result.logs.map((l) => l.message).join("\n"));
68
+ return { name: entry.name, js: `/.mandu/client/${outputName}`, route: entry.routeId, priority: entry.priority };
69
+ } catch (error) {
70
+ await fs.unlink(entryPath).catch(() => {});
71
+ throw error;
72
+ }
73
+ }
74
+
19
75
  /**
20
76
  * 빈 매니페스트 생성
21
77
  */
@@ -1292,7 +1348,8 @@ function createBundleManifest(
1292
1348
  runtimePath: string,
1293
1349
  vendorResult: VendorBuildResult,
1294
1350
  routerPath: string,
1295
- env: "development" | "production"
1351
+ env: "development" | "production",
1352
+ islandBundles?: Array<{ name: string; js: string; route: string; priority: IslandFileEntry["priority"] }>
1296
1353
  ): BundleManifest {
1297
1354
  const bundles: BundleManifest["bundles"] = {};
1298
1355
 
@@ -1307,11 +1364,25 @@ function createBundleManifest(
1307
1364
  };
1308
1365
  }
1309
1366
 
1367
+ // Per-island bundles (code splitting)
1368
+ let islands: BundleManifest["islands"];
1369
+ if (islandBundles && islandBundles.length > 0) {
1370
+ islands = {};
1371
+ for (const ib of islandBundles) {
1372
+ islands[ib.name] = {
1373
+ js: ib.js,
1374
+ route: ib.route,
1375
+ priority: ib.priority,
1376
+ };
1377
+ }
1378
+ }
1379
+
1310
1380
  return {
1311
1381
  version: 1,
1312
1382
  buildTime: new Date().toISOString(),
1313
1383
  env,
1314
1384
  bundles,
1385
+ ...(islands ? { islands } : {}),
1315
1386
  shared: {
1316
1387
  runtime: runtimePath,
1317
1388
  vendor: vendorResult.react, // primary vendor for backwards compatibility
@@ -1544,6 +1615,26 @@ export async function buildClientBundles(
1544
1615
  }
1545
1616
  }
1546
1617
 
1618
+ // 5.5. Per-island code splitting: scan and build individual island bundles
1619
+ const islandFiles = await scanIslandFiles(hydratedRoutes, rootDir);
1620
+ const islandBundles: Array<{ name: string; js: string; route: string; priority: IslandFileEntry["priority"] }> = [];
1621
+
1622
+ if (islandFiles.length > 0) {
1623
+ const islandResults = await Promise.all(
1624
+ islandFiles.map(async (entry) => {
1625
+ try {
1626
+ return await buildPerIslandBundle(entry, outDir, options);
1627
+ } catch (error) {
1628
+ errors.push(`[island:${entry.name}] ${String(error)}`);
1629
+ return null;
1630
+ }
1631
+ })
1632
+ );
1633
+ for (const result of islandResults) {
1634
+ if (result) islandBundles.push(result);
1635
+ }
1636
+ }
1637
+
1547
1638
  // 6. 번들 매니페스트 생성
1548
1639
  const bundleManifest = createBundleManifest(
1549
1640
  outputs,
@@ -1551,7 +1642,8 @@ export async function buildClientBundles(
1551
1642
  runtimeResult.outputPath,
1552
1643
  vendorResult,
1553
1644
  routerResult.outputPath,
1554
- env
1645
+ env,
1646
+ islandBundles
1555
1647
  );
1556
1648
 
1557
1649
  await fs.writeFile(