@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nera-static/nera",
3
- "version": "1.4.1",
3
+ "version": "1.5.1",
4
4
  "description": "The Nera CLI — scaffold, build, preview and update a Nera static site with one command over the @nera-static/core engine",
5
5
  "type": "module",
6
6
  "bin": {
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
- validateProjectName(projectName)
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 (fssync.existsSync(targetDir)) {
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, projectName)
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(`👉 Next steps:\n cd ${projectName}\n nera dev\n`)
217
+ console.log(
218
+ `👉 Next steps:\n${nextSteps.map((l) => ` ${l}\n`).join('')}`
219
+ )
154
220
  return targetDir
155
221
  }