create-sanifyfe 0.1.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/dist/index.js ADDED
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { fileURLToPath } from "node:url";
5
+ import { dirname, join as join2, resolve, basename } from "node:path";
6
+ import { createInterface } from "node:readline/promises";
7
+ import { stdin, stdout } from "node:process";
8
+
9
+ // src/scaffold.ts
10
+ import { cp, readFile, writeFile, rename, mkdir, readdir } from "node:fs/promises";
11
+ import { existsSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ var PLACEHOLDER = "__PROJECT_NAME__";
14
+ async function isEmptyDir(dir) {
15
+ const entries = await readdir(dir);
16
+ return entries.length === 0;
17
+ }
18
+ async function scaffold(opts) {
19
+ const { targetDir, projectName, templateDir } = opts;
20
+ if (existsSync(targetDir) && !await isEmptyDir(targetDir)) {
21
+ throw new Error(`Direktori "${targetDir}" sudah ada dan tidak kosong.`);
22
+ }
23
+ await mkdir(targetDir, { recursive: true });
24
+ await cp(templateDir, targetDir, { recursive: true });
25
+ const gitignoreSrc = join(targetDir, "_gitignore");
26
+ if (existsSync(gitignoreSrc)) {
27
+ await rename(gitignoreSrc, join(targetDir, ".gitignore"));
28
+ }
29
+ for (const rel of ["package.json", "README.md", "index.html"]) {
30
+ const file = join(targetDir, rel);
31
+ if (!existsSync(file))
32
+ continue;
33
+ const content = await readFile(file, "utf8");
34
+ await writeFile(file, content.replaceAll(PLACEHOLDER, projectName));
35
+ }
36
+ }
37
+
38
+ // src/index.ts
39
+ var here = dirname(fileURLToPath(import.meta.url));
40
+ var templateDir = join2(here, "..", "templates", "default");
41
+ function isValidName(name) {
42
+ return /^[a-z0-9][a-z0-9._-]*$/.test(name);
43
+ }
44
+ async function main() {
45
+ let target = process.argv[2];
46
+ if (!target) {
47
+ const rl = createInterface({ input: stdin, output: stdout });
48
+ target = (await rl.question("Nama project: ")).trim();
49
+ rl.close();
50
+ }
51
+ if (!target) {
52
+ console.error("Error: nama project wajib diisi.");
53
+ process.exit(1);
54
+ }
55
+ const targetDir = resolve(process.cwd(), target);
56
+ const projectName = basename(targetDir);
57
+ if (!isValidName(projectName)) {
58
+ console.error(`Error: nama "${projectName}" tidak valid (huruf kecil, angka, "-", "_", ".").`);
59
+ process.exit(1);
60
+ }
61
+ try {
62
+ await scaffold({ targetDir, projectName, templateDir });
63
+ } catch (err) {
64
+ console.error(`Error: ${err.message}`);
65
+ process.exit(1);
66
+ }
67
+ console.log(`
68
+ Project "${projectName}" dibuat di ${targetDir}
69
+ `);
70
+ console.log("Langkah berikutnya:");
71
+ console.log(` cd ${target}`);
72
+ console.log(" bun install");
73
+ console.log(` bun dev
74
+ `);
75
+ }
76
+ main();
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "create-sanifyfe",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold project Sanify baru — bun create sanifyfe",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Satria Agung Nugraha",
8
+ "bin": {
9
+ "create-sanifyfe": "./dist/index.js"
10
+ },
11
+ "files": ["dist", "templates"],
12
+ "scripts": {
13
+ "build": "bun build ./src/index.ts --outdir ./dist --target node",
14
+ "test": "bun test"
15
+ },
16
+ "engines": {
17
+ "bun": ">=1.3.0"
18
+ }
19
+ }
@@ -0,0 +1,27 @@
1
+ # __PROJECT_NAME__
2
+
3
+ Project frontend berbasis [Sanify](https://www.npmjs.com/package/@sanify/core) — framework reaktif fine-grained di atas Web Components.
4
+
5
+ ## Prasyarat
6
+
7
+ - [Bun](https://bun.sh) >= 1.3.0
8
+
9
+ ## Setup
10
+
11
+ ```bash
12
+ bun install
13
+ ```
14
+
15
+ ## Perintah
16
+
17
+ | Perintah | Fungsi |
18
+ | --- | --- |
19
+ | `bun dev` | Jalankan dev server di http://localhost:3000 |
20
+ | `bun run build` | Bundle produksi ke `dist/` |
21
+ | `bun run typecheck` | Cek tipe TypeScript |
22
+
23
+ ## Struktur
24
+
25
+ - `src/main.ts` — entry aplikasi (komponen, router, state).
26
+ - `index.html` — halaman host, memuat `main.js` hasil bundling.
27
+ - `dev-server.ts` — server dev Bun yang transpile TS on-the-fly.
@@ -0,0 +1,2 @@
1
+ node_modules/
2
+ dist/
@@ -0,0 +1,42 @@
1
+ // dev-server.ts — dev server sederhana pakai Bun, transpile TS on-the-fly
2
+
3
+ const PORT = 3000;
4
+
5
+ const server = Bun.serve({
6
+ port: PORT,
7
+ development: true,
8
+ async fetch(req) {
9
+ const url = new URL(req.url);
10
+ let path = url.pathname;
11
+ if (path === "/") path = "/index.html";
12
+
13
+ // bundel entry TS aplikasi on-the-fly
14
+ if (path === "/main.js") {
15
+ const built = await Bun.build({
16
+ entrypoints: ["./src/main.ts"],
17
+ target: "browser",
18
+ });
19
+ if (!built.success) {
20
+ return new Response("Build error:\n" + built.logs.join("\n"), {
21
+ status: 500,
22
+ headers: { "Content-Type": "text/plain" },
23
+ });
24
+ }
25
+ const js = await built.outputs[0]!.text();
26
+ return new Response(js, {
27
+ headers: { "Content-Type": "application/javascript" },
28
+ });
29
+ }
30
+
31
+ // file statis dari root project
32
+ const file = Bun.file("." + path);
33
+ if (await file.exists()) return new Response(file);
34
+
35
+ // SPA fallback: route apa pun → index.html
36
+ return new Response(Bun.file("./index.html"), {
37
+ headers: { "Content-Type": "text/html" },
38
+ });
39
+ },
40
+ });
41
+
42
+ console.log(`Dev server: http://localhost:${server.port}`);
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="id">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>__PROJECT_NAME__</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ </head>
9
+ <body class="bg-slate-50 text-slate-800">
10
+ <app-root></app-root>
11
+ <script type="module" src="/main.js"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "__PROJECT_NAME__",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "bun run dev-server.ts",
8
+ "build": "bun build ./src/main.ts --outdir ./dist --target browser --minify",
9
+ "typecheck": "tsc --noEmit"
10
+ },
11
+ "dependencies": {
12
+ "@sanify/core": "^0.1.0"
13
+ },
14
+ "devDependencies": {
15
+ "typescript": "^5.9.0",
16
+ "@types/bun": "latest"
17
+ },
18
+ "engines": {
19
+ "bun": ">=1.3.0"
20
+ }
21
+ }
@@ -0,0 +1,156 @@
1
+ // example/main.ts — aplikasi contoh memakai seluruh API Sanify
2
+ // Todo dengan persist + cross-tab sync, routing, props dua arah, list & conditional.
3
+
4
+ import {
5
+ component,
6
+ html,
7
+ signal,
8
+ computed,
9
+ persisted,
10
+ router,
11
+ navigate,
12
+ type ComponentContext,
13
+ } from "@sanify/core";
14
+
15
+ // ── State global (persisted + cross-tab sync) ───────────────
16
+ interface Todo {
17
+ id: number;
18
+ text: string;
19
+ done: boolean;
20
+ }
21
+
22
+ const [todos, setTodos] = persisted<Todo[]>("sanify:todos", [], { sync: true });
23
+
24
+ const remaining = computed(() => todos().filter((t) => !t.done).length);
25
+
26
+ function addTodo(text: string): void {
27
+ const trimmed = text.trim();
28
+ if (!trimmed) return;
29
+ setTodos((prev) => [...prev, { id: Date.now(), text: trimmed, done: false }]);
30
+ }
31
+
32
+ function toggleTodo(id: number): void {
33
+ setTodos((prev) => prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
34
+ }
35
+
36
+ function removeTodo(id: number): void {
37
+ setTodos((prev) => prev.filter((t) => t.id !== id));
38
+ }
39
+
40
+ // ── Komponen: satu baris todo (menerima objek lewat property) ─
41
+ interface TodoItemProps {
42
+ todo: Todo;
43
+ }
44
+
45
+ component<TodoItemProps>(
46
+ "todo-item",
47
+ ({ props }: ComponentContext<TodoItemProps>) => {
48
+ return () => html`
49
+ <li class="flex items-center gap-3 py-2 border-b border-slate-200">
50
+ <input
51
+ type="checkbox"
52
+ class="h-4 w-4"
53
+ .checked=${() => props.todo().done}
54
+ @change=${() => toggleTodo(props.todo().id)}
55
+ />
56
+ <span
57
+ class=${() =>
58
+ props.todo().done
59
+ ? "flex-1 line-through text-slate-400"
60
+ : "flex-1"}
61
+ >${() => props.todo().text}</span
62
+ >
63
+ <button
64
+ class="text-red-500 hover:text-red-700 text-sm"
65
+ @click=${() => removeTodo(props.todo().id)}
66
+ >
67
+ hapus
68
+ </button>
69
+ </li>
70
+ `;
71
+ },
72
+ { props: ["todo"] },
73
+ );
74
+
75
+ // ── Halaman: daftar todo ────────────────────────────────────
76
+ component("todo-page", () => {
77
+ const [draft, setDraft] = signal("");
78
+
79
+ const submit = () => {
80
+ addTodo(draft());
81
+ setDraft("");
82
+ };
83
+
84
+ return () => html`
85
+ <div class="max-w-md mx-auto mt-10 p-6 bg-white rounded-xl shadow">
86
+ <h1 class="text-2xl font-bold mb-1">Todo</h1>
87
+ <p class="text-sm text-slate-500 mb-4">
88
+ Sisa <strong>${() => remaining()}</strong> belum selesai
89
+ — buka di dua tab untuk lihat sync.
90
+ </p>
91
+
92
+ <div class="flex gap-2 mb-4">
93
+ <input
94
+ class="flex-1 border border-slate-300 rounded px-3 py-2"
95
+ placeholder="Tambah tugas..."
96
+ .value=${() => draft()}
97
+ @input=${(e: Event) => setDraft((e.target as HTMLInputElement).value)}
98
+ @keydown=${(e: KeyboardEvent) => e.key === "Enter" && submit()}
99
+ />
100
+ <button
101
+ class="bg-slate-800 text-white rounded px-4 py-2 hover:bg-slate-700"
102
+ @click=${submit}
103
+ >
104
+ Tambah
105
+ </button>
106
+ </div>
107
+
108
+ <ul>
109
+ ${() =>
110
+ todos().length === 0
111
+ ? html`<li class="text-slate-400 py-2">Belum ada tugas.</li>`
112
+ : todos().map((t) => html`<todo-item .todo=${t}></todo-item>`)}
113
+ </ul>
114
+
115
+ <div class="mt-4 text-sm">
116
+ <a data-link href="/about" class="text-blue-600 hover:underline"
117
+ >Tentang &rarr;</a
118
+ >
119
+ </div>
120
+ </div>
121
+ `;
122
+ });
123
+
124
+ // ── Halaman: about ──────────────────────────────────────────
125
+ component("about-page", () => {
126
+ return () => html`
127
+ <div class="max-w-md mx-auto mt-10 p-6 bg-white rounded-xl shadow">
128
+ <h1 class="text-2xl font-bold mb-2">Tentang</h1>
129
+ <p class="text-slate-600 mb-4">
130
+ Contoh kecil yang dibangun dengan Sanify: signal fine-grained,
131
+ Web Components Light DOM, persist + cross-tab sync, dan router.
132
+ </p>
133
+ <a data-link href="/" class="text-blue-600 hover:underline"
134
+ >&larr; Kembali</a
135
+ >
136
+ </div>
137
+ `;
138
+ });
139
+
140
+ // ── Root + router ───────────────────────────────────────────
141
+ component("app-root", () => {
142
+ const view = router({
143
+ "/": () => html`<todo-page></todo-page>`,
144
+ "/about": () => html`<about-page></about-page>`,
145
+ "*": () => html`
146
+ <div class="max-w-md mx-auto mt-10 text-center">
147
+ <p class="text-slate-500">Halaman tidak ditemukan.</p>
148
+ <a data-link href="/" class="text-blue-600 hover:underline">Beranda</a>
149
+ </div>
150
+ `,
151
+ });
152
+ return () => html`<div>${view}</div>`;
153
+ });
154
+
155
+ // hindari unused-import error untuk navigate (tersedia utk pemakaian manual)
156
+ void navigate;
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
4
+ "target": "ESNext",
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "moduleDetection": "force",
8
+ "verbatimModuleSyntax": true,
9
+ "noEmit": true,
10
+
11
+ "strict": true,
12
+ "noUnusedLocals": true,
13
+ "noUnusedParameters": true,
14
+ "noFallthroughCasesInSwitch": true,
15
+ "noUncheckedIndexedAccess": true,
16
+ "skipLibCheck": true,
17
+
18
+ "types": ["bun"]
19
+ },
20
+ "include": ["src", "dev-server.ts"]
21
+ }