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 +76 -0
- package/package.json +19 -0
- package/templates/default/README.md +27 -0
- package/templates/default/_gitignore +2 -0
- package/templates/default/dev-server.ts +42 -0
- package/templates/default/index.html +13 -0
- package/templates/default/package.json +21 -0
- package/templates/default/src/main.ts +156 -0
- package/templates/default/tsconfig.json +21 -0
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,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 →</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
|
+
>← 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
|
+
}
|