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 +85 -0
- package/bin/create-zeropress-theme.js +7 -0
- package/package.json +17 -0
- package/src/index.js +188 -0
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
|
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
|
+
}
|