@nera-static/nera 1.4.1 → 1.5.1
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 +6 -1
- package/bin/nera.js +1 -0
- package/package.json +1 -1
- package/src/scaffold.js +72 -6
package/README.md
CHANGED
|
@@ -14,11 +14,16 @@ cd my-site
|
|
|
14
14
|
npm run dev # or: nera dev
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
Already in the folder the site should live in? `nera new .` scaffolds into the
|
|
18
|
+
current folder instead. The folder must be empty apart from dotfiles such as
|
|
19
|
+
`.git` (an existing `.gitignore` is kept), and the package is named after the
|
|
20
|
+
folder (`My Bakery` → `my-bakery`).
|
|
21
|
+
|
|
17
22
|
## Commands
|
|
18
23
|
|
|
19
24
|
| Command | What it does |
|
|
20
25
|
|---|---|
|
|
21
|
-
| `nera new <name>` | Scaffold a new thin Nera site (one dependency: `@nera-static/nera`). Add `--theme <name>` to start from a theme. |
|
|
26
|
+
| `nera new <name>` | Scaffold a new thin Nera site (one dependency: `@nera-static/nera`). Add `--theme <name>` to start from a theme. `nera new .` scaffolds into the current, empty folder. |
|
|
22
27
|
| `nera build` | Render `pages/` → `public/`. With `--check`, then run `nera check` on the result — one command for CI. |
|
|
23
28
|
| `nera dev` | Build, serve `public/`, and rebuild on change with live reload. Watches `pages/`, `config/`, `theme/` and a local theme (`theme: ./themes/<name>`). |
|
|
24
29
|
| `nera serve` | Serve the already-built `public/` folder. |
|
package/bin/nera.js
CHANGED
|
@@ -13,6 +13,7 @@ const usage = `Nera — the static site generator CLI
|
|
|
13
13
|
|
|
14
14
|
Usage:
|
|
15
15
|
nera new <name> scaffold a new Nera site (--theme <name> to start from a theme)
|
|
16
|
+
nera new . scaffold into the current, empty folder
|
|
16
17
|
nera build render pages/ → public/ (--check to check the output after)
|
|
17
18
|
nera dev build, serve and live-reload
|
|
18
19
|
nera serve serve the built public/ folder
|
package/package.json
CHANGED
package/src/scaffold.js
CHANGED
|
@@ -12,7 +12,8 @@ const VALID_PROJECT_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/
|
|
|
12
12
|
export function validateProjectName(projectName) {
|
|
13
13
|
if (typeof projectName !== 'string' || projectName.trim() === '') {
|
|
14
14
|
throw new Error(
|
|
15
|
-
'Project name is required. Usage: nera new <project-name>'
|
|
15
|
+
'Project name is required. Usage: nera new <project-name> ' +
|
|
16
|
+
'(or `nera new .` for the current folder)'
|
|
16
17
|
)
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -26,6 +27,55 @@ export function validateProjectName(projectName) {
|
|
|
26
27
|
return projectName
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
// `nera new .` scaffolds into the current folder. Only these two spellings:
|
|
31
|
+
// `..`, absolute paths and every other name still go through
|
|
32
|
+
// validateProjectName unchanged.
|
|
33
|
+
const CURRENT_DIR = new Set(['.', './'])
|
|
34
|
+
|
|
35
|
+
export const isCurrentDir = (projectName) => CURRENT_DIR.has(projectName)
|
|
36
|
+
|
|
37
|
+
// Letters NFKD does not decompose into a base letter plus an accent.
|
|
38
|
+
const LETTER_FOLDS = { ß: 'ss', æ: 'ae', œ: 'oe', ø: 'o', ł: 'l', đ: 'd' }
|
|
39
|
+
|
|
40
|
+
// npm refuses these names outright, and longer ones than this.
|
|
41
|
+
const NPM_RESERVED = new Set(['node_modules', 'favicon.ico'])
|
|
42
|
+
const NPM_MAX_LENGTH = 214
|
|
43
|
+
|
|
44
|
+
// The package name for a site scaffolded in place, derived from the folder's
|
|
45
|
+
// basename and normalised until it passes validateProjectName and npm's own
|
|
46
|
+
// rules: accents dropped (`Bäckerei` → `backerei`, `Straße` → `strasse`),
|
|
47
|
+
// lower-case, each run of other characters → one `-`, no leading dot, dash or
|
|
48
|
+
// underscore, at most 214 characters. `nera-site` when nothing usable is left.
|
|
49
|
+
export function projectNameFromDir(dir) {
|
|
50
|
+
const name = path
|
|
51
|
+
.basename(path.resolve(dir))
|
|
52
|
+
.toLowerCase()
|
|
53
|
+
.replace(/[ßæœøłđ]/g, (c) => LETTER_FOLDS[c])
|
|
54
|
+
.normalize('NFKD')
|
|
55
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
56
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
57
|
+
.replace(/^[._-]+/, '')
|
|
58
|
+
.slice(0, NPM_MAX_LENGTH)
|
|
59
|
+
.replace(/[-.]+$/, '')
|
|
60
|
+
return name === '' || NPM_RESERVED.has(name)
|
|
61
|
+
? 'nera-site'
|
|
62
|
+
: validateProjectName(name)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// In-place scaffolding only goes ahead in an empty folder. Dotfiles such as
|
|
66
|
+
// `.git` or an editor folder do not count: `git init` first is the usual way
|
|
67
|
+
// to start a project.
|
|
68
|
+
async function assertEmptyDir(dir) {
|
|
69
|
+
const entries = (await fs.readdir(dir)).filter((n) => !n.startsWith('.'))
|
|
70
|
+
if (entries.length > 0) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`The current folder is not empty (${entries.slice(0, 3).join(', ')}` +
|
|
73
|
+
`${entries.length > 3 ? ', …' : ''}). Run \`nera new .\` in an ` +
|
|
74
|
+
'empty folder, or `nera new <name>` to create a new one.'
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
29
79
|
// What `--theme` accepts, mirroring the three forms `app.theme` takes in
|
|
30
80
|
// @nera-static/core (src/theme.js): a bare name (`example` →
|
|
31
81
|
// @nera-static/theme-example), a full package name (`@acme/my-theme`), or a
|
|
@@ -85,6 +135,10 @@ async function copyDir(src, dest, { skipStarters = false } = {}) {
|
|
|
85
135
|
await copyDir(from, to, { skipStarters })
|
|
86
136
|
} else if (skipStarters && (await isStarterTemplate(from))) {
|
|
87
137
|
continue
|
|
138
|
+
} else if (fssync.existsSync(to)) {
|
|
139
|
+
// Only in place, and then only a dotfile such as .gitignore can be
|
|
140
|
+
// there already (assertEmptyDir): the user's copy wins.
|
|
141
|
+
console.log(` • Kept your existing ${path.basename(to)}`)
|
|
88
142
|
} else {
|
|
89
143
|
await fs.copyFile(from, to)
|
|
90
144
|
}
|
|
@@ -126,17 +180,21 @@ async function applyTheme(targetDir, theme) {
|
|
|
126
180
|
export async function scaffoldProject(projectName, options = {}) {
|
|
127
181
|
const { install = true, cwd = process.cwd(), theme } = options
|
|
128
182
|
|
|
129
|
-
|
|
183
|
+
const inPlace = isCurrentDir(projectName)
|
|
184
|
+
if (!inPlace) validateProjectName(projectName)
|
|
130
185
|
if (theme !== undefined) validateThemeSpec(theme)
|
|
131
186
|
|
|
132
|
-
const targetDir = path.resolve(cwd, projectName)
|
|
133
|
-
if (
|
|
187
|
+
const targetDir = path.resolve(cwd, inPlace ? '.' : projectName)
|
|
188
|
+
if (inPlace) {
|
|
189
|
+
await assertEmptyDir(targetDir)
|
|
190
|
+
} else if (fssync.existsSync(targetDir)) {
|
|
134
191
|
throw new Error(`Target directory "${projectName}" already exists.`)
|
|
135
192
|
}
|
|
193
|
+
const packageName = inPlace ? projectNameFromDir(targetDir) : projectName
|
|
136
194
|
|
|
137
195
|
console.log(`📦 Creating a new Nera site in ${targetDir}...`)
|
|
138
196
|
await copyDir(templateDir(), targetDir, { skipStarters: Boolean(theme) })
|
|
139
|
-
await personalize(targetDir,
|
|
197
|
+
await personalize(targetDir, packageName)
|
|
140
198
|
if (theme) await applyTheme(targetDir, theme)
|
|
141
199
|
|
|
142
200
|
if (install) {
|
|
@@ -149,7 +207,15 @@ export async function scaffoldProject(projectName, options = {}) {
|
|
|
149
207
|
execFileSync('npm', args, { cwd: targetDir, stdio: 'inherit' })
|
|
150
208
|
}
|
|
151
209
|
|
|
210
|
+
// `npm run dev`, not `nera dev`: it works without a global `nera`.
|
|
211
|
+
const nextSteps = [
|
|
212
|
+
...(inPlace ? [] : [`cd ${projectName}`]),
|
|
213
|
+
...(install ? [] : ['npm install']),
|
|
214
|
+
'npm run dev',
|
|
215
|
+
]
|
|
152
216
|
console.log('✅ Done!')
|
|
153
|
-
console.log(
|
|
217
|
+
console.log(
|
|
218
|
+
`👉 Next steps:\n${nextSteps.map((l) => ` ${l}\n`).join('')}`
|
|
219
|
+
)
|
|
154
220
|
return targetDir
|
|
155
221
|
}
|