@ramonyx/bundler 0.1.1 → 0.3.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.1)** — framework Node.js ini adalah baseline produksi;
5
+ > **Alpha (0.3.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,76 @@
1
+ import { type AssetInfo, 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
+ /** Input prosesor: satu berkas `src/assets/**` (path relatif posix + isi). */
7
+ export interface AssetProcessorInput {
8
+ readonly path: string;
9
+ readonly data: Uint8Array;
10
+ }
11
+ /** Berkas turunan yang dihasilkan prosesor (mis. varian gambar ter-resize). */
12
+ export interface AssetProcessorOutput {
13
+ /** Path relatif terhadap `src/assets` (mis. `img/hero-640.webp`) — bundler menambah hash konten. */
14
+ readonly path: string;
15
+ readonly data: Uint8Array;
16
+ /** Lebar varian (dipakai `srcset`). */
17
+ readonly width: number;
18
+ readonly height?: number;
19
+ /** Format berkas; default dari ekstensi `path`. */
20
+ readonly format?: string;
21
+ }
22
+ /** Hasil prosesor: metadata entri asli + berkas turunan. */
23
+ export interface AssetProcessorResult {
24
+ readonly meta?: {
25
+ readonly width?: number;
26
+ readonly height?: number;
27
+ };
28
+ readonly outputs?: readonly AssetProcessorOutput[];
29
+ }
30
+ export type AssetProcessor = (asset: AssetProcessorInput) => AssetProcessorResult | undefined | Promise<AssetProcessorResult | undefined>;
31
+ export interface CollectAssetsOptions {
32
+ /** Prosesor plugin (dijalankan berurutan per berkas — ADR-0079). */
33
+ readonly processors?: readonly AssetProcessor[];
34
+ }
35
+ export interface CollectAssetsResult {
36
+ readonly assets: AssetMap;
37
+ readonly files: readonly string[];
38
+ }
39
+ /**
40
+ * Salin aset dari `assetsDir` ke `<clientOutDir>/**` dengan nama ber-hash konten.
41
+ * Key manifest = path relatif posix terhadap `assetsDir` (mis. `img/logo.png`);
42
+ * `file` = path relatif terhadap keluaran klien → URL publik = `<prefix>/<file>` (ADR-0079).
43
+ * Prosesor (bila ada) dapat menambah varian + metadata dimensi (ADR-0079).
44
+ */
45
+ export declare function collectAssets(assetsDir: string, clientOutDir: string, options?: CollectAssetsOptions): Promise<CollectAssetsResult>;
46
+ export interface AssetUrlOptions {
47
+ /** Prefix URL publik; default `/assets`. */
48
+ readonly urlPrefix?: string;
49
+ }
50
+ /** URL publik entri aset; `undefined` bila path tidak ada di manifest. */
51
+ export declare function assetUrl(manifest: AssetManifest | null, path: string, options?: AssetUrlOptions): string | undefined;
52
+ /**
53
+ * Manifest → peta `AssetInfo` (URL + dimensi + varian) untuk resolver: dipakai
54
+ * `createAssetResolver` (Node) dan artefak edge yang meng-inline peta (ADR-0069/0079).
55
+ */
56
+ export declare function assetInfoMap(manifest: AssetManifest | null, options?: AssetUrlOptions): Record<string, AssetInfo>;
57
+ export interface CreateAssetResolverOptions extends AssetUrlOptions {
58
+ /**
59
+ * Fallback ke `${prefix}/<path>` (tanpa hash) bila manifest belum ada/tidak memuat entri —
60
+ * untuk **dev** (Vite menyajikan `src/assets/**` apa adanya) — default false.
61
+ */
62
+ readonly fallbackToPath?: boolean;
63
+ }
64
+ /**
65
+ * Resolver aset untuk server (dipakai loader lewat `createPageRoutes({ assets })`):
66
+ * membaca manifest sekali; di dev pakai `fallbackToPath: true` (Vite menyajikan file mentah;
67
+ * tanpa varian — ADR-0079).
68
+ */
69
+ export declare function createAssetResolver(clientDir: string, options?: CreateAssetResolverOptions): Promise<AssetResolver>;
70
+ /** Direktori aset ada? (dipakai buildClient untuk memindai `src/assets` secara otomatis). */
71
+ export declare function assetsDirExists(assetsDir: string): Promise<boolean>;
72
+ /** Tag `<link rel="preload">` untuk font (masukkan lewat `metadata.head` halaman/dokumen). */
73
+ export declare function fontPreloadTag(href: string, options?: {
74
+ readonly type?: string;
75
+ readonly crossorigin?: boolean;
76
+ }): string;
package/dist/assets.js ADDED
@@ -0,0 +1,197 @@
1
+ // Pipeline aset (ADR-0069 M1, varian ADR-0079 M2 — tanpa dependency): pindai `src/assets/**`,
2
+ // salin ke **root keluaran klien** (`dist/client/**`) dengan **hash konten** (URL = `file`, ADR-0079),
3
+ // jalankan prosesor plugin (mis. varian gambar dari `@ramonyx/plugin-image`), catat semuanya di
4
+ // `ramonyx.assets.json`, dan sediakan resolver (`assetUrl`/`createAssetResolver`/`assetInfoMap`).
5
+ //
6
+ // Catatan desain: Node SSR tidak dapat `import foto.png` (hanya Vite/klien yang bisa). Karena itu
7
+ // M1 memakai **manifest + helper** (bukan loader kustom) — paritas SSR ↔ klien tanpa mesin baru.
8
+ import { createHash } from "node:crypto";
9
+ import { copyFile, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
10
+ import { dirname, extname, join, posix, relative, sep } from "node:path";
11
+ import { createAssetResolverFromMap, } from "@ramonyx/core";
12
+ import { readAssetManifest, } from "./manifest.js";
13
+ /** Direktori aset default (relatif appDir) — dipindai otomatis bila ada. */
14
+ export const DEFAULT_ASSETS_DIR = "src/assets";
15
+ async function collectFiles(dir, found = []) {
16
+ const entries = await readdir(dir, { withFileTypes: true });
17
+ for (const entry of entries) {
18
+ if (entry.name.startsWith(".")) {
19
+ continue;
20
+ }
21
+ const path = join(dir, entry.name);
22
+ if (entry.isDirectory()) {
23
+ await collectFiles(path, found);
24
+ }
25
+ else if (entry.isFile()) {
26
+ found.push(path);
27
+ }
28
+ }
29
+ return found;
30
+ }
31
+ function contentHash(data) {
32
+ return createHash("sha256").update(data).digest("hex").slice(0, 10);
33
+ }
34
+ /**
35
+ * `img/hero.png` + hash → `img/hero-<hash>.png` (relatif direktori keluaran klien).
36
+ *
37
+ * Catatan (koreksi M1, ADR-0079): berkas ditulis **di root keluaran klien** — konsisten dengan
38
+ * URL `<prefix>/<path>` dan middleware `serveStatic({ dir: dist/client, prefix: "/assets" })`.
39
+ * Layout M1 (`dist/client/assets/**`) tidak pernah dapat disajikan (URL → root), tertangkap
40
+ * oleh test HTTP aset di create-app.
41
+ */
42
+ function hashedTarget(rel, hash) {
43
+ const ext = extname(rel);
44
+ const tanpaExt = rel.slice(0, rel.length - ext.length);
45
+ return `${tanpaExt}-${hash}${ext}`;
46
+ }
47
+ /**
48
+ * Salin aset dari `assetsDir` ke `<clientOutDir>/**` dengan nama ber-hash konten.
49
+ * Key manifest = path relatif posix terhadap `assetsDir` (mis. `img/logo.png`);
50
+ * `file` = path relatif terhadap keluaran klien → URL publik = `<prefix>/<file>` (ADR-0079).
51
+ * Prosesor (bila ada) dapat menambah varian + metadata dimensi (ADR-0079).
52
+ */
53
+ export async function collectAssets(assetsDir, clientOutDir, options = {}) {
54
+ const files = await collectFiles(assetsDir);
55
+ const assets = {};
56
+ const copied = new Set();
57
+ for (const file of files) {
58
+ const data = await readFile(file);
59
+ const rel = relative(assetsDir, file).split(sep).join(posix.sep);
60
+ let meta = {};
61
+ const outputs = [];
62
+ for (const processor of options.processors ?? []) {
63
+ const hasil = await processor({ path: rel, data });
64
+ if (hasil === undefined) {
65
+ continue;
66
+ }
67
+ if (hasil.meta !== undefined) {
68
+ meta = { ...meta, ...hasil.meta };
69
+ }
70
+ if (hasil.outputs !== undefined) {
71
+ outputs.push(...hasil.outputs);
72
+ }
73
+ }
74
+ const targetRel = hashedTarget(rel, contentHash(data));
75
+ const target = join(clientOutDir, targetRel);
76
+ await mkdir(dirname(target), { recursive: true });
77
+ await copyFile(file, target);
78
+ copied.add(targetRel);
79
+ const variants = [];
80
+ for (const output of outputs) {
81
+ const outputRel = hashedTarget(output.path, contentHash(output.data));
82
+ const outputTarget = join(clientOutDir, outputRel);
83
+ await mkdir(dirname(outputTarget), { recursive: true });
84
+ await writeFile(outputTarget, output.data);
85
+ copied.add(outputRel);
86
+ variants.push({
87
+ file: outputRel,
88
+ bytes: output.data.byteLength,
89
+ width: output.width,
90
+ ...(output.height !== undefined ? { height: output.height } : {}),
91
+ format: (output.format ?? extname(output.path).slice(1)).toLowerCase(),
92
+ });
93
+ }
94
+ assets[rel] = {
95
+ file: targetRel,
96
+ bytes: data.byteLength,
97
+ ...(meta.width !== undefined ? { width: meta.width } : {}),
98
+ ...(meta.height !== undefined ? { height: meta.height } : {}),
99
+ ...(variants.length > 0 ? { variants } : {}),
100
+ };
101
+ }
102
+ return { assets, files: [...copied] };
103
+ }
104
+ function normalizeAssetPath(path) {
105
+ return path
106
+ .replace(/^\.?\//, "")
107
+ .split(sep)
108
+ .join(posix.sep);
109
+ }
110
+ /** Berkas keluaran (relatif clientOutDir) → URL publik: `<prefix>/<file>` (1:1 — ADR-0079). */
111
+ function fileToUrl(file, prefix) {
112
+ return `${prefix}/${file}`;
113
+ }
114
+ /** URL publik entri aset; `undefined` bila path tidak ada di manifest. */
115
+ export function assetUrl(manifest, path, options = {}) {
116
+ const entry = manifest?.assets?.[normalizeAssetPath(path)];
117
+ if (entry === undefined) {
118
+ return undefined;
119
+ }
120
+ return fileToUrl(entry.file, (options.urlPrefix ?? "/assets").replace(/\/$/, ""));
121
+ }
122
+ /**
123
+ * Manifest → peta `AssetInfo` (URL + dimensi + varian) untuk resolver: dipakai
124
+ * `createAssetResolver` (Node) dan artefak edge yang meng-inline peta (ADR-0069/0079).
125
+ */
126
+ export function assetInfoMap(manifest, options = {}) {
127
+ const prefix = (options.urlPrefix ?? "/assets").replace(/\/$/, "");
128
+ const map = {};
129
+ for (const [path, entry] of Object.entries(manifest?.assets ?? {})) {
130
+ map[path] = {
131
+ url: fileToUrl(entry.file, prefix),
132
+ ...(entry.width !== undefined ? { width: entry.width } : {}),
133
+ ...(entry.height !== undefined ? { height: entry.height } : {}),
134
+ ...(entry.variants !== undefined
135
+ ? {
136
+ variants: entry.variants.map((variant) => ({
137
+ url: fileToUrl(variant.file, prefix),
138
+ width: variant.width,
139
+ ...(variant.height !== undefined ? { height: variant.height } : {}),
140
+ format: variant.format,
141
+ bytes: variant.bytes,
142
+ })),
143
+ }
144
+ : {}),
145
+ };
146
+ }
147
+ return map;
148
+ }
149
+ /**
150
+ * Resolver aset untuk server (dipakai loader lewat `createPageRoutes({ assets })`):
151
+ * membaca manifest sekali; di dev pakai `fallbackToPath: true` (Vite menyajikan file mentah;
152
+ * tanpa varian — ADR-0079).
153
+ */
154
+ export async function createAssetResolver(clientDir, options = {}) {
155
+ const manifest = await readAssetManifest(clientDir);
156
+ const prefix = (options.urlPrefix ?? "/assets").replace(/\/$/, "");
157
+ const base = createAssetResolverFromMap(assetInfoMap(manifest, { urlPrefix: prefix }));
158
+ return {
159
+ url(path) {
160
+ const found = base.url(path);
161
+ if (found !== undefined) {
162
+ return found;
163
+ }
164
+ if (options.fallbackToPath === true) {
165
+ return `${prefix}/${normalizeAssetPath(path)}`;
166
+ }
167
+ return undefined;
168
+ },
169
+ image(path) {
170
+ const found = base.image(path);
171
+ if (found !== undefined) {
172
+ return found;
173
+ }
174
+ if (options.fallbackToPath === true) {
175
+ // Dev: berkas disajikan apa adanya (tanpa hash/varian/dimensi).
176
+ return { src: `${prefix}/${normalizeAssetPath(path)}` };
177
+ }
178
+ return undefined;
179
+ },
180
+ };
181
+ }
182
+ /** Direktori aset ada? (dipakai buildClient untuk memindai `src/assets` secara otomatis). */
183
+ export async function assetsDirExists(assetsDir) {
184
+ try {
185
+ return (await stat(assetsDir)).isDirectory();
186
+ }
187
+ catch {
188
+ return false;
189
+ }
190
+ }
191
+ /** Tag `<link rel="preload">` untuk font (masukkan lewat `metadata.head` halaman/dokumen). */
192
+ export function fontPreloadTag(href, options = {}) {
193
+ const type = options.type ?? "font/woff2";
194
+ const crossorigin = (options.crossorigin ?? true) ? " crossorigin" : "";
195
+ return `<link rel="preload" as="font" type="${type}" href="${href}"${crossorigin} />`;
196
+ }
197
+ //# sourceMappingURL=assets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assets.js","sourceRoot":"","sources":["../../../packages/bundler/src/assets.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,uGAAuG;AACvG,gGAAgG;AAChG,kGAAkG;AAClG,EAAE;AACF,kGAAkG;AAClG,iGAAiG;AAEjG,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACvF,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACzE,OAAO,EAGL,0BAA0B,GAE3B,MAAM,eAAe,CAAC;AACvB,OAAO,EAKL,iBAAiB,GAClB,MAAM,eAAe,CAAC;AAIvB,4EAA4E;AAC5E,MAAM,CAAC,MAAM,kBAAkB,GAAG,YAAY,CAAC;AAgC/C,KAAK,UAAU,YAAY,CAAC,GAAW,EAAE,KAAK,GAAa,EAAE;IAC3D,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5D,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAC/B,SAAS;QACX,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;YACxB,MAAM,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAClC,CAAC;aAAM,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;YAC1B,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,WAAW,CAAC,IAAgB;IACnC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AACtE,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,YAAY,CAAC,GAAW,EAAE,IAAY;IAC7C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IACzB,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;IACvD,OAAO,GAAG,QAAQ,IAAI,IAAI,GAAG,GAAG,EAAE,CAAC;AACrC,CAAC;AAYD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,SAAiB,EACjB,YAAoB,EACpB,OAAO,GAAyB,EAAE;IAElC,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,SAAS,CAAC,CAAC;IAC5C,MAAM,MAAM,GAA+B,EAAE,CAAC;IAC9C,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;IAEjC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC;QAClC,MAAM,GAAG,GAAG,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAEjE,IAAI,IAAI,GAAwC,EAAE,CAAC;QACnD,MAAM,OAAO,GAA2B,EAAE,CAAC;QAC3C,KAAK,MAAM,SAAS,IAAI,OAAO,CAAC,UAAU,IAAI,EAAE,EAAE,CAAC;YACjD,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;YACnD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,SAAS;YACX,CAAC;YACD,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;gBAC7B,IAAI,GAAG,EAAE,GAAG,IAAI,EAAE,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;YACpC,CAAC;YACD,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;gBAChC,OAAO,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;YACjC,CAAC;QACH,CAAC;QAED,MAAM,SAAS,GAAG,YAAY,CAAC,GAAG,EAAE,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC;QACvD,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC;QAC7C,MAAM,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC7B,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAEtB,MAAM,QAAQ,GAAwB,EAAE,CAAC;QACzC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,MAAM,SAAS,GAAG,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;YACtE,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC;YACnD,MAAM,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACxD,MAAM,SAAS,CAAC,YAAY,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;YAC3C,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YACtB,QAAQ,CAAC,IAAI,CAAC;gBACZ,IAAI,EAAE,SAAS;gBACf,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,UAAU;gBAC7B,KAAK,EAAE,MAAM,CAAC,KAAK;gBACnB,GAAG,CAAC,MAAM,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACjE,MAAM,EAAE,CAAC,MAAM,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE;aACvE,CAAC,CAAC;QACL,CAAC;QAED,MAAM,CAAC,GAAG,CAAC,GAAG;YACZ,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,IAAI,CAAC,UAAU;YACtB,GAAG,CAAC,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC1D,GAAG,CAAC,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7D,GAAG,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC7C,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC,EAAE,CAAC;AACxC,CAAC;AAOD,SAAS,kBAAkB,CAAC,IAAY;IACtC,OAAO,IAAI;SACR,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;SACrB,KAAK,CAAC,GAAG,CAAC;SACV,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AACrB,CAAC;AAED,+FAA+F;AAC/F,SAAS,SAAS,CAAC,IAAY,EAAE,MAAc;IAC7C,OAAO,GAAG,MAAM,IAAI,IAAI,EAAE,CAAC;AAC7B,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,QAAQ,CACtB,QAA8B,EAC9B,IAAY,EACZ,OAAO,GAAoB,EAAE;IAE7B,MAAM,KAAK,GAAG,QAAQ,EAAE,MAAM,EAAE,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3D,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,SAAS,IAAI,SAAS,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC;AACpF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAC1B,QAA8B,EAC9B,OAAO,GAAoB,EAAE;IAE7B,MAAM,MAAM,GAAG,CAAC,OAAO,CAAC,SAAS,IAAI,SAAS,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACnE,MAAM,GAAG,GAA8B,EAAE,CAAC;IAC1C,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,CAAC,EAAE,CAAC;QACnE,GAAG,CAAC,IAAI,CAAC,GAAG;YACV,GAAG,EAAE,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC;YAClC,GAAG,CAAC,KAAK,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC5D,GAAG,CAAC,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC/D,GAAG,CAAC,KAAK,CAAC,QAAQ,KAAK,SAAS;gBAC9B,CAAC,CAAC;oBACE,QAAQ,EAAE,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;wBACzC,GAAG,EAAE,SAAS,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;wBACpC,KAAK,EAAE,OAAO,CAAC,KAAK;wBACpB,GAAG,CAAC,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;wBACnE,MAAM,EAAE,OAAO,CAAC,MAAM;wBACtB,KAAK,EAAE,OAAO,CAAC,KAAK;qBACrB,CAAC,CAAC;iBACJ;gBACH,CAAC,CAAC,EAAE,CAAC;SACR,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAUD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,SAAiB,EACjB,OAAO,GAA+B,EAAE;IAExC,MAAM,QAAQ,GAAG,MAAM,iBAAiB,CAAC,SAAS,CAAC,CAAC;IACpD,MAAM,MAAM,GAAG,CAAC,OAAO,CAAC,SAAS,IAAI,SAAS,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACnE,MAAM,IAAI,GAAG,0BAA0B,CAAC,YAAY,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;IAEvF,OAAO;QACL,GAAG,CAAC,IAAY;YACd,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC7B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,OAAO,KAAK,CAAC;YACf,CAAC;YACD,IAAI,OAAO,CAAC,cAAc,KAAK,IAAI,EAAE,CAAC;gBACpC,OAAO,GAAG,MAAM,IAAI,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;YACjD,CAAC;YACD,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,KAAK,CAAC,IAAY;YAChB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAC/B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,OAAO,KAAK,CAAC;YACf,CAAC;YACD,IAAI,OAAO,CAAC,cAAc,KAAK,IAAI,EAAE,CAAC;gBACpC,gEAAgE;gBAChE,OAAO,EAAE,GAAG,EAAE,GAAG,MAAM,IAAI,kBAAkB,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;YAC1D,CAAC;YACD,OAAO,SAAS,CAAC;QACnB,CAAC;KACF,CAAC;AACJ,CAAC;AAED,6FAA6F;AAC7F,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,SAAiB;IACrD,IAAI,CAAC;QACH,OAAO,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,cAAc,CAC5B,IAAY,EACZ,OAAO,GAA+D,EAAE;IAExE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,YAAY,CAAC;IAC1C,MAAM,WAAW,GAAG,CAAC,OAAO,CAAC,WAAW,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC;IACxE,OAAO,uCAAuC,IAAI,WAAW,IAAI,IAAI,WAAW,KAAK,CAAC;AACxF,CAAC","sourcesContent":["// Pipeline aset (ADR-0069 M1, varian ADR-0079 M2 — tanpa dependency): pindai `src/assets/**`,\n// salin ke **root keluaran klien** (`dist/client/**`) dengan **hash konten** (URL = `file`, ADR-0079),\n// jalankan prosesor plugin (mis. varian gambar dari `@ramonyx/plugin-image`), catat semuanya di\n// `ramonyx.assets.json`, dan sediakan resolver (`assetUrl`/`createAssetResolver`/`assetInfoMap`).\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, writeFile } from \"node:fs/promises\";\nimport { dirname, extname, join, posix, relative, sep } from \"node:path\";\nimport {\n type AssetInfo,\n type AssetResolver,\n createAssetResolverFromMap,\n type ResolvedImage,\n} from \"@ramonyx/core\";\nimport {\n type AssetEntry,\n type AssetManifest,\n type AssetMap,\n type AssetVariantEntry,\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\n// ── Kontrak prosesor aset (dipakai hook plugin `assets` — ADR-0079) ──────────────────────────\n\n/** Input prosesor: satu berkas `src/assets/**` (path relatif posix + isi). */\nexport interface AssetProcessorInput {\n readonly path: string;\n readonly data: Uint8Array;\n}\n\n/** Berkas turunan yang dihasilkan prosesor (mis. varian gambar ter-resize). */\nexport interface AssetProcessorOutput {\n /** Path relatif terhadap `src/assets` (mis. `img/hero-640.webp`) — bundler menambah hash konten. */\n readonly path: string;\n readonly data: Uint8Array;\n /** Lebar varian (dipakai `srcset`). */\n readonly width: number;\n readonly height?: number;\n /** Format berkas; default dari ekstensi `path`. */\n readonly format?: string;\n}\n\n/** Hasil prosesor: metadata entri asli + berkas turunan. */\nexport interface AssetProcessorResult {\n readonly meta?: { readonly width?: number; readonly height?: number };\n readonly outputs?: readonly AssetProcessorOutput[];\n}\n\nexport type AssetProcessor = (\n asset: AssetProcessorInput,\n) => AssetProcessorResult | undefined | Promise<AssetProcessorResult | undefined>;\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\nfunction contentHash(data: Uint8Array): string {\n return createHash(\"sha256\").update(data).digest(\"hex\").slice(0, 10);\n}\n\n/**\n * `img/hero.png` + hash → `img/hero-<hash>.png` (relatif direktori keluaran klien).\n *\n * Catatan (koreksi M1, ADR-0079): berkas ditulis **di root keluaran klien** — konsisten dengan\n * URL `<prefix>/<path>` dan middleware `serveStatic({ dir: dist/client, prefix: \"/assets\" })`.\n * Layout M1 (`dist/client/assets/**`) tidak pernah dapat disajikan (URL → root), tertangkap\n * oleh test HTTP aset di create-app.\n */\nfunction hashedTarget(rel: string, hash: string): string {\n const ext = extname(rel);\n const tanpaExt = rel.slice(0, rel.length - ext.length);\n return `${tanpaExt}-${hash}${ext}`;\n}\n\nexport interface CollectAssetsOptions {\n /** Prosesor plugin (dijalankan berurutan per berkas — ADR-0079). */\n readonly processors?: readonly AssetProcessor[];\n}\n\nexport interface CollectAssetsResult {\n readonly assets: AssetMap;\n readonly files: readonly string[];\n}\n\n/**\n * Salin aset dari `assetsDir` ke `<clientOutDir>/**` dengan nama ber-hash konten.\n * Key manifest = path relatif posix terhadap `assetsDir` (mis. `img/logo.png`);\n * `file` = path relatif terhadap keluaran klien → URL publik = `<prefix>/<file>` (ADR-0079).\n * Prosesor (bila ada) dapat menambah varian + metadata dimensi (ADR-0079).\n */\nexport async function collectAssets(\n assetsDir: string,\n clientOutDir: string,\n options: CollectAssetsOptions = {},\n): Promise<CollectAssetsResult> {\n const files = await collectFiles(assetsDir);\n const assets: Record<string, AssetEntry> = {};\n const copied = new Set<string>();\n\n for (const file of files) {\n const data = await readFile(file);\n const rel = relative(assetsDir, file).split(sep).join(posix.sep);\n\n let meta: { width?: number; height?: number } = {};\n const outputs: AssetProcessorOutput[] = [];\n for (const processor of options.processors ?? []) {\n const hasil = await processor({ path: rel, data });\n if (hasil === undefined) {\n continue;\n }\n if (hasil.meta !== undefined) {\n meta = { ...meta, ...hasil.meta };\n }\n if (hasil.outputs !== undefined) {\n outputs.push(...hasil.outputs);\n }\n }\n\n const targetRel = hashedTarget(rel, contentHash(data));\n const target = join(clientOutDir, targetRel);\n await mkdir(dirname(target), { recursive: true });\n await copyFile(file, target);\n copied.add(targetRel);\n\n const variants: AssetVariantEntry[] = [];\n for (const output of outputs) {\n const outputRel = hashedTarget(output.path, contentHash(output.data));\n const outputTarget = join(clientOutDir, outputRel);\n await mkdir(dirname(outputTarget), { recursive: true });\n await writeFile(outputTarget, output.data);\n copied.add(outputRel);\n variants.push({\n file: outputRel,\n bytes: output.data.byteLength,\n width: output.width,\n ...(output.height !== undefined ? { height: output.height } : {}),\n format: (output.format ?? extname(output.path).slice(1)).toLowerCase(),\n });\n }\n\n assets[rel] = {\n file: targetRel,\n bytes: data.byteLength,\n ...(meta.width !== undefined ? { width: meta.width } : {}),\n ...(meta.height !== undefined ? { height: meta.height } : {}),\n ...(variants.length > 0 ? { variants } : {}),\n };\n }\n\n return { assets, files: [...copied] };\n}\n\nexport interface AssetUrlOptions {\n /** Prefix URL publik; default `/assets`. */\n readonly urlPrefix?: string;\n}\n\nfunction normalizeAssetPath(path: string): string {\n return path\n .replace(/^\\.?\\//, \"\")\n .split(sep)\n .join(posix.sep);\n}\n\n/** Berkas keluaran (relatif clientOutDir) → URL publik: `<prefix>/<file>` (1:1 — ADR-0079). */\nfunction fileToUrl(file: string, prefix: string): string {\n return `${prefix}/${file}`;\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 return fileToUrl(entry.file, (options.urlPrefix ?? \"/assets\").replace(/\\/$/, \"\"));\n}\n\n/**\n * Manifest → peta `AssetInfo` (URL + dimensi + varian) untuk resolver: dipakai\n * `createAssetResolver` (Node) dan artefak edge yang meng-inline peta (ADR-0069/0079).\n */\nexport function assetInfoMap(\n manifest: AssetManifest | null,\n options: AssetUrlOptions = {},\n): Record<string, AssetInfo> {\n const prefix = (options.urlPrefix ?? \"/assets\").replace(/\\/$/, \"\");\n const map: Record<string, AssetInfo> = {};\n for (const [path, entry] of Object.entries(manifest?.assets ?? {})) {\n map[path] = {\n url: fileToUrl(entry.file, prefix),\n ...(entry.width !== undefined ? { width: entry.width } : {}),\n ...(entry.height !== undefined ? { height: entry.height } : {}),\n ...(entry.variants !== undefined\n ? {\n variants: entry.variants.map((variant) => ({\n url: fileToUrl(variant.file, prefix),\n width: variant.width,\n ...(variant.height !== undefined ? { height: variant.height } : {}),\n format: variant.format,\n bytes: variant.bytes,\n })),\n }\n : {}),\n };\n }\n return map;\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 * tanpa varian — ADR-0079).\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 const base = createAssetResolverFromMap(assetInfoMap(manifest, { urlPrefix: prefix }));\n\n return {\n url(path: string): string | undefined {\n const found = base.url(path);\n if (found !== undefined) {\n return found;\n }\n if (options.fallbackToPath === true) {\n return `${prefix}/${normalizeAssetPath(path)}`;\n }\n return undefined;\n },\n image(path: string): ResolvedImage | undefined {\n const found = base.image(path);\n if (found !== undefined) {\n return found;\n }\n if (options.fallbackToPath === true) {\n // Dev: berkas disajikan apa adanya (tanpa hash/varian/dimensi).\n return { src: `${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,8 @@
1
- import type { LoadedPage } from "@ramonyx/core";
1
+ import { type LoadedPage } from "@ramonyx/core";
2
+ import { type PluginOption } from "vite";
3
+ import { type AssetProcessor } from "./assets.js";
4
+ export type { AssetEntry, AssetManifest, AssetMap } from "./manifest.js";
5
+ export { CLIENT_MANIFEST_FILE, readAssetManifest } from "./manifest.js";
2
6
  export interface BuildClientOptions {
3
7
  readonly appDir: string;
4
8
  readonly pages: readonly LoadedPage[];
@@ -6,17 +10,33 @@ export interface BuildClientOptions {
6
10
  readonly outDir?: string;
7
11
  /** Hasilkan source map (`.js.map` + `sourcesContent`); default true. */
8
12
  readonly sourcemap?: boolean;
9
- /** URL publik bundle untuk `<script type="module">`; default "/assets/client.js". */
10
- readonly scriptPath?: string;
13
+ /** Prefix URL publik aset (untuk `scriptPath` hasil); default "/assets". */
14
+ readonly assetsUrlPrefix?: string;
11
15
  readonly minify?: boolean;
16
+ /** Plugin Vite tambahan (dari plugin Ramonyx — ADR-0067). */
17
+ readonly plugins?: readonly PluginOption[];
18
+ /** Prosesor aset (hook `assets` plugin — ADR-0079). */
19
+ readonly assetProcessors?: readonly AssetProcessor[];
20
+ /**
21
+ * Direktori aset (relatif appDir) yang dipindai & disalin ber-hash ke `assets/**` +
22
+ * dicatat di manifest (ADR-0069). Default `src/assets` bila ada; `false` mematikan.
23
+ */
24
+ readonly assetsDir?: string | false;
12
25
  }
13
26
  export interface BuildClientResult {
14
27
  readonly outDir: string;
28
+ /** URL bundle klien ber-fingerprint, mis. `/assets/client-D3fK2p.js`. */
15
29
  readonly scriptPath: string;
16
30
  readonly entryFile: string;
31
+ /** Nama berkas (relatif terhadap outDir) yang dihasilkan build. */
32
+ readonly files: readonly string[];
17
33
  }
34
+ export declare function buildClient(options: BuildClientOptions): Promise<BuildClientResult>;
18
35
  /**
19
- * Membundel client (React + registry halaman) memakai Vite (ADR-0005).
20
- * Output ditulis ke `<appDir>/dist/client` dan siap disajikan sebagai aset statis.
36
+ * URL bundle klien dari manifest hasil build — dipakai server (`ramonyx start`, aplikasi)
37
+ * agar tetap benar saat nama berkas ber-hash. Fallback `/assets/client.js` bila manifest
38
+ * belum ada (mis. server dijalankan tanpa build — CLI sudah memperingatkan).
21
39
  */
22
- export declare function buildClient(options: BuildClientOptions): Promise<BuildClientResult>;
40
+ export declare function readClientScript(clientDir: string, options?: {
41
+ urlPrefix?: string;
42
+ }): 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,47 @@ 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
+ processors: options.assetProcessors,
116
+ });
117
+ assets = collected.assets;
118
+ }
119
+ const manifest = {
120
+ client: clientFile,
121
+ files: [...files].sort(),
122
+ ...(Object.keys(assets).length > 0 ? { assets } : {}),
43
123
  };
124
+ await writeAssetManifest(outDir, manifest);
125
+ const prefix = (options.assetsUrlPrefix ?? "/assets").replace(/\/$/, "");
126
+ return { outDir, scriptPath: `${prefix}/${clientFile}`, entryFile, files: manifest.files };
127
+ }
128
+ /**
129
+ * URL bundle klien dari manifest hasil build — dipakai server (`ramonyx start`, aplikasi)
130
+ * agar tetap benar saat nama berkas ber-hash. Fallback `/assets/client.js` bila manifest
131
+ * belum ada (mis. server dijalankan tanpa build — CLI sudah memperingatkan).
132
+ */
133
+ export async function readClientScript(clientDir, options = {}) {
134
+ const prefix = (options.urlPrefix ?? "/assets").replace(/\/$/, "");
135
+ const manifest = await readAssetManifest(clientDir);
136
+ return manifest === null ? `${prefix}/client.js` : `${prefix}/${manifest.client}`;
44
137
  }
45
138
  //# 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 { type AssetProcessor, 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 /** Prosesor aset (hook `assets` plugin — ADR-0079). */\n readonly assetProcessors?: readonly AssetProcessor[];\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 processors: options.assetProcessors,\n });\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
  /**