create-zoijs 0.1.4 → 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.
@@ -1,171 +1,197 @@
1
- #!/usr/bin/env node
2
- // create-zoijs — a tiny scaffolder for new Zoijs apps. No build step, no
3
- // dependencies. It copies a template, fills in the app name, and prints the
4
- // three commands to get going. That's the whole tool — read it top to bottom.
5
- //
6
- // Usage:
7
- // npm create zoijs@latest my-app (npm maps this to create-zoijs)
8
- // npx create-zoijs my-app --template basic
9
- //
10
- // The pure functions below are exported so they can be unit-tested; main() only
11
- // runs when this file is executed directly (not when imported).
12
-
13
- import fs from "node:fs";
14
- import path from "node:path";
15
- import readline from "node:readline";
16
- import { fileURLToPath, pathToFileURL } from "node:url";
17
-
18
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
-
20
- export const TEMPLATES = ["basic", "app", "typescript", "minimal", "library"];
21
- export const DEFAULT_TEMPLATE = "app";
22
- const TEMPLATES_DIR = path.join(__dirname, "..", "templates");
23
-
24
- // ---- argument parsing --------------------------------------------------------
25
-
26
- /** Parse argv into { name, template }. The first non-flag arg is the app name. */
27
- export function parseArgs(argv) {
28
- let name;
29
- let template = DEFAULT_TEMPLATE;
30
- for (let i = 0; i < argv.length; i++) {
31
- const a = argv[i];
32
- if (a === "--template" || a === "-t") template = argv[++i];
33
- else if (a.startsWith("--template=")) template = a.slice("--template=".length);
34
- else if (!a.startsWith("-") && name === undefined) name = a;
35
- }
36
- return { name, template };
37
- }
38
-
39
- // ---- naming ------------------------------------------------------------------
40
-
41
- /** A readable Title Case form of a project name: "task-manager" → "Task Manager". */
42
- export function toTitle(name) {
43
- return String(name)
44
- .replace(/[-_]+/g, " ")
45
- .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
46
- .trim()
47
- .split(/\s+/)
48
- .filter(Boolean)
49
- .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
50
- .join(" ");
51
- }
52
-
53
- /** Validate a project name. Returns { valid, problems[] }. Keeps it npm-safe. */
54
- export function validateName(name) {
55
- const problems = [];
56
- if (!name || !name.trim()) {
57
- problems.push("Project name cannot be empty.");
58
- return { valid: false, problems };
59
- }
60
- if (name.trim() !== name) problems.push("Project name cannot have leading or trailing spaces.");
61
- if (name.length > 214) problems.push("Project name must be 214 characters or fewer.");
62
- if (/^[._]/.test(name)) problems.push("Project name cannot start with a dot or underscore.");
63
- if (!/^[a-zA-Z0-9._-]+$/.test(name)) {
64
- problems.push("Project name may only contain letters, numbers, dashes, dots, and underscores.");
65
- }
66
- return { valid: problems.length === 0, problems };
67
- }
68
-
69
- // ---- filesystem --------------------------------------------------------------
70
-
71
- /** Throw if the target directory exists and already has files in it. */
72
- export function ensureTargetAvailable(targetDir) {
73
- if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
74
- throw new Error(`Directory "${path.basename(targetDir)}" already exists and is not empty.`);
75
- }
76
- }
77
-
78
- const TEXT_EXT = new Set([".js", ".ts", ".mjs", ".html", ".css", ".json", ".md", ".txt", ".svg"]);
79
- const isText = (file) => file === "_gitignore" || TEXT_EXT.has(path.extname(file));
80
-
81
- // npm strips/renames files and dirs that start with a dot from a published package,
82
- // so templates ship them with a "_" prefix and we restore the dot name on copy.
83
- const RENAME = { _gitignore: ".gitignore", _vscode: ".vscode" };
84
-
85
- function applyTokens(content, tokens) {
86
- return content.replaceAll("{{APP_NAME}}", tokens.APP_NAME).replaceAll("{{APP_TITLE}}", tokens.APP_TITLE);
87
- }
88
-
89
- function copyDir(srcDir, destDir, tokens, written) {
90
- fs.mkdirSync(destDir, { recursive: true });
91
- for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
92
- const srcPath = path.join(srcDir, entry.name);
93
- const destName = RENAME[entry.name] ?? entry.name; // restore dot-prefixed names
94
- const destPath = path.join(destDir, destName);
95
- if (entry.isDirectory()) {
96
- copyDir(srcPath, destPath, tokens, written);
97
- } else if (isText(entry.name)) {
98
- fs.writeFileSync(destPath, applyTokens(fs.readFileSync(srcPath, "utf8"), tokens));
99
- written.push(destPath);
100
- } else {
101
- fs.copyFileSync(srcPath, destPath);
102
- written.push(destPath);
103
- }
104
- }
105
- }
106
-
107
- /**
108
- * Copy a template into targetDir, substituting the app name. Pure FS work — the
109
- * caller decides the name/template/dirs, which makes this easy to test.
110
- */
111
- export function scaffold({ name, template = DEFAULT_TEMPLATE, targetDir, templatesDir = TEMPLATES_DIR }) {
112
- const tplDir = path.join(templatesDir, template);
113
- if (!TEMPLATES.includes(template) || !fs.existsSync(tplDir)) {
114
- throw new Error(`Unknown template "${template}". Available templates: ${TEMPLATES.join(", ")}.`);
115
- }
116
- const tokens = { APP_NAME: name.toLowerCase(), APP_TITLE: toTitle(name) };
117
- const written = [];
118
- copyDir(tplDir, targetDir, tokens, written);
119
- return { written, tokens };
120
- }
121
-
122
- // ---- the CLI -----------------------------------------------------------------
123
-
124
- function ask(question) {
125
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
126
- return new Promise((resolve) => rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); }));
127
- }
128
-
129
- const fail = (message) => { console.error(`\n✖ ${message}\n`); process.exit(1); };
130
-
131
- async function main(argv) {
132
- const { name: argName, template } = parseArgs(argv);
133
-
134
- let name = argName;
135
- if (!name) name = await ask("Project name: ");
136
-
137
- const { valid, problems } = validateName(name);
138
- if (!valid) fail(problems.join("\n "));
139
-
140
- const targetDir = path.resolve(process.cwd(), name);
141
- try {
142
- ensureTargetAvailable(targetDir);
143
- } catch (err) {
144
- fail(err.message);
145
- }
146
-
147
- let result;
148
- try {
149
- result = scaffold({ name, template, targetDir });
150
- } catch (err) {
151
- fail(err.message);
152
- }
153
-
154
- console.log(`\n✔ Created ${name} (${template} template) — ${result.written.length} files\n`);
155
- console.log("Next steps:");
156
- console.log(` cd ${name}`);
157
- console.log(" npm install");
158
- console.log(" npm run dev\n");
159
- console.log("Zoijs works without this tool, too — it's only a convenience. See https://zoijs.dev/start\n");
160
- }
161
-
162
- // Run only when executed directly (resolve symlinks so the npm bin shim works).
163
- function invokedDirectly() {
164
- try {
165
- return process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
166
- } catch {
167
- return process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url;
168
- }
169
- }
170
-
171
- if (invokedDirectly()) main(process.argv.slice(2));
1
+ #!/usr/bin/env node
2
+ // create-zoijs — a tiny scaffolder for new Zoijs apps. No build step, no
3
+ // dependencies. It copies a template, fills in the app name, and prints the
4
+ // three commands to get going. That's the whole tool — read it top to bottom.
5
+ //
6
+ // Usage:
7
+ // npm create zoijs@latest my-app (npm maps this to create-zoijs)
8
+ // npx create-zoijs my-app --template basic
9
+ //
10
+ // The pure functions below are exported so they can be unit-tested; main() only
11
+ // runs when this file is executed directly (not when imported).
12
+
13
+ import fs from "node:fs";
14
+ import path from "node:path";
15
+ import readline from "node:readline";
16
+ import { fileURLToPath, pathToFileURL } from "node:url";
17
+
18
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
+
20
+ export const TEMPLATES = ["basic", "app", "typescript", "minimal", "library"];
21
+ export const DEFAULT_TEMPLATE = "app";
22
+ const TEMPLATES_DIR = path.join(__dirname, "..", "templates");
23
+
24
+ // The one exact @zoijs/core version generated apps target, plus an integrity-protected
25
+ // jsDelivr import map for it (generated by scripts/cdn-importmap.mjs, verified at release).
26
+ // Templates never hard-code a Zoijs version: they use the tokens below.
27
+ const CORE_CDN = path.join(__dirname, "..", "core-cdn.json");
28
+ export function loadCore(file = CORE_CDN) {
29
+ const cdn = JSON.parse(fs.readFileSync(file, "utf8"));
30
+ return { version: cdn.packages["@zoijs/core"].version, imports: cdn.imports, integrity: cdn.integrity };
31
+ }
32
+
33
+ // ---- argument parsing --------------------------------------------------------
34
+
35
+ /** Parse argv into { name, template }. The first non-flag arg is the app name. */
36
+ export function parseArgs(argv) {
37
+ let name;
38
+ let template = DEFAULT_TEMPLATE;
39
+ for (let i = 0; i < argv.length; i++) {
40
+ const a = argv[i];
41
+ if (a === "--template" || a === "-t") template = argv[++i];
42
+ else if (a.startsWith("--template=")) template = a.slice("--template=".length);
43
+ else if (!a.startsWith("-") && name === undefined) name = a;
44
+ }
45
+ return { name, template };
46
+ }
47
+
48
+ // ---- naming ------------------------------------------------------------------
49
+
50
+ /** A readable Title Case form of a project name: "task-manager" → "Task Manager". */
51
+ export function toTitle(name) {
52
+ return String(name)
53
+ .replace(/[-_]+/g, " ")
54
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
55
+ .trim()
56
+ .split(/\s+/)
57
+ .filter(Boolean)
58
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
59
+ .join(" ");
60
+ }
61
+
62
+ /** Validate a project name. Returns { valid, problems[] }. Keeps it npm-safe. */
63
+ export function validateName(name) {
64
+ const problems = [];
65
+ if (!name || !name.trim()) {
66
+ problems.push("Project name cannot be empty.");
67
+ return { valid: false, problems };
68
+ }
69
+ if (name.trim() !== name) problems.push("Project name cannot have leading or trailing spaces.");
70
+ if (name.length > 214) problems.push("Project name must be 214 characters or fewer.");
71
+ if (/^[._]/.test(name)) problems.push("Project name cannot start with a dot or underscore.");
72
+ if (!/^[a-zA-Z0-9._-]+$/.test(name)) {
73
+ problems.push("Project name may only contain letters, numbers, dashes, dots, and underscores.");
74
+ }
75
+ return { valid: problems.length === 0, problems };
76
+ }
77
+
78
+ // ---- filesystem --------------------------------------------------------------
79
+
80
+ /** Throw if the target directory exists and already has files in it. */
81
+ export function ensureTargetAvailable(targetDir) {
82
+ if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
83
+ throw new Error(`Directory "${path.basename(targetDir)}" already exists and is not empty.`);
84
+ }
85
+ }
86
+
87
+ const TEXT_EXT = new Set([".js", ".ts", ".mjs", ".html", ".css", ".json", ".md", ".txt", ".svg"]);
88
+ const isText = (file) => file === "_gitignore" || TEXT_EXT.has(path.extname(file));
89
+
90
+ // npm strips/renames files and dirs that start with a dot from a published package,
91
+ // so templates ship them with a "_" prefix and we restore the dot name on copy.
92
+ const RENAME = { _gitignore: ".gitignore", _vscode: ".vscode" };
93
+
94
+ function applyTokens(content, tokens) {
95
+ return content.replace(/\{\{(APP_NAME|APP_TITLE|ZOIJS_CORE_VERSION|ZOIJS_CORE_CDN|ZOIJS_CORE_IMPORTMAP)\}\}/g, (_, key) => tokens[key]);
96
+ }
97
+
98
+ // The import map for an HTML template, indented to sit inside <script type="importmap">.
99
+ function importMapJson(core) {
100
+ return JSON.stringify({ imports: core.imports, integrity: core.integrity }, null, 2).replace(/\n/g, "\n ");
101
+ }
102
+
103
+ function copyDir(srcDir, destDir, tokens, written) {
104
+ fs.mkdirSync(destDir, { recursive: true });
105
+ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
106
+ const srcPath = path.join(srcDir, entry.name);
107
+ const destName = RENAME[entry.name] ?? entry.name; // restore dot-prefixed names
108
+ const destPath = path.join(destDir, destName);
109
+ if (entry.isDirectory()) {
110
+ copyDir(srcPath, destPath, tokens, written);
111
+ } else if (isText(entry.name)) {
112
+ fs.writeFileSync(destPath, applyTokens(fs.readFileSync(srcPath, "utf8"), tokens));
113
+ written.push(destPath);
114
+ } else {
115
+ fs.copyFileSync(srcPath, destPath);
116
+ written.push(destPath);
117
+ }
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Copy a template into targetDir, substituting the app name. Pure FS work — the
123
+ * caller decides the name/template/dirs, which makes this easy to test.
124
+ */
125
+ export function scaffold({ name, template = DEFAULT_TEMPLATE, targetDir, templatesDir = TEMPLATES_DIR, core = loadCore() }) {
126
+ const tplDir = path.join(templatesDir, template);
127
+ if (!TEMPLATES.includes(template) || !fs.existsSync(tplDir)) {
128
+ throw new Error(`Unknown template "${template}". Available templates: ${TEMPLATES.join(", ")}.`);
129
+ }
130
+ const tokens = {
131
+ APP_NAME: name.toLowerCase(),
132
+ APP_TITLE: toTitle(name),
133
+ ZOIJS_CORE_VERSION: core.version,
134
+ ZOIJS_CORE_CDN: `https://cdn.jsdelivr.net/npm/@zoijs/core@${core.version}/src/`,
135
+ ZOIJS_CORE_IMPORTMAP: importMapJson(core),
136
+ };
137
+ const written = [];
138
+ copyDir(tplDir, targetDir, tokens, written);
139
+ return { written, tokens };
140
+ }
141
+
142
+ /** The commands to print after scaffolding (minimal has nothing to install; library has no app to serve). */
143
+ export function nextSteps(name, template = DEFAULT_TEMPLATE) {
144
+ if (template === "minimal") return [`cd ${name}`, "npm run dev"];
145
+ if (template === "library") return [`cd ${name}`, "npm install", "npm test"];
146
+ return [`cd ${name}`, "npm install", "npm run dev"];
147
+ }
148
+
149
+ // ---- the CLI -----------------------------------------------------------------
150
+
151
+ function ask(question) {
152
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
153
+ return new Promise((resolve) => rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); }));
154
+ }
155
+
156
+ const fail = (message) => { console.error(`\n✖ ${message}\n`); process.exit(1); };
157
+
158
+ async function main(argv) {
159
+ const { name: argName, template } = parseArgs(argv);
160
+
161
+ let name = argName;
162
+ if (!name) name = await ask("Project name: ");
163
+
164
+ const { valid, problems } = validateName(name);
165
+ if (!valid) fail(problems.join("\n "));
166
+
167
+ const targetDir = path.resolve(process.cwd(), name);
168
+ try {
169
+ ensureTargetAvailable(targetDir);
170
+ } catch (err) {
171
+ fail(err.message);
172
+ }
173
+
174
+ let result;
175
+ try {
176
+ result = scaffold({ name, template, targetDir });
177
+ } catch (err) {
178
+ fail(err.message);
179
+ }
180
+
181
+ console.log(`\n✔ Created ${name} (${template} template) — ${result.written.length} files\n`);
182
+ console.log("Next steps:");
183
+ for (const step of nextSteps(name, template)) console.log(` ${step}`);
184
+ console.log("");
185
+ console.log("Zoijs works without this tool, too — it's only a convenience. See https://zoijs.dev/start\n");
186
+ }
187
+
188
+ // Run only when executed directly (resolve symlinks so the npm bin shim works).
189
+ function invokedDirectly() {
190
+ try {
191
+ return process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
192
+ } catch {
193
+ return process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url;
194
+ }
195
+ }
196
+
197
+ if (invokedDirectly()) main(process.argv.slice(2));
package/core-cdn.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "prod": false,
3
+ "imports": {
4
+ "@zoijs/core": "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/index.js"
5
+ },
6
+ "integrity": {
7
+ "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/core/boundary.js": "sha384-sgP+7Uk+gh1Gk39OqMxZEo9Mox6uM6upgCbk9n4HKa8L3LywliSiXjQPQrjj/OG/",
8
+ "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/core/brand.js": "sha384-SQKAavcGif93XT58P5unWneM/VMR1rPsGM/wF63kNN4YaR2RdK2mu2IwULo9VYix",
9
+ "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/core/each.js": "sha384-ple5ogdJi5exkzQbgaQ8jnLLOymkmnG8dG/B8L36QkDbmIOiuRL+IdNVNNLYh+3N",
10
+ "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/core/html.js": "sha384-5sS5ABTdRpfXmhwZuIyhqcSO7fCY1VFVlYcolWRfLsRVzytc9JnUkjfX2Adfy8WI",
11
+ "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/core/mount.js": "sha384-eAm8c9K1SY4j66NnZ4pc4exzlek7iG67di9hU5U9S3PnaZnElkqRGpzOy2z4Uz2h",
12
+ "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/core/renderer.js": "sha384-H2K6FI3+kv2iPxyLb8V9e9Ux4X95PcPQGYu/h7Gq9rk4brE1nLHKFvI4iG5vt7Uz",
13
+ "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/index.js": "sha384-dDKK+RVA9Pj7X7aJvv0zgepWNZQi9W20zZ1JgACZ4EbIQ9LjvLsss8gsQGiyvcRb",
14
+ "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/prod.js": "sha384-2Wg5h5XW9gULwwZPet1azoni63tKm9iTs9FrAoLV0vGUa77h5WPgqsG/F3TFPuBy",
15
+ "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/reactivity/computed.js": "sha384-nlAK86Jy1IR3yFSePE38l6cjZqcu5SdnYRVQxJh/+2YQL3WhTHdZ18yCKeKL4ffm",
16
+ "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/reactivity/core.js": "sha384-dBKT0ohkuylmZHrv0HAjRtTxrMNyo6FZTk8Fqf6LH+aAi3moXikSLGdlPaeZi5IV",
17
+ "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/reactivity/devtools.js": "sha384-KyEdZp4AcE9RdgocIKg0Q3NKTIyCUPnMi8LLPGrDDhRCJ55DA/JcqSZRFjoHHR5X",
18
+ "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/reactivity/effect.js": "sha384-EMCI2Kpw6k0EUn5QPC6tJLoON+nUgQ7L25AXgIrWLmhMCLKJq/jP+C6Bk/P6gGDY",
19
+ "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/reactivity/env.js": "sha384-41791FgWw6cq/pCRB6up+hZExngmUr1tAVxQG58SdvonImEcU6jK7g14jzZAPBFL",
20
+ "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/reactivity/owner.js": "sha384-r8/NbofI3PXPfKvjxBuhWcRZGMJQn+Ldbne0ghFkjELu6SjvpaWIHnxF8FuScUAp",
21
+ "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/reactivity/runtime.js": "sha384-5CQnBBrMFUfcq+ahlgE4DX/nG0ua+Bo0l70VtRkXVPWQyvGtXoSScup7FHsQqbA2",
22
+ "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/reactivity/state.js": "sha384-w2HVkiWmSdb8h7qqr4OViqHJrDqlD/oInr+5Lal+lSvXWu3SPuZv35WWDPsEOgUq",
23
+ "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/utils/dom.js": "sha384-XsdCmmxzHfpBmOdz1648tCRrAMMHbWKIzvm4Aj2VIEpWJDIIaKmihqEARvxN6d+2",
24
+ "https://cdn.jsdelivr.net/npm/@zoijs/core@1.9.0/src/utils/security.js": "sha384-7UafFG7FtgE/ZSVl+gEOKHc7qewaF0V6hKLEqgI9yj9mGTxz62GTzkrUPfbOXOtT"
25
+ },
26
+ "packages": {
27
+ "@zoijs/core": {
28
+ "version": "1.9.0",
29
+ "entries": [
30
+ "src/index.js",
31
+ "src/prod.js"
32
+ ],
33
+ "files": 18
34
+ }
35
+ }
36
+ }
package/package.json CHANGED
@@ -1,43 +1,44 @@
1
- {
2
- "name": "create-zoijs",
3
- "version": "0.1.4",
4
- "description": "Create a new Zoijs app with one command. Plain HTML, CSS, and JavaScript — no build step.",
5
- "type": "module",
6
- "bin": {
7
- "create-zoijs": "bin/create-zoijs.js"
8
- },
9
- "files": [
10
- "bin",
11
- "templates",
12
- "README.md",
13
- "CHANGELOG.md",
14
- "LICENSE"
15
- ],
16
- "engines": {
17
- "node": ">=18"
18
- },
19
- "author": "Zoijs contributors (https://zoijs.dev)",
20
- "license": "MIT",
21
- "homepage": "https://zoijs.dev",
22
- "repository": {
23
- "type": "git",
24
- "url": "git+https://github.com/Zoijs/zoijs.git",
25
- "directory": "create"
26
- },
27
- "bugs": {
28
- "url": "https://github.com/Zoijs/zoijs/issues"
29
- },
30
- "keywords": [
31
- "zoijs",
32
- "create",
33
- "create-zoijs",
34
- "scaffold",
35
- "starter",
36
- "cli",
37
- "no-build",
38
- "no-jsx"
39
- ],
40
- "scripts": {
41
- "test": "node --test tests/create.test.js"
42
- }
43
- }
1
+ {
2
+ "name": "create-zoijs",
3
+ "version": "0.2.0",
4
+ "description": "Create a new Zoijs app with one command. Plain HTML, CSS, and JavaScript — no build step.",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-zoijs": "bin/create-zoijs.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "core-cdn.json",
12
+ "templates",
13
+ "README.md",
14
+ "CHANGELOG.md",
15
+ "LICENSE"
16
+ ],
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "author": "Zoijs contributors (https://zoijs.dev)",
21
+ "license": "MIT",
22
+ "homepage": "https://zoijs.dev",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/Zoijs/zoijs.git",
26
+ "directory": "create"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/Zoijs/zoijs/issues"
30
+ },
31
+ "keywords": [
32
+ "zoijs",
33
+ "create",
34
+ "create-zoijs",
35
+ "scaffold",
36
+ "starter",
37
+ "cli",
38
+ "no-build",
39
+ "no-jsx"
40
+ ],
41
+ "scripts": {
42
+ "test": "node --test tests/create.test.js tests/dev-server.test.js tests/versions.test.js"
43
+ }
44
+ }
@@ -1,48 +1,68 @@
1
- # {{APP_TITLE}}
2
-
3
- A small **project dashboard** built with [Zoijs](https://zoijs.dev) — plain HTML,
4
- CSS, and JavaScript. **No build step.**
5
-
6
- ## Develop
7
-
8
- ```bash
9
- npm install
10
- npm run dev
11
- ```
12
-
13
- ```text
14
- Zoijs dev server
15
-
16
- - Local: http://localhost:7310
17
- ```
18
-
19
- Open the printed URL (it falls back to 7311–7313 if the port is busy).
20
-
21
- ## Project layout
22
-
23
- ```
24
- {{APP_NAME}}/
25
- index.html import map + #app mount point
26
- dev-server.mjs tiny zero-dependency static server (npm run dev)
27
- src/
28
- app.js owns all state; composes the dashboard
29
- style.css plain CSS (light/dark, responsive)
30
- components/
31
- Header.js parent → child: hero + summary stats
32
- StatCard.js parent → child: a reusable stat card (used ×4)
33
- TaskItem.js child → parent: reports toggle/delete via callbacks
34
- ```
35
-
36
- ## What it shows
37
-
38
- - **`createState`** — the task list, the active filter, and the input draft.
39
- - **`computed`** — total / completed / active counts and the filtered list.
40
- - **`each`** — keyed rendering of the visible tasks.
41
- - **Conditional rendering** — the empty state and the active-filter highlight.
42
- - **Component composition** — `Header`, `StatCard` (×4), and `TaskItem`.
43
- - **Parent → child** — `App` passes data down to its components as arguments.
44
- - **Child → parent** — `TaskItem` reports events up through callbacks; `App`
45
- owns the state and performs the changes.
46
-
47
- No bundler, no build step. Open the files and read them. Learn more at
48
- [zoijs.dev](https://zoijs.dev).
1
+ # {{APP_TITLE}}
2
+
3
+ A small **project dashboard** built with [Zoijs](https://zoijs.dev) — plain HTML,
4
+ CSS, and JavaScript. **No build step.**
5
+
6
+ ## Develop
7
+
8
+ ```bash
9
+ npm install
10
+ npm run dev
11
+ ```
12
+
13
+ ```text
14
+ Zoijs dev server
15
+
16
+ - Local: http://localhost:7310
17
+ ```
18
+
19
+ Open the printed URL (it falls back to 7311–7313 if the port is busy).
20
+
21
+ The dev server is for **development only**: it listens on `127.0.0.1` (this machine),
22
+ never serves dotfiles (`.env`, `.git/`, …) or anything outside the project folder, and is
23
+ not meant for hosting. To try the app from a phone on your network, opt in explicitly with
24
+ `ZOIJS_HOST=0.0.0.0 npm run dev` — while it runs, anyone on that network can read the
25
+ project's files.
26
+
27
+ ## Deploy
28
+
29
+ This page loads the **development** entry of `@zoijs/core` (helpful warnings, devtools).
30
+ When you deploy, change the import map in `index.html` to the **production** entry —
31
+ same API, warnings and the devtools hook off:
32
+
33
+ ```html
34
+ "@zoijs/core": "./node_modules/@zoijs/core/src/prod.js"
35
+ ```
36
+
37
+ Then deploy the files to a static host (not the dev server) and go through the
38
+ [production security checklist](https://zoijs.dev/production-security) — security headers,
39
+ a Content-Security-Policy with your import map's hash, CSRF, and more.
40
+
41
+ ## Project layout
42
+
43
+ ```
44
+ {{APP_NAME}}/
45
+ index.html import map + #app mount point
46
+ dev-server.mjs tiny zero-dependency static server (npm run dev)
47
+ src/
48
+ app.js owns all state; composes the dashboard
49
+ style.css plain CSS (light/dark, responsive)
50
+ components/
51
+ Header.js parent → child: hero + summary stats
52
+ StatCard.js parent → child: a reusable stat card (used ×4)
53
+ TaskItem.js child → parent: reports toggle/delete via callbacks
54
+ ```
55
+
56
+ ## What it shows
57
+
58
+ - **`createState`** — the task list, the active filter, and the input draft.
59
+ - **`computed`** — total / completed / active counts and the filtered list.
60
+ - **`each`** — keyed rendering of the visible tasks.
61
+ - **Conditional rendering** — the empty state and the active-filter highlight.
62
+ - **Component composition** — `Header`, `StatCard` (×4), and `TaskItem`.
63
+ - **Parent → child** — `App` passes data down to its components as arguments.
64
+ - **Child → parent** — `TaskItem` reports events up through callbacks; `App`
65
+ owns the state and performs the changes.
66
+
67
+ No bundler, no build step. Open the files and read them. Learn more at
68
+ [zoijs.dev](https://zoijs.dev).
@@ -1 +1 @@
1
- node_modules
1
+ node_modules