@nera-static/nera 1.0.0 → 1.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 +35 -2
- package/bin/nera.js +1 -1
- package/package.json +3 -3
- package/src/commands/new.js +19 -3
- package/src/scaffold.js +78 -5
- package/template/theme/views/layouts/layout.pug +1 -0
- package/template/theme/views/pages/default.pug +1 -0
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ The Nera CLI — scaffold, build, preview and update a [Nera](https://github.com
|
|
|
4
4
|
static site with a single command. It runs over the [`@nera-static/core`](https://github.com/seebaermichi/nera)
|
|
5
5
|
engine and is the one package a Nera site depends on.
|
|
6
6
|
|
|
7
|
+
📖 **Documentation:** [nera.js.org](https://nera.js.org)
|
|
8
|
+
|
|
7
9
|
## Quick start
|
|
8
10
|
|
|
9
11
|
```bash
|
|
@@ -16,11 +18,12 @@ npm run dev # or: nera dev
|
|
|
16
18
|
|
|
17
19
|
| Command | What it does |
|
|
18
20
|
|---|---|
|
|
19
|
-
| `nera new <name>` | Scaffold a new thin Nera site (one dependency: `@nera-static/nera`). |
|
|
21
|
+
| `nera new <name>` | Scaffold a new thin Nera site (one dependency: `@nera-static/nera`). Add `--theme <name>` to start from a theme. |
|
|
20
22
|
| `nera build` | Render `pages/` → `public/`. |
|
|
21
23
|
| `nera dev` | Build, serve `public/`, and rebuild on change with live reload. |
|
|
22
24
|
| `nera serve` | Serve the already-built `public/` folder. |
|
|
23
25
|
| `nera update` | Update the site's Nera packages. On a legacy cloned site, `nera update --migrate` converts it to the thin model. |
|
|
26
|
+
| `nera validate` | Check the site (layouts, includes, YAML) before publishing; exits non-zero on any error. |
|
|
24
27
|
|
|
25
28
|
A scaffolded site is a thin project — no vendored engine, no clone. It lists one
|
|
26
29
|
dependency and its scripts call the CLI:
|
|
@@ -32,6 +35,26 @@ dependency and its scripts call the CLI:
|
|
|
32
35
|
}
|
|
33
36
|
```
|
|
34
37
|
|
|
38
|
+
## Starting from a theme
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx @nera-static/nera new my-site --theme example
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This installs the theme (`example` → `@nera-static/theme-example`; a full
|
|
45
|
+
package name such as `@acme/my-theme`, or a local path such as `./my-theme`,
|
|
46
|
+
works too), sets `theme:` in `config/app.yaml`, and leaves out the scaffold's
|
|
47
|
+
starter templates.
|
|
48
|
+
|
|
49
|
+
That last part matters. A site's own `theme/views/` wins over its theme's views
|
|
50
|
+
**file by file** — that is how you override a single theme template. Without
|
|
51
|
+
`--theme`, `nera new` writes a starter `theme/views/layouts/layout.pug` and
|
|
52
|
+
`theme/views/pages/default.pug`, the same names a theme uses, so a theme added
|
|
53
|
+
later stays hidden behind them. Each starter file carries a
|
|
54
|
+
`//- nera:scaffold-default` first line, and `nera validate` warns
|
|
55
|
+
(`theme-shadowed`) while such a file hides a theme's. Delete it to use the
|
|
56
|
+
theme's version, or delete the marker line to keep yours.
|
|
57
|
+
|
|
35
58
|
## Migrating a cloned (legacy) site
|
|
36
59
|
|
|
37
60
|
Older Nera sites were git clones that vendored the engine under `src/`. Inside
|
|
@@ -43,7 +66,17 @@ nera update --migrate
|
|
|
43
66
|
|
|
44
67
|
It adds `@nera-static/nera`, rewrites the scripts, removes the vendored `src/`
|
|
45
68
|
engine and root `index.js`, and installs — leaving your `pages/`, `config/` and
|
|
46
|
-
`theme/` untouched. Local plugins in `src/plugins/` are moved to `plugins
|
|
69
|
+
`theme/` untouched. Local plugins in `src/plugins/` are moved to `plugins/`;
|
|
70
|
+
add this to `config/app.yaml` so the engine discovers them there:
|
|
71
|
+
|
|
72
|
+
```yaml
|
|
73
|
+
folders:
|
|
74
|
+
plugins: ./plugins
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
A root `views/` and `assets/` (no `theme/` folder) keeps rendering but prints a
|
|
78
|
+
deprecation warning — move them to `theme/views/` and `theme/assets/`, the
|
|
79
|
+
layout `nera new` scaffolds.
|
|
47
80
|
|
|
48
81
|
## Requirements
|
|
49
82
|
|
package/bin/nera.js
CHANGED
|
@@ -11,7 +11,7 @@ const [, , command, ...rest] = process.argv
|
|
|
11
11
|
const usage = `Nera — the static site generator CLI
|
|
12
12
|
|
|
13
13
|
Usage:
|
|
14
|
-
nera new <name> scaffold a new Nera site
|
|
14
|
+
nera new <name> scaffold a new Nera site (--theme <name> to start from a theme)
|
|
15
15
|
nera build render pages/ → public/
|
|
16
16
|
nera dev build, serve and live-reload
|
|
17
17
|
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.
|
|
3
|
+
"version": "1.1.0",
|
|
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": {
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"vite": "^8.1.5"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"eslint": "^
|
|
52
|
+
"eslint": "^10.8.0",
|
|
53
53
|
"husky": "^9.1.7",
|
|
54
|
-
"vitest": "^
|
|
54
|
+
"vitest": "^5.0.3"
|
|
55
55
|
}
|
|
56
56
|
}
|
package/src/commands/new.js
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
import { scaffoldProject } from '../scaffold.js'
|
|
2
2
|
|
|
3
|
+
// `nera new <name> [--theme <spec> | --theme=<spec>] [--no-install]`
|
|
4
|
+
export function parseNewArgs(args = []) {
|
|
5
|
+
let projectName
|
|
6
|
+
let theme
|
|
7
|
+
for (let i = 0; i < args.length; i++) {
|
|
8
|
+
const arg = args[i]
|
|
9
|
+
if (arg === '--theme') {
|
|
10
|
+
theme = args[++i] ?? ''
|
|
11
|
+
} else if (arg.startsWith('--theme=')) {
|
|
12
|
+
theme = arg.slice('--theme='.length)
|
|
13
|
+
} else if (!arg.startsWith('-') && projectName === undefined) {
|
|
14
|
+
projectName = arg
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return { projectName, theme, install: !args.includes('--no-install') }
|
|
18
|
+
}
|
|
19
|
+
|
|
3
20
|
export async function runNew(args = []) {
|
|
4
|
-
const projectName
|
|
5
|
-
|
|
6
|
-
await scaffoldProject(projectName, { install })
|
|
21
|
+
const { projectName, theme, install } = parseNewArgs(args)
|
|
22
|
+
await scaffoldProject(projectName, { install, theme })
|
|
7
23
|
}
|
package/src/scaffold.js
CHANGED
|
@@ -26,11 +26,54 @@ export function validateProjectName(projectName) {
|
|
|
26
26
|
return projectName
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// What `--theme` accepts, mirroring the three forms `app.theme` takes in
|
|
30
|
+
// @nera-static/core (src/theme.js): a bare name (`example` →
|
|
31
|
+
// @nera-static/theme-example), a full package name (`@acme/my-theme`), or a
|
|
32
|
+
// local path (`./my-theme`). Validated because it lands in app.yaml and, for
|
|
33
|
+
// the package forms, on the npm command line.
|
|
34
|
+
const BARE_THEME = /^[a-z0-9][a-z0-9._-]*$/
|
|
35
|
+
const SCOPED_THEME = /^@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/
|
|
36
|
+
const LOCAL_THEME = /^\.{1,2}(\/[A-Za-z0-9._-]+)*\/?$/
|
|
37
|
+
|
|
38
|
+
export function validateThemeSpec(theme) {
|
|
39
|
+
if (
|
|
40
|
+
typeof theme === 'string' &&
|
|
41
|
+
(BARE_THEME.test(theme) ||
|
|
42
|
+
SCOPED_THEME.test(theme) ||
|
|
43
|
+
LOCAL_THEME.test(theme))
|
|
44
|
+
) {
|
|
45
|
+
return theme
|
|
46
|
+
}
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Invalid theme "${theme}". Use a theme name (example), a package ` +
|
|
49
|
+
'name (@scope/theme) or a local path (./my-theme).'
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The npm package a theme spec installs, or null for a local path. Same rule
|
|
54
|
+
// as core's packageName() — keep the two in step.
|
|
55
|
+
export function themePackageName(theme) {
|
|
56
|
+
if (theme.startsWith('.')) return null
|
|
57
|
+
return theme.includes('/') || theme.startsWith('@')
|
|
58
|
+
? theme
|
|
59
|
+
: `@nera-static/theme-${theme}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Starter templates carry this marker on their first line. With a theme they
|
|
63
|
+
// are left out: site views win over theme views file by file, so a starter
|
|
64
|
+
// layout would hide the theme's own. @nera-static/validate warns on the same
|
|
65
|
+
// marker (`theme-shadowed`) for sites that kept them.
|
|
66
|
+
const SCAFFOLD_MARKER = 'nera:scaffold-default'
|
|
67
|
+
|
|
68
|
+
const isStarterTemplate = async (file) =>
|
|
69
|
+
file.endsWith('.pug') &&
|
|
70
|
+
(await fs.readFile(file, 'utf-8')).includes(SCAFFOLD_MARKER)
|
|
71
|
+
|
|
29
72
|
// The scaffold template ships inside this package (package.json `files`).
|
|
30
73
|
export const templateDir = () =>
|
|
31
74
|
path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'template')
|
|
32
75
|
|
|
33
|
-
async function copyDir(src, dest) {
|
|
76
|
+
async function copyDir(src, dest, { skipStarters = false } = {}) {
|
|
34
77
|
await fs.mkdir(dest, { recursive: true })
|
|
35
78
|
for (const entry of await fs.readdir(src, { withFileTypes: true })) {
|
|
36
79
|
const from = path.join(src, entry.name)
|
|
@@ -39,7 +82,9 @@ async function copyDir(src, dest) {
|
|
|
39
82
|
const name = entry.name === '_gitignore' ? '.gitignore' : entry.name
|
|
40
83
|
const to = path.join(dest, name)
|
|
41
84
|
if (entry.isDirectory()) {
|
|
42
|
-
await copyDir(from, to)
|
|
85
|
+
await copyDir(from, to, { skipStarters })
|
|
86
|
+
} else if (skipStarters && (await isStarterTemplate(from))) {
|
|
87
|
+
continue
|
|
43
88
|
} else {
|
|
44
89
|
await fs.copyFile(from, to)
|
|
45
90
|
}
|
|
@@ -54,13 +99,35 @@ async function personalize(targetDir, projectName) {
|
|
|
54
99
|
console.log(` ✓ Configured project as "${projectName}"`)
|
|
55
100
|
}
|
|
56
101
|
|
|
102
|
+
// Point the site at its theme: `theme:` in app.yaml (appended, so the
|
|
103
|
+
// template's own lines and comments stay as they are) and, for a package
|
|
104
|
+
// theme, a dependency that `npm install` resolves.
|
|
105
|
+
async function applyTheme(targetDir, theme) {
|
|
106
|
+
const appYaml = path.join(targetDir, 'config', 'app.yaml')
|
|
107
|
+
const yaml = await fs.readFile(appYaml, 'utf-8')
|
|
108
|
+
await fs.writeFile(
|
|
109
|
+
appYaml,
|
|
110
|
+
`${yaml.replace(/\n*$/, '\n')}\ntheme: ${theme}\n`
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
const pkgName = themePackageName(theme)
|
|
114
|
+
if (pkgName) {
|
|
115
|
+
const pkgPath = path.join(targetDir, 'package.json')
|
|
116
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'))
|
|
117
|
+
pkg.dependencies = { ...pkg.dependencies, [pkgName]: 'latest' }
|
|
118
|
+
await fs.writeFile(pkgPath, `${JSON.stringify(pkg, null, 4)}\n`)
|
|
119
|
+
}
|
|
120
|
+
console.log(` ✓ Using theme "${theme}"${pkgName ? ` (${pkgName})` : ''}`)
|
|
121
|
+
}
|
|
122
|
+
|
|
57
123
|
// Scaffold a thin Nera site: copy the template, name it, and (by default)
|
|
58
124
|
// install its single dependency, @nera-static/nera. No git clone, no vendored
|
|
59
125
|
// engine — the opposite of the old installer's clone-and-strip flow.
|
|
60
126
|
export async function scaffoldProject(projectName, options = {}) {
|
|
61
|
-
const { install = true, cwd = process.cwd() } = options
|
|
127
|
+
const { install = true, cwd = process.cwd(), theme } = options
|
|
62
128
|
|
|
63
129
|
validateProjectName(projectName)
|
|
130
|
+
if (theme !== undefined) validateThemeSpec(theme)
|
|
64
131
|
|
|
65
132
|
const targetDir = path.resolve(cwd, projectName)
|
|
66
133
|
if (fssync.existsSync(targetDir)) {
|
|
@@ -68,12 +135,18 @@ export async function scaffoldProject(projectName, options = {}) {
|
|
|
68
135
|
}
|
|
69
136
|
|
|
70
137
|
console.log(`📦 Creating a new Nera site in ${targetDir}...`)
|
|
71
|
-
await copyDir(templateDir(), targetDir)
|
|
138
|
+
await copyDir(templateDir(), targetDir, { skipStarters: Boolean(theme) })
|
|
72
139
|
await personalize(targetDir, projectName)
|
|
140
|
+
if (theme) await applyTheme(targetDir, theme)
|
|
73
141
|
|
|
74
142
|
if (install) {
|
|
75
143
|
console.log('📦 Installing dependencies...')
|
|
76
|
-
|
|
144
|
+
// Naming the theme package installs everything else too, and makes npm
|
|
145
|
+
// replace the `latest` placeholder with a caret range for the version
|
|
146
|
+
// it resolved.
|
|
147
|
+
const themePkg = theme ? themePackageName(theme) : null
|
|
148
|
+
const args = themePkg ? ['install', themePkg] : ['install']
|
|
149
|
+
execFileSync('npm', args, { cwd: targetDir, stdio: 'inherit' })
|
|
77
150
|
}
|
|
78
151
|
|
|
79
152
|
console.log('✅ Done!')
|