@ramonyx/bundler 0.1.0 → 0.2.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Paket framework Ramonyx.
4
4
 
5
- > **Alpha (0.1.0)** — framework Node.js ini adalah baseline produksi;
5
+ > **Alpha (0.2.0)** — framework Node.js ini adalah baseline produksi;
6
6
  > runtime Rust/V8 bersifat eksperimental. Butuh **Node.js ≥ 24**.
7
7
 
8
8
  ## Instalasi
@@ -0,0 +1,39 @@
1
+ import type { AssetResolver } from "@ramonyx/core";
2
+ import { type AssetManifest, type AssetMap } from "./manifest.js";
3
+ export type { AssetEntry, AssetMap } from "./manifest.js";
4
+ /** Direktori aset default (relatif appDir) — dipindai otomatis bila ada. */
5
+ export declare const DEFAULT_ASSETS_DIR = "src/assets";
6
+ export interface CollectAssetsResult {
7
+ readonly assets: AssetMap;
8
+ readonly files: readonly string[];
9
+ }
10
+ /**
11
+ * Salin aset dari `assetsDir` ke `<clientOutDir>/assets/**` dengan nama ber-hash konten.
12
+ * Key manifest = path relatif posix terhadap `assetsDir` (mis. `img/logo.png`).
13
+ */
14
+ export declare function collectAssets(assetsDir: string, clientOutDir: string): Promise<CollectAssetsResult>;
15
+ export interface AssetUrlOptions {
16
+ /** Prefix URL publik; default `/assets`. */
17
+ readonly urlPrefix?: string;
18
+ }
19
+ /** URL publik entri aset; `undefined` bila path tidak ada di manifest. */
20
+ export declare function assetUrl(manifest: AssetManifest | null, path: string, options?: AssetUrlOptions): string | undefined;
21
+ export interface CreateAssetResolverOptions extends AssetUrlOptions {
22
+ /**
23
+ * Fallback ke `${prefix}/<path>` (tanpa hash) bila manifest belum ada/tidak memuat entri —
24
+ * untuk **dev** (Vite menyajikan `src/assets/**` apa adanya) — default false.
25
+ */
26
+ readonly fallbackToPath?: boolean;
27
+ }
28
+ /**
29
+ * Resolver aset untuk server (dipakai loader lewat `createPageRoutes({ assets })`):
30
+ * membaca manifest sekali; di dev pakai `fallbackToPath: true` (Vite menyajikan file mentah).
31
+ */
32
+ export declare function createAssetResolver(clientDir: string, options?: CreateAssetResolverOptions): Promise<AssetResolver>;
33
+ /** Direktori aset ada? (dipakai buildClient untuk memindai `src/assets` secara otomatis). */
34
+ export declare function assetsDirExists(assetsDir: string): Promise<boolean>;
35
+ /** Tag `<link rel="preload">` untuk font (masukkan lewat `metadata.head` halaman/dokumen). */
36
+ export declare function fontPreloadTag(href: string, options?: {
37
+ readonly type?: string;
38
+ readonly crossorigin?: boolean;
39
+ }): string;
package/dist/assets.js ADDED
@@ -0,0 +1,105 @@
1
+ // Pipeline aset (ADR-0069, M1 — tanpa dependency): pindai `src/assets/**`, salin ke
2
+ // `dist/client/assets/**` dengan **hash konten**, catat di `ramonyx.assets.json`, dan sediakan
3
+ // resolver untuk server (`assetUrl`/`createAssetResolver`).
4
+ //
5
+ // Catatan desain: Node SSR tidak dapat `import foto.png` (hanya Vite/klien yang bisa). Karena itu
6
+ // M1 memakai **manifest + helper** (bukan loader kustom) — paritas SSR ↔ klien tanpa mesin baru.
7
+ import { createHash } from "node:crypto";
8
+ import { copyFile, mkdir, readdir, readFile, stat } from "node:fs/promises";
9
+ import { dirname, extname, join, posix, relative, sep } from "node:path";
10
+ import { readAssetManifest, } from "./manifest.js";
11
+ /** Direktori aset default (relatif appDir) — dipindai otomatis bila ada. */
12
+ export const DEFAULT_ASSETS_DIR = "src/assets";
13
+ async function collectFiles(dir, found = []) {
14
+ const entries = await readdir(dir, { withFileTypes: true });
15
+ for (const entry of entries) {
16
+ if (entry.name.startsWith(".")) {
17
+ continue;
18
+ }
19
+ const path = join(dir, entry.name);
20
+ if (entry.isDirectory()) {
21
+ await collectFiles(path, found);
22
+ }
23
+ else if (entry.isFile()) {
24
+ found.push(path);
25
+ }
26
+ }
27
+ return found;
28
+ }
29
+ /**
30
+ * Salin aset dari `assetsDir` ke `<clientOutDir>/assets/**` dengan nama ber-hash konten.
31
+ * Key manifest = path relatif posix terhadap `assetsDir` (mis. `img/logo.png`).
32
+ */
33
+ export async function collectAssets(assetsDir, clientOutDir) {
34
+ const files = await collectFiles(assetsDir);
35
+ const assets = {};
36
+ const copied = [];
37
+ for (const file of files) {
38
+ const data = await readFile(file);
39
+ const hash = createHash("sha256").update(data).digest("hex").slice(0, 10);
40
+ const rel = relative(assetsDir, file).split(sep).join(posix.sep);
41
+ const ext = extname(rel);
42
+ const tanpaExt = rel.slice(0, rel.length - ext.length);
43
+ const targetRel = posix.join("assets", `${tanpaExt}-${hash}${ext}`);
44
+ const target = join(clientOutDir, targetRel);
45
+ await mkdir(dirname(target), { recursive: true });
46
+ await copyFile(file, target);
47
+ assets[rel] = { file: targetRel, bytes: data.byteLength };
48
+ copied.push(targetRel);
49
+ }
50
+ return { assets, files: copied };
51
+ }
52
+ /** URL publik entri aset; `undefined` bila path tidak ada di manifest. */
53
+ export function assetUrl(manifest, path, options = {}) {
54
+ const entry = manifest?.assets?.[normalizeAssetPath(path)];
55
+ if (entry === undefined) {
56
+ return undefined;
57
+ }
58
+ const prefix = (options.urlPrefix ?? "/assets").replace(/\/$/, "");
59
+ const tanpaPrefix = entry.file.startsWith("assets/")
60
+ ? entry.file.slice("assets/".length)
61
+ : entry.file;
62
+ return `${prefix}/${tanpaPrefix}`;
63
+ }
64
+ function normalizeAssetPath(path) {
65
+ return path
66
+ .replace(/^\.?\//, "")
67
+ .split(sep)
68
+ .join(posix.sep);
69
+ }
70
+ /**
71
+ * Resolver aset untuk server (dipakai loader lewat `createPageRoutes({ assets })`):
72
+ * membaca manifest sekali; di dev pakai `fallbackToPath: true` (Vite menyajikan file mentah).
73
+ */
74
+ export async function createAssetResolver(clientDir, options = {}) {
75
+ const manifest = await readAssetManifest(clientDir);
76
+ const prefix = (options.urlPrefix ?? "/assets").replace(/\/$/, "");
77
+ return {
78
+ url(path) {
79
+ const found = assetUrl(manifest, path, { urlPrefix: prefix });
80
+ if (found !== undefined) {
81
+ return found;
82
+ }
83
+ if (options.fallbackToPath === true) {
84
+ return `${prefix}/${normalizeAssetPath(path)}`;
85
+ }
86
+ return undefined;
87
+ },
88
+ };
89
+ }
90
+ /** Direktori aset ada? (dipakai buildClient untuk memindai `src/assets` secara otomatis). */
91
+ export async function assetsDirExists(assetsDir) {
92
+ try {
93
+ return (await stat(assetsDir)).isDirectory();
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ }
99
+ /** Tag `<link rel="preload">` untuk font (masukkan lewat `metadata.head` halaman/dokumen). */
100
+ export function fontPreloadTag(href, options = {}) {
101
+ const type = options.type ?? "font/woff2";
102
+ const crossorigin = (options.crossorigin ?? true) ? " crossorigin" : "";
103
+ return `<link rel="preload" as="font" type="${type}" href="${href}"${crossorigin} />`;
104
+ }
105
+ //# sourceMappingURL=assets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assets.js","sourceRoot":"","sources":["../../../packages/bundler/src/assets.ts"],"names":[],"mappings":"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","sourcesContent":["// Pipeline aset (ADR-0069, M1 — tanpa dependency): pindai `src/assets/**`, salin ke\n// `dist/client/assets/**` dengan **hash konten**, catat di `ramonyx.assets.json`, dan sediakan\n// resolver untuk server (`assetUrl`/`createAssetResolver`).\n//\n// Catatan desain: Node SSR tidak dapat `import foto.png` (hanya Vite/klien yang bisa). Karena itu\n// M1 memakai **manifest + helper** (bukan loader kustom) — paritas SSR ↔ klien tanpa mesin baru.\n\nimport { createHash } from \"node:crypto\";\nimport { copyFile, mkdir, readdir, readFile, stat } from \"node:fs/promises\";\nimport { dirname, extname, join, posix, relative, sep } from \"node:path\";\nimport type { AssetResolver } from \"@ramonyx/core\";\nimport {\n type AssetEntry,\n type AssetManifest,\n type AssetMap,\n readAssetManifest,\n} from \"./manifest.ts\";\n\nexport type { AssetEntry, AssetMap } from \"./manifest.ts\";\n\n/** Direktori aset default (relatif appDir) — dipindai otomatis bila ada. */\nexport const DEFAULT_ASSETS_DIR = \"src/assets\";\n\nasync function collectFiles(dir: string, found: string[] = []): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.name.startsWith(\".\")) {\n continue;\n }\n const path = join(dir, entry.name);\n if (entry.isDirectory()) {\n await collectFiles(path, found);\n } else if (entry.isFile()) {\n found.push(path);\n }\n }\n return found;\n}\n\nexport interface CollectAssetsResult {\n readonly assets: AssetMap;\n readonly files: readonly string[];\n}\n\n/**\n * Salin aset dari `assetsDir` ke `<clientOutDir>/assets/**` dengan nama ber-hash konten.\n * Key manifest = path relatif posix terhadap `assetsDir` (mis. `img/logo.png`).\n */\nexport async function collectAssets(\n assetsDir: string,\n clientOutDir: string,\n): Promise<CollectAssetsResult> {\n const files = await collectFiles(assetsDir);\n const assets: Record<string, AssetEntry> = {};\n const copied: string[] = [];\n\n for (const file of files) {\n const data = await readFile(file);\n const hash = createHash(\"sha256\").update(data).digest(\"hex\").slice(0, 10);\n const rel = relative(assetsDir, file).split(sep).join(posix.sep);\n const ext = extname(rel);\n const tanpaExt = rel.slice(0, rel.length - ext.length);\n const targetRel = posix.join(\"assets\", `${tanpaExt}-${hash}${ext}`);\n const target = join(clientOutDir, targetRel);\n\n await mkdir(dirname(target), { recursive: true });\n await copyFile(file, target);\n\n assets[rel] = { file: targetRel, bytes: data.byteLength };\n copied.push(targetRel);\n }\n\n return { assets, files: copied };\n}\n\nexport interface AssetUrlOptions {\n /** Prefix URL publik; default `/assets`. */\n readonly urlPrefix?: string;\n}\n\n/** URL publik entri aset; `undefined` bila path tidak ada di manifest. */\nexport function assetUrl(\n manifest: AssetManifest | null,\n path: string,\n options: AssetUrlOptions = {},\n): string | undefined {\n const entry = manifest?.assets?.[normalizeAssetPath(path)];\n if (entry === undefined) {\n return undefined;\n }\n const prefix = (options.urlPrefix ?? \"/assets\").replace(/\\/$/, \"\");\n const tanpaPrefix = entry.file.startsWith(\"assets/\")\n ? entry.file.slice(\"assets/\".length)\n : entry.file;\n return `${prefix}/${tanpaPrefix}`;\n}\n\nfunction normalizeAssetPath(path: string): string {\n return path\n .replace(/^\\.?\\//, \"\")\n .split(sep)\n .join(posix.sep);\n}\n\nexport interface CreateAssetResolverOptions extends AssetUrlOptions {\n /**\n * Fallback ke `${prefix}/<path>` (tanpa hash) bila manifest belum ada/tidak memuat entri —\n * untuk **dev** (Vite menyajikan `src/assets/**` apa adanya) — default false.\n */\n readonly fallbackToPath?: boolean;\n}\n\n/**\n * Resolver aset untuk server (dipakai loader lewat `createPageRoutes({ assets })`):\n * membaca manifest sekali; di dev pakai `fallbackToPath: true` (Vite menyajikan file mentah).\n */\nexport async function createAssetResolver(\n clientDir: string,\n options: CreateAssetResolverOptions = {},\n): Promise<AssetResolver> {\n const manifest = await readAssetManifest(clientDir);\n const prefix = (options.urlPrefix ?? \"/assets\").replace(/\\/$/, \"\");\n\n return {\n url(path: string): string | undefined {\n const found = assetUrl(manifest, path, { urlPrefix: prefix });\n if (found !== undefined) {\n return found;\n }\n if (options.fallbackToPath === true) {\n return `${prefix}/${normalizeAssetPath(path)}`;\n }\n return undefined;\n },\n };\n}\n\n/** Direktori aset ada? (dipakai buildClient untuk memindai `src/assets` secara otomatis). */\nexport async function assetsDirExists(assetsDir: string): Promise<boolean> {\n try {\n return (await stat(assetsDir)).isDirectory();\n } catch {\n return false;\n }\n}\n\n/** Tag `<link rel=\"preload\">` untuk font (masukkan lewat `metadata.head` halaman/dokumen). */\nexport function fontPreloadTag(\n href: string,\n options: { readonly type?: string; readonly crossorigin?: boolean } = {},\n): string {\n const type = options.type ?? \"font/woff2\";\n const crossorigin = (options.crossorigin ?? true) ? \" crossorigin\" : \"\";\n return `<link rel=\"preload\" as=\"font\" type=\"${type}\" href=\"${href}\"${crossorigin} />`;\n}\n"]}
package/dist/build.d.ts CHANGED
@@ -1,4 +1,7 @@
1
- import type { LoadedPage } from "@ramonyx/core";
1
+ import { type LoadedPage } from "@ramonyx/core";
2
+ import { type PluginOption } from "vite";
3
+ export type { AssetEntry, AssetManifest, AssetMap } from "./manifest.js";
4
+ export { CLIENT_MANIFEST_FILE, readAssetManifest } from "./manifest.js";
2
5
  export interface BuildClientOptions {
3
6
  readonly appDir: string;
4
7
  readonly pages: readonly LoadedPage[];
@@ -6,17 +9,31 @@ export interface BuildClientOptions {
6
9
  readonly outDir?: string;
7
10
  /** Hasilkan source map (`.js.map` + `sourcesContent`); default true. */
8
11
  readonly sourcemap?: boolean;
9
- /** URL publik bundle untuk `<script type="module">`; default "/assets/client.js". */
10
- readonly scriptPath?: string;
12
+ /** Prefix URL publik aset (untuk `scriptPath` hasil); default "/assets". */
13
+ readonly assetsUrlPrefix?: string;
11
14
  readonly minify?: boolean;
15
+ /** Plugin Vite tambahan (dari plugin Ramonyx — ADR-0067). */
16
+ readonly plugins?: readonly PluginOption[];
17
+ /**
18
+ * Direktori aset (relatif appDir) yang dipindai & disalin ber-hash ke `assets/**` +
19
+ * dicatat di manifest (ADR-0069). Default `src/assets` bila ada; `false` mematikan.
20
+ */
21
+ readonly assetsDir?: string | false;
12
22
  }
13
23
  export interface BuildClientResult {
14
24
  readonly outDir: string;
25
+ /** URL bundle klien ber-fingerprint, mis. `/assets/client-D3fK2p.js`. */
15
26
  readonly scriptPath: string;
16
27
  readonly entryFile: string;
28
+ /** Nama berkas (relatif terhadap outDir) yang dihasilkan build. */
29
+ readonly files: readonly string[];
17
30
  }
31
+ export declare function buildClient(options: BuildClientOptions): Promise<BuildClientResult>;
18
32
  /**
19
- * Membundel client (React + registry halaman) memakai Vite (ADR-0005).
20
- * Output ditulis ke `<appDir>/dist/client` dan siap disajikan sebagai aset statis.
33
+ * URL bundle klien dari manifest hasil build — dipakai server (`ramonyx start`, aplikasi)
34
+ * agar tetap benar saat nama berkas ber-hash. Fallback `/assets/client.js` bila manifest
35
+ * belum ada (mis. server dijalankan tanpa build — CLI sudah memperingatkan).
21
36
  */
22
- export declare function buildClient(options: BuildClientOptions): Promise<BuildClientResult>;
37
+ export declare function readClientScript(clientDir: string, options?: {
38
+ urlPrefix?: string;
39
+ }): Promise<string>;
package/dist/build.js CHANGED
@@ -1,15 +1,77 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
3
  import { join } from "node:path";
3
4
  import { generateClientEntry } from "@ramonyx/compiler";
5
+ import { errorHint } from "@ramonyx/core";
4
6
  import { build } from "vite";
7
+ import { collectAssets, DEFAULT_ASSETS_DIR } from "./assets.js";
8
+ import { CLIENT_MANIFEST_FILE, readAssetManifest, writeAssetManifest, } from "./manifest.js";
9
+ import { serverOnlyPlugin } from "./server-only.js";
10
+ export { CLIENT_MANIFEST_FILE, readAssetManifest } from "./manifest.js";
11
+ const SERVER_ONLY_IMPORT = /(?:from|import\s*\(|require\s*\()\s*["'](node:[^"']+)["']/g;
12
+ const BROWSER_EXTERNAL_MARKER = "__vite-browser-external";
5
13
  /**
6
14
  * Membundel client (React + registry halaman) memakai Vite (ADR-0005).
7
- * Output ditulis ke `<appDir>/dist/client` dan siap disajikan sebagai aset statis.
15
+ *
16
+ * Nama berkas memuat **hash konten** (`client-<hash>.js`, ADR-0055) sehingga dapat disajikan
17
+ * dengan `cache-control: immutable`; peta nama ditulis ke `ramonyx.assets.json` dan dibaca
18
+ * server lewat `readClientScript`.
8
19
  */
20
+ /** Pre-check (ADR-0063): modul yang masuk bundle klien (page/layout) dilarang mengimpor `node:*`. */
21
+ async function assertClientFilesBrowserSafe(pages) {
22
+ const files = new Set();
23
+ for (const page of pages) {
24
+ if (page.pageFile !== undefined) {
25
+ files.add(page.pageFile);
26
+ }
27
+ for (const layout of page.layoutFiles ?? []) {
28
+ files.add(layout);
29
+ }
30
+ }
31
+ const violations = [];
32
+ for (const file of files) {
33
+ let source;
34
+ try {
35
+ source = await readFile(file, "utf8");
36
+ }
37
+ catch {
38
+ continue;
39
+ }
40
+ for (const match of source.matchAll(SERVER_ONLY_IMPORT)) {
41
+ violations.push(`${file} → ${match[1] ?? "?"}`);
42
+ }
43
+ }
44
+ if (violations.length > 0) {
45
+ throw new Error("Batas server/client dilanggar (ADR-0063): modul yang masuk bundle klien mengimpor API server:\n" +
46
+ violations.map((entry) => ` - ${entry}`).join("\n") +
47
+ "\nPindahkan kode server ke modul `*.server.ts` yang hanya diimpor dari loader/action/route API, " +
48
+ `atau akses lewat API route. ${errorHint("RMX-BUILD-1")}`);
49
+ }
50
+ }
51
+ /** Post-build (ADR-0063): output tidak boleh memuat shim browser-external Vite atau `node:*` (impor transitif). */
52
+ async function assertBundleBrowserSafe(outDir) {
53
+ const files = (await readdir(outDir, { recursive: true })).filter((file) => file.endsWith(".js"));
54
+ const violations = new Set();
55
+ for (const file of files) {
56
+ const source = await readFile(join(outDir, file), "utf8");
57
+ if (source.includes(BROWSER_EXTERNAL_MARKER)) {
58
+ violations.add(file);
59
+ }
60
+ for (const match of source.matchAll(SERVER_ONLY_IMPORT)) {
61
+ violations.add(`${file} → ${match[1] ?? "?"}`);
62
+ }
63
+ }
64
+ if (violations.size > 0) {
65
+ throw new Error("Bundel klien memuat API server (ADR-0063) — kemungkinan impor transitif dari modul klien:\n" +
66
+ [...violations].map((entry) => ` - ${entry}`).join("\n") +
67
+ `\nPisahkan modul server (\`*.server.ts\`) dan impor hanya dari loader/action/route API. ${errorHint("RMX-BUILD-2")}`);
68
+ }
69
+ }
9
70
  export async function buildClient(options) {
10
71
  const outDir = join(options.appDir, options.outDir ?? "dist/client");
11
72
  const entryDir = join(options.appDir, ".ramonyx");
12
73
  const entryFile = join(entryDir, "client-entry.ts");
74
+ await assertClientFilesBrowserSafe(options.pages);
13
75
  await mkdir(entryDir, { recursive: true });
14
76
  await writeFile(entryFile, generateClientEntry(options.pages, { from: entryDir }));
15
77
  await build({
@@ -17,6 +79,7 @@ export async function buildClient(options) {
17
79
  root: options.appDir,
18
80
  logLevel: "warn",
19
81
  esbuild: { jsx: "automatic" },
82
+ plugins: [...(options.plugins ?? []), serverOnlyPlugin()],
20
83
  build: {
21
84
  outDir,
22
85
  emptyOutDir: true,
@@ -29,17 +92,45 @@ export async function buildClient(options) {
29
92
  rollupOptions: {
30
93
  input: entryFile,
31
94
  output: {
32
- entryFileNames: "client.js",
95
+ entryFileNames: "client-[hash].js",
33
96
  chunkFileNames: "chunks/[name]-[hash].js",
34
- assetFileNames: "[name][extname]",
97
+ assetFileNames: "[name]-[hash][extname]",
35
98
  },
36
99
  },
37
100
  },
38
101
  });
39
- return {
40
- outDir,
41
- scriptPath: options.scriptPath ?? "/assets/client.js",
42
- entryFile,
102
+ await assertBundleBrowserSafe(outDir);
103
+ const files = (await readdir(outDir, { recursive: true })).filter((file) => !file.endsWith(CLIENT_MANIFEST_FILE));
104
+ const clientFile = files.find((file) => /^client-.*\.js$/.test(file));
105
+ if (clientFile === undefined) {
106
+ throw new Error(`build klien tidak menghasilkan client-<hash>.js di ${outDir}`);
107
+ }
108
+ // Pipeline aset (ADR-0069): pindai `src/assets/**` → salin ber-hash + catat di manifest.
109
+ const assetsDir = options.assetsDir === false
110
+ ? null
111
+ : join(options.appDir, options.assetsDir ?? DEFAULT_ASSETS_DIR);
112
+ let assets = {};
113
+ if (assetsDir !== null && existsSync(assetsDir)) {
114
+ const collected = await collectAssets(assetsDir, outDir);
115
+ assets = collected.assets;
116
+ }
117
+ const manifest = {
118
+ client: clientFile,
119
+ files: [...files].sort(),
120
+ ...(Object.keys(assets).length > 0 ? { assets } : {}),
43
121
  };
122
+ await writeAssetManifest(outDir, manifest);
123
+ const prefix = (options.assetsUrlPrefix ?? "/assets").replace(/\/$/, "");
124
+ return { outDir, scriptPath: `${prefix}/${clientFile}`, entryFile, files: manifest.files };
125
+ }
126
+ /**
127
+ * URL bundle klien dari manifest hasil build — dipakai server (`ramonyx start`, aplikasi)
128
+ * agar tetap benar saat nama berkas ber-hash. Fallback `/assets/client.js` bila manifest
129
+ * belum ada (mis. server dijalankan tanpa build — CLI sudah memperingatkan).
130
+ */
131
+ export async function readClientScript(clientDir, options = {}) {
132
+ const prefix = (options.urlPrefix ?? "/assets").replace(/\/$/, "");
133
+ const manifest = await readAssetManifest(clientDir);
134
+ return manifest === null ? `${prefix}/client.js` : `${prefix}/${manifest.client}`;
44
135
  }
45
136
  //# sourceMappingURL=build.js.map
package/dist/build.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"build.js","sourceRoot":"","sources":["../../../packages/bundler/src/build.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAExD,OAAO,EAAE,KAAK,EAAE,MAAM,MAAM,CAAC;AAoB7B;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAA2B;IAC3D,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,aAAa,CAAC,CAAC;IACrE,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAClD,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,EAAE,iBAAiB,CAAC,CAAC;IAEpD,MAAM,KAAK,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,MAAM,SAAS,CAAC,SAAS,EAAE,mBAAmB,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;IAEnF,MAAM,KAAK,CAAC;QACV,UAAU,EAAE,KAAK;QACjB,IAAI,EAAE,OAAO,CAAC,MAAM;QACpB,QAAQ,EAAE,MAAM;QAChB,OAAO,EAAE,EAAE,GAAG,EAAE,WAAW,EAAE;QAC7B,KAAK,EAAE;YACL,MAAM;YACN,WAAW,EAAE,IAAI;YACjB,MAAM,EAAE,QAAQ;YAChB,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,KAAK;YAC/B,gFAAgF;YAChF,2DAA2D;YAC3D,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,IAAI;YACpC,aAAa,EAAE,KAAK;YACpB,aAAa,EAAE;gBACb,KAAK,EAAE,SAAS;gBAChB,MAAM,EAAE;oBACN,cAAc,EAAE,WAAW;oBAC3B,cAAc,EAAE,yBAAyB;oBACzC,cAAc,EAAE,iBAAiB;iBAClC;aACF;SACF;KACF,CAAC,CAAC;IAEH,OAAO;QACL,MAAM;QACN,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,mBAAmB;QACrD,SAAS;KACV,CAAC;AACJ,CAAC","sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { generateClientEntry } from \"@ramonyx/compiler\";\nimport type { LoadedPage } from \"@ramonyx/core\";\nimport { build } from \"vite\";\n\nexport interface BuildClientOptions {\n readonly appDir: string;\n readonly pages: readonly LoadedPage[];\n /** Relatif terhadap appDir; default \"dist/client\". */\n readonly outDir?: string;\n /** Hasilkan source map (`.js.map` + `sourcesContent`); default true. */\n readonly sourcemap?: boolean;\n /** URL publik bundle untuk `<script type=\"module\">`; default \"/assets/client.js\". */\n readonly scriptPath?: string;\n readonly minify?: boolean;\n}\n\nexport interface BuildClientResult {\n readonly outDir: string;\n readonly scriptPath: string;\n readonly entryFile: string;\n}\n\n/**\n * Membundel client (React + registry halaman) memakai Vite (ADR-0005).\n * Output ditulis ke `<appDir>/dist/client` dan siap disajikan sebagai aset statis.\n */\nexport async function buildClient(options: BuildClientOptions): Promise<BuildClientResult> {\n const outDir = join(options.appDir, options.outDir ?? \"dist/client\");\n const entryDir = join(options.appDir, \".ramonyx\");\n const entryFile = join(entryDir, \"client-entry.ts\");\n\n await mkdir(entryDir, { recursive: true });\n await writeFile(entryFile, generateClientEntry(options.pages, { from: entryDir }));\n\n await build({\n configFile: false,\n root: options.appDir,\n logLevel: \"warn\",\n esbuild: { jsx: \"automatic\" },\n build: {\n outDir,\n emptyOutDir: true,\n target: \"es2022\",\n minify: options.minify ?? false,\n // Source map produksi (default aktif): `sourcesContent` di-embed agar map tetap\n // berguna walau sumber TS tidak ikut disajikan (ADR-0046).\n sourcemap: options.sourcemap ?? true,\n modulePreload: false,\n rollupOptions: {\n input: entryFile,\n output: {\n entryFileNames: \"client.js\",\n chunkFileNames: \"chunks/[name]-[hash].js\",\n assetFileNames: \"[name][extname]\",\n },\n },\n },\n });\n\n return {\n outDir,\n scriptPath: options.scriptPath ?? \"/assets/client.js\",\n entryFile,\n };\n}\n"]}
1
+ {"version":3,"file":"build.js","sourceRoot":"","sources":["../../../packages/bundler/src/build.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { mkdir, readdir, readFile, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { generateClientEntry } from \"@ramonyx/compiler\";\nimport { errorHint, type LoadedPage } from \"@ramonyx/core\";\nimport { build, type PluginOption } from \"vite\";\nimport { collectAssets, DEFAULT_ASSETS_DIR } from \"./assets.ts\";\nimport {\n type AssetManifest,\n CLIENT_MANIFEST_FILE,\n readAssetManifest,\n writeAssetManifest,\n} from \"./manifest.ts\";\nimport { serverOnlyPlugin } from \"./server-only.ts\";\n\nexport type { AssetEntry, AssetManifest, AssetMap } from \"./manifest.ts\";\nexport { CLIENT_MANIFEST_FILE, readAssetManifest } from \"./manifest.ts\";\n\nconst SERVER_ONLY_IMPORT = /(?:from|import\\s*\\(|require\\s*\\()\\s*[\"'](node:[^\"']+)[\"']/g;\nconst BROWSER_EXTERNAL_MARKER = \"__vite-browser-external\";\n\nexport interface BuildClientOptions {\n readonly appDir: string;\n readonly pages: readonly LoadedPage[];\n /** Relatif terhadap appDir; default \"dist/client\". */\n readonly outDir?: string;\n /** Hasilkan source map (`.js.map` + `sourcesContent`); default true. */\n readonly sourcemap?: boolean;\n /** Prefix URL publik aset (untuk `scriptPath` hasil); default \"/assets\". */\n readonly assetsUrlPrefix?: string;\n readonly minify?: boolean;\n /** Plugin Vite tambahan (dari plugin Ramonyx — ADR-0067). */\n readonly plugins?: readonly PluginOption[];\n /**\n * Direktori aset (relatif appDir) yang dipindai & disalin ber-hash ke `assets/**` +\n * dicatat di manifest (ADR-0069). Default `src/assets` bila ada; `false` mematikan.\n */\n readonly assetsDir?: string | false;\n}\n\nexport interface BuildClientResult {\n readonly outDir: string;\n /** URL bundle klien ber-fingerprint, mis. `/assets/client-D3fK2p.js`. */\n readonly scriptPath: string;\n readonly entryFile: string;\n /** Nama berkas (relatif terhadap outDir) yang dihasilkan build. */\n readonly files: readonly string[];\n}\n\n/**\n * Membundel client (React + registry halaman) memakai Vite (ADR-0005).\n *\n * Nama berkas memuat **hash konten** (`client-<hash>.js`, ADR-0055) sehingga dapat disajikan\n * dengan `cache-control: immutable`; peta nama ditulis ke `ramonyx.assets.json` dan dibaca\n * server lewat `readClientScript`.\n */\n/** Pre-check (ADR-0063): modul yang masuk bundle klien (page/layout) dilarang mengimpor `node:*`. */\nasync function assertClientFilesBrowserSafe(pages: readonly LoadedPage[]): Promise<void> {\n const files = new Set<string>();\n for (const page of pages) {\n if (page.pageFile !== undefined) {\n files.add(page.pageFile);\n }\n for (const layout of page.layoutFiles ?? []) {\n files.add(layout);\n }\n }\n\n const violations: string[] = [];\n for (const file of files) {\n let source: string;\n try {\n source = await readFile(file, \"utf8\");\n } catch {\n continue;\n }\n for (const match of source.matchAll(SERVER_ONLY_IMPORT)) {\n violations.push(`${file} → ${match[1] ?? \"?\"}`);\n }\n }\n\n if (violations.length > 0) {\n throw new Error(\n \"Batas server/client dilanggar (ADR-0063): modul yang masuk bundle klien mengimpor API server:\\n\" +\n violations.map((entry) => ` - ${entry}`).join(\"\\n\") +\n \"\\nPindahkan kode server ke modul `*.server.ts` yang hanya diimpor dari loader/action/route API, \" +\n `atau akses lewat API route. ${errorHint(\"RMX-BUILD-1\")}`,\n );\n }\n}\n\n/** Post-build (ADR-0063): output tidak boleh memuat shim browser-external Vite atau `node:*` (impor transitif). */\nasync function assertBundleBrowserSafe(outDir: string): Promise<void> {\n const files = (await readdir(outDir, { recursive: true })).filter((file) => file.endsWith(\".js\"));\n const violations = new Set<string>();\n for (const file of files) {\n const source = await readFile(join(outDir, file), \"utf8\");\n if (source.includes(BROWSER_EXTERNAL_MARKER)) {\n violations.add(file);\n }\n for (const match of source.matchAll(SERVER_ONLY_IMPORT)) {\n violations.add(`${file} → ${match[1] ?? \"?\"}`);\n }\n }\n\n if (violations.size > 0) {\n throw new Error(\n \"Bundel klien memuat API server (ADR-0063) — kemungkinan impor transitif dari modul klien:\\n\" +\n [...violations].map((entry) => ` - ${entry}`).join(\"\\n\") +\n `\\nPisahkan modul server (\\`*.server.ts\\`) dan impor hanya dari loader/action/route API. ${errorHint(\"RMX-BUILD-2\")}`,\n );\n }\n}\n\nexport async function buildClient(options: BuildClientOptions): Promise<BuildClientResult> {\n const outDir = join(options.appDir, options.outDir ?? \"dist/client\");\n const entryDir = join(options.appDir, \".ramonyx\");\n const entryFile = join(entryDir, \"client-entry.ts\");\n\n await assertClientFilesBrowserSafe(options.pages);\n\n await mkdir(entryDir, { recursive: true });\n await writeFile(entryFile, generateClientEntry(options.pages, { from: entryDir }));\n\n await build({\n configFile: false,\n root: options.appDir,\n logLevel: \"warn\",\n esbuild: { jsx: \"automatic\" },\n plugins: [...(options.plugins ?? []), serverOnlyPlugin()],\n build: {\n outDir,\n emptyOutDir: true,\n target: \"es2022\",\n minify: options.minify ?? false,\n // Source map produksi (default aktif): `sourcesContent` di-embed agar map tetap\n // berguna walau sumber TS tidak ikut disajikan (ADR-0046).\n sourcemap: options.sourcemap ?? true,\n modulePreload: false,\n rollupOptions: {\n input: entryFile,\n output: {\n entryFileNames: \"client-[hash].js\",\n chunkFileNames: \"chunks/[name]-[hash].js\",\n assetFileNames: \"[name]-[hash][extname]\",\n },\n },\n },\n });\n\n await assertBundleBrowserSafe(outDir);\n\n const files = (await readdir(outDir, { recursive: true })).filter(\n (file) => !file.endsWith(CLIENT_MANIFEST_FILE),\n );\n const clientFile = files.find((file) => /^client-.*\\.js$/.test(file));\n if (clientFile === undefined) {\n throw new Error(`build klien tidak menghasilkan client-<hash>.js di ${outDir}`);\n }\n\n // Pipeline aset (ADR-0069): pindai `src/assets/**` → salin ber-hash + catat di manifest.\n const assetsDir =\n options.assetsDir === false\n ? null\n : join(options.appDir, options.assetsDir ?? DEFAULT_ASSETS_DIR);\n let assets: Awaited<ReturnType<typeof collectAssets>>[\"assets\"] = {};\n if (assetsDir !== null && existsSync(assetsDir)) {\n const collected = await collectAssets(assetsDir, outDir);\n assets = collected.assets;\n }\n\n const manifest: AssetManifest = {\n client: clientFile,\n files: [...files].sort(),\n ...(Object.keys(assets).length > 0 ? { assets } : {}),\n };\n await writeAssetManifest(outDir, manifest);\n\n const prefix = (options.assetsUrlPrefix ?? \"/assets\").replace(/\\/$/, \"\");\n return { outDir, scriptPath: `${prefix}/${clientFile}`, entryFile, files: manifest.files };\n}\n\n/**\n * URL bundle klien dari manifest hasil build — dipakai server (`ramonyx start`, aplikasi)\n * agar tetap benar saat nama berkas ber-hash. Fallback `/assets/client.js` bila manifest\n * belum ada (mis. server dijalankan tanpa build — CLI sudah memperingatkan).\n */\nexport async function readClientScript(\n clientDir: string,\n options: { urlPrefix?: string } = {},\n): Promise<string> {\n const prefix = (options.urlPrefix ?? \"/assets\").replace(/\\/$/, \"\");\n const manifest = await readAssetManifest(clientDir);\n return manifest === null ? `${prefix}/client.js` : `${prefix}/${manifest.client}`;\n}\n"]}
package/dist/dev.d.ts CHANGED
@@ -1,15 +1,21 @@
1
1
  import { type ModuleImporter } from "@ramonyx/compiler";
2
+ import { type PluginOption } from "vite";
3
+ import { type DevtoolsCollector } from "./devtools.js";
2
4
  export interface DevServerOptions {
3
5
  readonly appDir: string;
4
6
  /** URL server SSR yang diproksikan (mis. "http://localhost:3000"). */
5
7
  readonly target: string;
6
8
  readonly port?: number;
9
+ /** Plugin Vite tambahan (dari plugin Ramonyx — ADR-0067). */
10
+ readonly plugins?: readonly PluginOption[];
7
11
  }
8
12
  export interface DevServer {
9
13
  readonly url: string;
10
14
  /** Importer untuk compiler: modul mengikuti module graph Vite (invalidasi saat file berubah). */
11
15
  readonly importer: ModuleImporter;
12
16
  readonly target: string;
17
+ /** Kolektor panel devtools (ADR-0077) — pasang `hooks`/`middleware`-nya ke handler aplikasi. */
18
+ readonly devtools: DevtoolsCollector;
13
19
  close(): Promise<void>;
14
20
  }
15
21
  /**
package/dist/dev.js CHANGED
@@ -1,10 +1,70 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
2
- import { join, relative } from "node:path";
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
3
+ import { extname, join, relative } from "node:path";
3
4
  import { Readable } from "node:stream";
4
5
  import { pipeline } from "node:stream/promises";
5
6
  import { generateClientEntry, loadPageRoutes, RouteManifestError, } from "@ramonyx/compiler";
6
7
  import { createServer } from "vite";
8
+ import { createDevtoolsCollector, devtoolsPanelHtml } from "./devtools.js";
9
+ import { readAssetManifest } from "./manifest.js";
10
+ import { serverOnlyPlugin } from "./server-only.js";
7
11
  const ENTRY_FILE = "client-entry.ts";
12
+ const DEV_ASSET_TYPES = {
13
+ ".svg": "image/svg+xml",
14
+ ".png": "image/png",
15
+ ".jpg": "image/jpeg",
16
+ ".jpeg": "image/jpeg",
17
+ ".gif": "image/gif",
18
+ ".webp": "image/webp",
19
+ ".avif": "image/avif",
20
+ ".ico": "image/x-icon",
21
+ ".woff": "font/woff",
22
+ ".woff2": "font/woff2",
23
+ ".ttf": "font/ttf",
24
+ ".otf": "font/otf",
25
+ };
26
+ /**
27
+ * Dev: sajikan `src/assets/**` apa adanya di `/assets/*` (tanpa hash) — paritas dengan
28
+ * resolver aset dev (`createAssetResolver(..., { fallbackToPath: true })`, ADR-0069).
29
+ */
30
+ function assetsDevPlugin(appDir) {
31
+ return {
32
+ name: "ramonyx-assets-dev",
33
+ configureServer(server) {
34
+ server.middlewares.use((req, res, next) => {
35
+ const url = req.url ?? "";
36
+ if (!url.startsWith("/assets/")) {
37
+ next();
38
+ return;
39
+ }
40
+ let rel;
41
+ try {
42
+ rel = decodeURIComponent(url.slice("/assets/".length).split("?")[0] ?? "");
43
+ }
44
+ catch {
45
+ next();
46
+ return;
47
+ }
48
+ if (rel.length === 0 || rel.includes("..")) {
49
+ next();
50
+ return;
51
+ }
52
+ const file = join(appDir, "src", "assets", rel);
53
+ if (!existsSync(file)) {
54
+ next();
55
+ return;
56
+ }
57
+ void readFile(file)
58
+ .then((data) => {
59
+ res.setHeader("content-type", DEV_ASSET_TYPES[extname(file).toLowerCase()] ?? "application/octet-stream");
60
+ res.setHeader("cache-control", "no-cache");
61
+ res.end(data);
62
+ })
63
+ .catch(() => next());
64
+ });
65
+ },
66
+ };
67
+ }
8
68
  /** Path yang dimiliki Vite (jangan diproksikan ke SSR). */
9
69
  function isViteRequest(url) {
10
70
  const path = url.split("?")[0] ?? "";
@@ -82,6 +142,33 @@ export async function createDevServer(options) {
82
142
  const entryDir = join(appDir, ".ramonyx");
83
143
  const entryFile = join(entryDir, ENTRY_FILE);
84
144
  const routesDir = join(appDir, "src", "routes");
145
+ const devtools = createDevtoolsCollector();
146
+ const clientDir = join(appDir, "dist", "client");
147
+ /** Ukuran bundel/aset dari manifest build (bila sudah pernah dibangun). */
148
+ const refreshAssets = async () => {
149
+ const manifest = await readAssetManifest(clientDir);
150
+ if (manifest === null) {
151
+ devtools.setAssets([]);
152
+ return;
153
+ }
154
+ const names = new Set([manifest.client, ...manifest.files]);
155
+ const entries = [];
156
+ for (const name of names) {
157
+ try {
158
+ const info = await stat(join(clientDir, name));
159
+ entries.push({ name, bytes: info.size });
160
+ }
161
+ catch {
162
+ // berkas hilang (mis. dibersihkan) — lewati
163
+ }
164
+ }
165
+ if (manifest.assets !== undefined) {
166
+ for (const entry of Object.values(manifest.assets)) {
167
+ entries.push({ name: entry.file, bytes: entry.bytes });
168
+ }
169
+ }
170
+ devtools.setAssets(entries);
171
+ };
85
172
  const vite = await createServer({
86
173
  configFile: false,
87
174
  root: appDir,
@@ -90,6 +177,34 @@ export async function createDevServer(options) {
90
177
  esbuild: { jsx: "automatic" },
91
178
  server: { port: options.port ?? 5173 },
92
179
  plugins: [
180
+ ...(options.plugins ?? []),
181
+ serverOnlyPlugin(),
182
+ assetsDevPlugin(appDir),
183
+ {
184
+ name: "ramonyx-devtools",
185
+ configureServer(server) {
186
+ server.middlewares.use((req, res, next) => {
187
+ const path = (req.url ?? "").split("?")[0] ?? "";
188
+ if (path === "/__ramonyx/devtools") {
189
+ res.setHeader("content-type", "text/html; charset=utf-8");
190
+ res.setHeader("cache-control", "no-store");
191
+ res.end(devtoolsPanelHtml());
192
+ return;
193
+ }
194
+ if (path === "/__ramonyx/data") {
195
+ void refreshAssets()
196
+ .catch(() => undefined)
197
+ .then(() => {
198
+ res.setHeader("content-type", "application/json; charset=utf-8");
199
+ res.setHeader("cache-control", "no-store");
200
+ res.end(JSON.stringify(devtools.snapshot()));
201
+ });
202
+ return;
203
+ }
204
+ next();
205
+ });
206
+ },
207
+ },
93
208
  {
94
209
  name: "ramonyx-ssr-proxy",
95
210
  configureServer(server) {
@@ -114,6 +229,10 @@ export async function createDevServer(options) {
114
229
  };
115
230
  const regenerateEntry = async () => {
116
231
  const pages = await loadPageRoutes({ dir: routesDir, importer });
232
+ devtools.setRoutes(pages.map((page) => ({
233
+ path: page.path,
234
+ ...(page.pageFile !== undefined ? { file: relative(appDir, page.pageFile) } : {}),
235
+ })));
117
236
  await mkdir(entryDir, { recursive: true });
118
237
  await writeFile(entryFile, generateClientEntry(pages, { from: entryDir }));
119
238
  };
@@ -135,6 +254,7 @@ export async function createDevServer(options) {
135
254
  url: `http://localhost:${port}`,
136
255
  importer,
137
256
  target: options.target,
257
+ devtools,
138
258
  close: () => vite.close(),
139
259
  };
140
260
  }