create-zeropress-theme 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/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # create-zeropress-theme
2
+
3
+ ZeroPress 테마 프로젝트를 생성하는 scaffolding CLI입니다.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npx create-zeropress-theme my-theme
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ npx create-zeropress-theme <name> [options]
15
+ ```
16
+
17
+ ### Options
18
+
19
+ | Option | Description | Default |
20
+ |--------|-------------|---------|
21
+ | `--template <name>` | 템플릿 선택 (`minimal`, `blog`, `magazine`) | `minimal` |
22
+ | `--with-devtools` | 개발 편의 `package.json` 포함 | - |
23
+
24
+ ### Examples
25
+
26
+ ```bash
27
+ # 기본 테마 생성
28
+ npx create-zeropress-theme my-theme
29
+
30
+ # 템플릿 지정
31
+ npx create-zeropress-theme my-theme --template blog
32
+
33
+ # devtools 포함
34
+ npx create-zeropress-theme my-theme --with-devtools
35
+ ```
36
+
37
+ > 현재 v0.1에서는 모든 템플릿이 동일한 starter 구조를 생성합니다. 향후 버전에서 차등 제공 예정입니다.
38
+
39
+ ## 생성되는 구조
40
+
41
+ ```
42
+ my-theme/
43
+ theme.json
44
+ layout.html
45
+ index.html
46
+ post.html
47
+ page.html
48
+ archive.html
49
+ category.html
50
+ tag.html
51
+ 404.html
52
+ partials/
53
+ header.html
54
+ footer.html
55
+ assets/
56
+ style.css
57
+ ```
58
+
59
+ ## --with-devtools
60
+
61
+ `--with-devtools` 옵션을 사용하면 테마 폴더에 `package.json`이 추가됩니다.
62
+
63
+ ```bash
64
+ npx create-zeropress-theme my-theme --with-devtools
65
+ cd my-theme
66
+ npm run dev # 프리뷰 서버
67
+ npm run validate # 테마 검증
68
+ npm run pack # zip 패키징
69
+ ```
70
+
71
+ 내부적으로 `zeropress-theme` CLI를 호출하는 thin wrapper이며, 별도 의존성이나 lockfile은 생성하지 않습니다. 테마 런타임 스펙 자체를 변경하지 않으므로 업로드 패키지는 순수 테마 파일 기준으로 검증됩니다.
72
+
73
+ ## Requirements
74
+
75
+ - Node.js >= 18.18.0
76
+ - ESM only
77
+
78
+ ## Related
79
+
80
+ - [zeropress-theme](https://www.npmjs.com/package/zeropress-theme) — 테마 개발/검증/패키징 toolkit
81
+ - [ZeroPress Theme Spec](https://github.com/user/zeropress/blob/main/theme_guide_v2/THEME_SPEC.md)
82
+
83
+ ## License
84
+
85
+ MIT
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { run } from '../src/index.js';
3
+
4
+ run(process.argv.slice(2)).catch((error) => {
5
+ console.error(`[create-zeropress-theme] ${error.message}`);
6
+ process.exit(1);
7
+ });
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "create-zeropress-theme",
3
+ "version": "0.1.0",
4
+ "description": "ZeroPress theme scaffolding CLI",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "create-zeropress-theme": "./bin/create-zeropress-theme.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "src"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18.18.0"
16
+ }
17
+ }
package/src/index.js ADDED
@@ -0,0 +1,188 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ const TEMPLATES = new Set(['minimal', 'blog', 'magazine']);
5
+
6
+ export async function run(argv) {
7
+ const { name, template, withDevtools } = parseArgs(argv);
8
+ const targetDir = path.resolve(process.cwd(), name);
9
+
10
+ await ensureEmptyDirectory(targetDir);
11
+ await scaffoldTheme(targetDir, name);
12
+
13
+ if (withDevtools) {
14
+ await writeDevtoolsPackageJson(targetDir);
15
+ }
16
+
17
+ console.log(`Created ZeroPress theme at ${targetDir}`);
18
+ console.log(`Template: ${template} (current v0.1 behavior uses same starter files for all templates)`);
19
+ if (withDevtools) {
20
+ console.log('Devtools enabled: npm run dev / npm run validate / npm run pack');
21
+ }
22
+ }
23
+
24
+ function parseArgs(argv) {
25
+ if (argv.length === 0) {
26
+ throw new Error('Usage: create-zeropress-theme <name> [--template <minimal|blog|magazine>] [--with-devtools]');
27
+ }
28
+
29
+ const positional = [];
30
+ let template = 'minimal';
31
+ let withDevtools = false;
32
+
33
+ for (let i = 0; i < argv.length; i += 1) {
34
+ const arg = argv[i];
35
+ if (!arg.startsWith('--')) {
36
+ positional.push(arg);
37
+ continue;
38
+ }
39
+
40
+ if (arg === '--with-devtools') {
41
+ withDevtools = true;
42
+ continue;
43
+ }
44
+
45
+ if (arg === '--template') {
46
+ const value = argv[i + 1];
47
+ if (!value) {
48
+ throw new Error('--template requires a value');
49
+ }
50
+ if (!TEMPLATES.has(value)) {
51
+ throw new Error(`Invalid template "${value}". Allowed: minimal, blog, magazine`);
52
+ }
53
+ template = value;
54
+ i += 1;
55
+ continue;
56
+ }
57
+
58
+ throw new Error(`Unknown option: ${arg}`);
59
+ }
60
+
61
+ if (positional.length !== 1) {
62
+ throw new Error('Expected exactly one theme directory name');
63
+ }
64
+
65
+ const name = positional[0];
66
+ if (!name || name.includes('..') || path.isAbsolute(name)) {
67
+ throw new Error('Theme name must be a relative directory name');
68
+ }
69
+
70
+ return { name, template, withDevtools };
71
+ }
72
+
73
+ async function ensureEmptyDirectory(targetDir) {
74
+ try {
75
+ const stat = await fs.stat(targetDir);
76
+ if (!stat.isDirectory()) {
77
+ throw new Error(`Path exists and is not a directory: ${targetDir}`);
78
+ }
79
+ const entries = await fs.readdir(targetDir);
80
+ if (entries.length > 0) {
81
+ throw new Error(`Directory is not empty: ${targetDir}`);
82
+ }
83
+ } catch (error) {
84
+ if (error.code === 'ENOENT') {
85
+ await fs.mkdir(targetDir, { recursive: true });
86
+ return;
87
+ }
88
+ throw error;
89
+ }
90
+ }
91
+
92
+ async function scaffoldTheme(targetDir, name) {
93
+ await fs.mkdir(path.join(targetDir, 'partials'), { recursive: true });
94
+ await fs.mkdir(path.join(targetDir, 'assets'), { recursive: true });
95
+
96
+ const files = {
97
+ 'theme.json': JSON.stringify(
98
+ {
99
+ name,
100
+ version: '0.1.0',
101
+ author: 'Author Name',
102
+ description: 'ZeroPress theme',
103
+ },
104
+ null,
105
+ 2
106
+ ) + '\n',
107
+ 'layout.html': `<!doctype html>
108
+ <html lang="en">
109
+ <head>
110
+ <meta charset="utf-8" />
111
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
112
+ <title>{{site.title}}</title>
113
+ <meta name="description" content="{{site.description}}" />
114
+ <link rel="stylesheet" href="/assets/style.css" />
115
+ </head>
116
+ <body>
117
+ <header>{{slot:header}}</header>
118
+ <main>{{slot:content}}</main>
119
+ <footer>{{slot:footer}}</footer>
120
+ </body>
121
+ </html>
122
+ `,
123
+ 'index.html': `<section>
124
+ <h1>{{site.title}}</h1>
125
+ <p>{{site.description}}</p>
126
+ <div>{{posts}}</div>
127
+ <nav>{{pagination}}</nav>
128
+ </section>
129
+ `,
130
+ 'post.html': `<article>
131
+ <h1>{{post.title}}</h1>
132
+ <div>{{post.html}}</div>
133
+ </article>
134
+ `,
135
+ 'page.html': `<article>
136
+ <h1>{{page.title}}</h1>
137
+ <div>{{page.html}}</div>
138
+ </article>
139
+ `,
140
+ 'archive.html': `<section>
141
+ <h1>Archive</h1>
142
+ <div>{{posts}}</div>
143
+ </section>
144
+ `,
145
+ 'category.html': `<section>
146
+ <h1>Category</h1>
147
+ <div>{{posts}}</div>
148
+ </section>
149
+ `,
150
+ 'tag.html': `<section>
151
+ <h1>Tag</h1>
152
+ <div>{{posts}}</div>
153
+ </section>
154
+ `,
155
+ '404.html': `<section>
156
+ <h1>404</h1>
157
+ <p>Not Found</p>
158
+ </section>
159
+ `,
160
+ 'partials/header.html': '<a href="/">Home</a>\n',
161
+ 'partials/footer.html': '<small>Powered by ZeroPress</small>\n',
162
+ 'assets/style.css': `:root { font-family: system-ui, sans-serif; }
163
+ body { margin: 0; padding: 2rem; line-height: 1.5; }
164
+ h1 { margin-top: 0; }
165
+ `,
166
+ };
167
+
168
+ await Promise.all(
169
+ Object.entries(files).map(([filePath, content]) =>
170
+ fs.writeFile(path.join(targetDir, filePath), content, 'utf8')
171
+ )
172
+ );
173
+ }
174
+
175
+ async function writeDevtoolsPackageJson(targetDir) {
176
+ const pkg = {
177
+ name: path.basename(targetDir),
178
+ private: true,
179
+ version: '0.1.0',
180
+ type: 'module',
181
+ scripts: {
182
+ dev: 'zeropress-theme dev',
183
+ validate: 'zeropress-theme validate',
184
+ pack: 'zeropress-theme pack',
185
+ },
186
+ };
187
+ await fs.writeFile(path.join(targetDir, 'package.json'), `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
188
+ }