create-ramonyx-app 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 +1 -1
- package/package.json +1 -1
- package/template/README.md +6 -4
- package/template/package.json +12 -11
- package/template/src/app.ts +74 -25
- package/template/src/assets/logo.svg +4 -0
- package/template/src/routes/[...slug]/page.tsx +19 -0
- package/template/src/routes/middleware.ts +7 -39
- package/template/src/routes/page.tsx +39 -5
- package/template/build.ts +0 -13
- package/template/src/main.ts +0 -16
package/README.md
CHANGED
package/package.json
CHANGED
package/template/README.md
CHANGED
|
@@ -8,17 +8,19 @@ Aplikasi [Ramonyx](https://github.com/ramonyx/ramonyx) — framework full-stack
|
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
pnpm install
|
|
11
|
-
pnpm
|
|
12
|
-
pnpm
|
|
13
|
-
pnpm
|
|
11
|
+
pnpm dev # Vite + HMR (http://localhost:5173) → SSR :3000
|
|
12
|
+
pnpm build # bundel klien (wajib sebelum start produksi)
|
|
13
|
+
pnpm start # http://127.0.0.1:3000 (NODE_ENV=production otomatis)
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
Perintah memakai CLI **`ramonyx`** (`@ramonyx/devtools`): `ramonyx dev|build|start|doctor`.
|
|
17
|
+
|
|
16
18
|
## Struktur
|
|
17
19
|
|
|
20
|
+
- `src/app.ts` — mengekspor `createApp(options?)` (dipakai CLI)
|
|
18
21
|
- `src/routes/page.tsx` — halaman (loader + komponen React; `layout.tsx`/`error.tsx` opsional)
|
|
19
22
|
- `src/routes/api/halo/[nama].ts` — route API (`export const GET = route({ ... })`)
|
|
20
23
|
- `src/routes/middleware.ts` — security headers + CSP nonce + aset `/assets/*`
|
|
21
|
-
- `build.ts` — bundel klien (Vite) lewat `@ramonyx/bundler`
|
|
22
24
|
- `ramonyx.config.ts` — port/host + strict validation
|
|
23
25
|
|
|
24
26
|
## Endpoint contoh
|
package/template/package.json
CHANGED
|
@@ -6,19 +6,20 @@
|
|
|
6
6
|
"node": ">=24"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
|
-
"build": "
|
|
10
|
-
"start": "
|
|
11
|
-
"dev": "
|
|
9
|
+
"build": "ramonyx build",
|
|
10
|
+
"start": "ramonyx start",
|
|
11
|
+
"dev": "ramonyx dev"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@ramonyx/adapter-runtime": "^0.
|
|
15
|
-
"@ramonyx/bundler": "^0.
|
|
16
|
-
"@ramonyx/compiler": "^0.
|
|
17
|
-
"@ramonyx/
|
|
18
|
-
"@ramonyx/
|
|
19
|
-
"@ramonyx/
|
|
20
|
-
"@ramonyx/
|
|
21
|
-
"ramonyx": "^0.
|
|
14
|
+
"@ramonyx/adapter-runtime": "^0.2.0",
|
|
15
|
+
"@ramonyx/bundler": "^0.2.0",
|
|
16
|
+
"@ramonyx/compiler": "^0.2.0",
|
|
17
|
+
"@ramonyx/devtools": "^0.2.0",
|
|
18
|
+
"@ramonyx/react": "^0.2.0",
|
|
19
|
+
"@ramonyx/security": "^0.2.0",
|
|
20
|
+
"@ramonyx/server": "^0.2.0",
|
|
21
|
+
"@ramonyx/ssr": "^0.2.0",
|
|
22
|
+
"ramonyx": "^0.2.0",
|
|
22
23
|
"react": "19.3.1",
|
|
23
24
|
"react-dom": "19.3.1"
|
|
24
25
|
},
|
package/template/src/app.ts
CHANGED
|
@@ -1,38 +1,87 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
|
-
import {
|
|
2
|
+
import { serveStatic } from "@ramonyx/adapter-runtime";
|
|
3
|
+
import { createAssetResolver } from "@ramonyx/bundler";
|
|
4
|
+
import { createRouteManifest, loadPageRoutes, type ModuleImporter } from "@ramonyx/compiler";
|
|
3
5
|
import { createReactRenderer } from "@ramonyx/react";
|
|
4
6
|
import { createHandler } from "@ramonyx/server";
|
|
5
|
-
import { createPageRoutes } from "@ramonyx/ssr";
|
|
6
|
-
import type { FetchHandler } from "ramonyx";
|
|
7
|
+
import { createPageRoutes, createSeoRoutes } from "@ramonyx/ssr";
|
|
8
|
+
import type { AppHooks, FetchHandler } from "ramonyx";
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
// Aset statis hanya untuk host Node (edge: platform menyajikan aset; `middleware.ts` tetap
|
|
11
|
+
// edge-safe agar `ramonyx build --target edge` bisa membundelnya — ADR-0058).
|
|
12
|
+
const clientDir = fileURLToPath(new URL("../dist/client", import.meta.url));
|
|
13
|
+
// Resolver aset (ADR-0069): produksi memakai manifest (ber-hash); dev memakai fallback
|
|
14
|
+
// (`fallbackToPath`) yang disajikan Vite dari `src/assets/**`.
|
|
15
|
+
const assets = await createAssetResolver(clientDir, { fallbackToPath: true });
|
|
16
|
+
const staticAssets = serveStatic({
|
|
17
|
+
dir: clientDir,
|
|
18
|
+
prefix: "/assets",
|
|
19
|
+
immutable: true,
|
|
20
|
+
headers: { "x-content-type-options": "nosniff" },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export interface CreateAppOptions {
|
|
24
|
+
/** URL bundle klien (produksi "/assets/client.js"; dev via entry Vite). */
|
|
25
|
+
readonly clientScript?: string;
|
|
26
|
+
/** Modul tambahan sebelum entry (mis. "/@vite/client"). */
|
|
27
|
+
readonly clientModules?: readonly string[];
|
|
28
|
+
/** Importer modul (dev: module graph Vite). */
|
|
29
|
+
readonly importer?: ModuleImporter;
|
|
30
|
+
/** Bangun ulang manifest tiap request (dev, tanpa restart). */
|
|
31
|
+
readonly reloadManifests?: boolean;
|
|
32
|
+
readonly strictValidation?: boolean;
|
|
33
|
+
/** SSR streaming untuk halaman (ADR-0047). */
|
|
34
|
+
readonly streaming?: boolean;
|
|
35
|
+
/** Cache data loader (ADR-0066) — halaman ber-`revalidate` memakainya. */
|
|
36
|
+
readonly cache?: import("ramonyx").Cache;
|
|
37
|
+
/** Hook aplikasi (plugin/CLI — ADR-0065/0067). */
|
|
38
|
+
readonly hooks?: AppHooks;
|
|
39
|
+
}
|
|
9
40
|
|
|
10
41
|
/**
|
|
11
42
|
* Aplikasi __APP_NAME__: route API + halaman SSR React dari `src/routes` (file routing),
|
|
12
|
-
* dengan hydration klien
|
|
43
|
+
* dengan hydration klien.
|
|
13
44
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* - `api/*.ts` — route API (`export const GET = route({ ... })`).
|
|
45
|
+
* Dijalankan lewat CLI: `ramonyx dev` (Vite + HMR), `ramonyx build` (bundle klien),
|
|
46
|
+
* `ramonyx start` (produksi — `NODE_ENV=production` otomatis).
|
|
17
47
|
*/
|
|
18
|
-
export async function createApp(): Promise<FetchHandler> {
|
|
48
|
+
export async function createApp(options: CreateAppOptions = {}): Promise<FetchHandler> {
|
|
19
49
|
const dir = fileURLToPath(new URL("./routes", import.meta.url));
|
|
50
|
+
const client =
|
|
51
|
+
options.clientScript === undefined
|
|
52
|
+
? undefined
|
|
53
|
+
: { script: options.clientScript, extraModules: options.clientModules };
|
|
54
|
+
|
|
55
|
+
const build = async (): Promise<FetchHandler> => {
|
|
56
|
+
const [apiRoutes, pages] = await Promise.all([
|
|
57
|
+
createRouteManifest({ dir, importer: options.importer }),
|
|
58
|
+
loadPageRoutes({ dir, importer: options.importer }),
|
|
59
|
+
]);
|
|
60
|
+
// /sitemap.xml + /robots.txt dari manifest halaman (ADR-0059).
|
|
61
|
+
const seoRoutes = await createSeoRoutes({ pages });
|
|
20
62
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
63
|
+
return createHandler({
|
|
64
|
+
routes: [
|
|
65
|
+
...apiRoutes,
|
|
66
|
+
...seoRoutes,
|
|
67
|
+
...createPageRoutes({
|
|
68
|
+
pages,
|
|
69
|
+
renderer: createReactRenderer(),
|
|
70
|
+
client,
|
|
71
|
+
streaming: options.streaming,
|
|
72
|
+
cache: options.cache,
|
|
73
|
+
hooks: options.hooks,
|
|
74
|
+
assets,
|
|
75
|
+
}),
|
|
76
|
+
],
|
|
77
|
+
middleware: [staticAssets],
|
|
78
|
+
strictValidation: options.strictValidation,
|
|
79
|
+
hooks: options.hooks,
|
|
80
|
+
});
|
|
81
|
+
};
|
|
25
82
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
...createPageRoutes({
|
|
31
|
-
pages,
|
|
32
|
-
renderer: createReactRenderer(),
|
|
33
|
-
client: { script: "/assets/client.js" },
|
|
34
|
-
}),
|
|
35
|
-
],
|
|
36
|
-
middleware,
|
|
37
|
-
});
|
|
83
|
+
if (options.reloadManifests === true) {
|
|
84
|
+
return async (request) => (await build())(request);
|
|
85
|
+
}
|
|
86
|
+
return build();
|
|
38
87
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" role="img" aria-label="Logo">
|
|
2
|
+
<rect width="32" height="32" rx="7" fill="#7cc4ff"/>
|
|
3
|
+
<text x="16" y="22" font-family="system-ui, sans-serif" font-size="18" font-weight="700" text-anchor="middle" fill="#0f1115">R</text>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PageProps } from "@ramonyx/react";
|
|
2
|
+
|
|
3
|
+
export const metadata = { title: "Tidak ditemukan", status: 404 };
|
|
4
|
+
/** Halaman 404 catch-all — tidak diprerender (rutenya tak terbatas). */
|
|
5
|
+
export const prerender = false;
|
|
6
|
+
|
|
7
|
+
export default function TidakDitemukan({ params }: PageProps<unknown>) {
|
|
8
|
+
return (
|
|
9
|
+
<main>
|
|
10
|
+
<h1>404 — Tidak ditemukan</h1>
|
|
11
|
+
<p>
|
|
12
|
+
Halaman <code>/{params.slug}</code> tidak ada.
|
|
13
|
+
</p>
|
|
14
|
+
<p>
|
|
15
|
+
<a href="/">Kembali ke beranda</a>
|
|
16
|
+
</p>
|
|
17
|
+
</main>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -1,42 +1,10 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { extname, resolve, sep } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
1
|
import { cspNonce, securityHeaders } from "@ramonyx/security";
|
|
5
2
|
import type { Middleware } from "ramonyx";
|
|
6
3
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
/** Aset statis hasil `pnpm build` di `/assets/*` (guard traversal). */
|
|
17
|
-
const staticAssets: Middleware = async (context, next) => {
|
|
18
|
-
const url = new URL(context.request.url);
|
|
19
|
-
if (context.request.method !== "GET" || !url.pathname.startsWith("/assets/")) {
|
|
20
|
-
return next();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const relativePath = decodeURIComponent(url.pathname.slice("/assets/".length));
|
|
24
|
-
const filePath = resolve(clientDir, relativePath);
|
|
25
|
-
if (!filePath.startsWith(clientDir + sep)) {
|
|
26
|
-
return next();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
const data = await readFile(filePath);
|
|
31
|
-
return new Response(data, {
|
|
32
|
-
headers: {
|
|
33
|
-
"content-type": CONTENT_TYPES[extname(filePath)] ?? "application/octet-stream",
|
|
34
|
-
"cache-control": "no-store",
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
} catch {
|
|
38
|
-
return next();
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export default [securityHeaders(), cspNonce(), staticAssets] satisfies Middleware[];
|
|
4
|
+
/**
|
|
5
|
+
* Middleware aplikasi — **edge-safe** (Web Crypto + web standard saja; tanpa `node:*`),
|
|
6
|
+
* sehingga `ramonyx build --target edge` dapat membundelnya.
|
|
7
|
+
*
|
|
8
|
+
* Penyajian aset statis (`serveStatic`) adalah urusan host Node dan dipasang di `src/app.ts`.
|
|
9
|
+
*/
|
|
10
|
+
export default [securityHeaders(), cspNonce()] satisfies Middleware[];
|
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
import type { PageProps } from "@ramonyx/react";
|
|
2
|
+
import { Image } from "@ramonyx/react/assets";
|
|
3
|
+
import type { PageActionContext } from "ramonyx";
|
|
2
4
|
import { useState } from "react";
|
|
3
5
|
|
|
4
|
-
export const metadata = {
|
|
6
|
+
export const metadata = {
|
|
7
|
+
title: "__APP_NAME__",
|
|
8
|
+
description: "Aplikasi Ramonyx — SSR React, file routing, dan server actions.",
|
|
9
|
+
};
|
|
10
|
+
/** Cache data loader 60 dtk (ADR-0066) — hapus atau sesuaikan bila data harus selalu segar. */
|
|
11
|
+
export const revalidate = 60;
|
|
5
12
|
|
|
6
|
-
export async function loader() {
|
|
7
|
-
return {
|
|
13
|
+
export async function loader({ assets }: { assets?: { url(path: string): string | undefined } }) {
|
|
14
|
+
return {
|
|
15
|
+
pesan: "Halo dari SSR Ramonyx",
|
|
16
|
+
fitur: ["SSR React", "hydration", "route API"],
|
|
17
|
+
logo: assets?.url("logo.svg"),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Mutasi form: POST ke path yang sama (progressive enhancement — ADR-0057). */
|
|
22
|
+
export function action({ formData }: PageActionContext) {
|
|
23
|
+
const nama = String(formData.get("nama") ?? "").trim();
|
|
24
|
+
if (nama.length < 2) {
|
|
25
|
+
return { errors: { nama: "Nama minimal 2 karakter" } };
|
|
26
|
+
}
|
|
27
|
+
return { message: `Halo, ${nama}!` };
|
|
8
28
|
}
|
|
9
29
|
|
|
10
30
|
/** Komponen interaktif: server merender 0, klik menambah setelah hydration. */
|
|
@@ -19,16 +39,30 @@ function Counter() {
|
|
|
19
39
|
);
|
|
20
40
|
}
|
|
21
41
|
|
|
22
|
-
export default function Beranda({
|
|
42
|
+
export default function Beranda({
|
|
43
|
+
data,
|
|
44
|
+
action,
|
|
45
|
+
}: PageProps<{ pesan: string; fitur: string[]; logo?: string }>) {
|
|
23
46
|
return (
|
|
24
47
|
<main>
|
|
25
|
-
<h1>
|
|
48
|
+
<h1>
|
|
49
|
+
<Image src={data.logo} alt="Logo" width={32} height={32} /> {data.pesan}
|
|
50
|
+
</h1>
|
|
26
51
|
<ul>
|
|
27
52
|
{data.fitur.map((item) => (
|
|
28
53
|
<li key={item}>{item}</li>
|
|
29
54
|
))}
|
|
30
55
|
</ul>
|
|
31
56
|
<Counter />
|
|
57
|
+
<h2>Form dengan action</h2>
|
|
58
|
+
<form method="post">
|
|
59
|
+
<label>
|
|
60
|
+
Nama: <input name="nama" />
|
|
61
|
+
</label>
|
|
62
|
+
<button type="submit">Kirim</button>
|
|
63
|
+
</form>
|
|
64
|
+
{action?.errors?.nama !== undefined ? <p role="alert">{action.errors.nama}</p> : null}
|
|
65
|
+
{action?.message !== undefined ? <p role="status">{action.message}</p> : null}
|
|
32
66
|
<p>
|
|
33
67
|
Coba API dinamis: <a href="/api/halo/dunia">/api/halo/dunia</a>
|
|
34
68
|
</p>
|
package/template/build.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
// Build client (React + hydration) lewat Vite — jalankan `pnpm build` sebelum `pnpm start`
|
|
2
|
-
// di produksi. Hasil: `dist/client/client.js` disajikan di `/assets/client.js`.
|
|
3
|
-
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { buildClient } from "@ramonyx/bundler";
|
|
7
|
-
import { loadPageRoutes } from "@ramonyx/compiler";
|
|
8
|
-
|
|
9
|
-
const appDir = fileURLToPath(new URL(".", import.meta.url));
|
|
10
|
-
const pages = await loadPageRoutes({ dir: join(appDir, "src/routes") });
|
|
11
|
-
const result = await buildClient({ appDir, pages });
|
|
12
|
-
|
|
13
|
-
console.log(`✓ client bundle: ${result.scriptPath} (${result.outDir})`);
|
package/template/src/main.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { createNodeHost } from "@ramonyx/adapter-runtime";
|
|
2
|
-
import { resolveConfig } from "ramonyx";
|
|
3
|
-
|
|
4
|
-
import config from "../ramonyx.config.ts";
|
|
5
|
-
import { createApp } from "./app.ts";
|
|
6
|
-
|
|
7
|
-
const resolved = resolveConfig(config);
|
|
8
|
-
const host = createNodeHost();
|
|
9
|
-
const app = await createApp();
|
|
10
|
-
|
|
11
|
-
const server = await host.serve(app, {
|
|
12
|
-
port: resolved.server.port,
|
|
13
|
-
host: resolved.server.host,
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
console.log(`__APP_NAME__ berjalan di ${server.url}`);
|