create-ramonyx-app 0.1.1 → 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
  Scaffolder aplikasi Ramonyx — SSR React + hydration, file routing `src/routes`.
4
4
 
5
- > **Alpha (0.1.1)** — butuh **Node.js ≥ 24**.
5
+ > **Alpha (0.2.0)** — butuh **Node.js ≥ 24**.
6
6
 
7
7
  ## Pemakaian
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ramonyx-app",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Scaffolder aplikasi Ramonyx (SSR React + hydration, file routing src/routes).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -11,13 +11,15 @@
11
11
  "dev": "ramonyx dev"
12
12
  },
13
13
  "dependencies": {
14
- "@ramonyx/compiler": "^0.1.0",
15
- "@ramonyx/devtools": "^0.1.0",
16
- "@ramonyx/react": "^0.1.0",
17
- "@ramonyx/security": "^0.1.0",
18
- "@ramonyx/server": "^0.1.0",
19
- "@ramonyx/ssr": "^0.1.0",
20
- "ramonyx": "^0.1.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",
21
23
  "react": "19.3.1",
22
24
  "react-dom": "19.3.1"
23
25
  },
@@ -1,11 +1,24 @@
1
1
  import { fileURLToPath } from "node:url";
2
+ import { serveStatic } from "@ramonyx/adapter-runtime";
3
+ import { createAssetResolver } from "@ramonyx/bundler";
2
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
- import middleware from "./routes/middleware.ts";
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
+ });
9
22
 
10
23
  export interface CreateAppOptions {
11
24
  /** URL bundle klien (produksi "/assets/client.js"; dev via entry Vite). */
@@ -19,6 +32,10 @@ export interface CreateAppOptions {
19
32
  readonly strictValidation?: boolean;
20
33
  /** SSR streaming untuk halaman (ADR-0047). */
21
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;
22
39
  }
23
40
 
24
41
  /**
@@ -40,19 +57,26 @@ export async function createApp(options: CreateAppOptions = {}): Promise<FetchHa
40
57
  createRouteManifest({ dir, importer: options.importer }),
41
58
  loadPageRoutes({ dir, importer: options.importer }),
42
59
  ]);
60
+ // /sitemap.xml + /robots.txt dari manifest halaman (ADR-0059).
61
+ const seoRoutes = await createSeoRoutes({ pages });
43
62
 
44
63
  return createHandler({
45
64
  routes: [
46
65
  ...apiRoutes,
66
+ ...seoRoutes,
47
67
  ...createPageRoutes({
48
68
  pages,
49
69
  renderer: createReactRenderer(),
50
70
  client,
51
71
  streaming: options.streaming,
72
+ cache: options.cache,
73
+ hooks: options.hooks,
74
+ assets,
52
75
  }),
53
76
  ],
54
- middleware,
77
+ middleware: [staticAssets],
55
78
  strictValidation: options.strictValidation,
79
+ hooks: options.hooks,
56
80
  });
57
81
  };
58
82
 
@@ -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
- const clientDir = fileURLToPath(new URL("../../dist/client", import.meta.url));
8
-
9
- const CONTENT_TYPES: Record<string, string> = {
10
- ".js": "text/javascript; charset=utf-8",
11
- ".css": "text/css; charset=utf-8",
12
- ".map": "application/json; charset=utf-8",
13
- ".svg": "image/svg+xml",
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 = { title: "__APP_NAME__" };
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 { pesan: "Halo dari SSR Ramonyx", fitur: ["SSR React", "hydration", "route API"] };
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({ data }: PageProps<{ pesan: string; fitur: string[] }>) {
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>{data.pesan}</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>