@nera-static/nera 1.14.0 → 1.15.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 +8 -4
- package/package.json +1 -1
- package/src/commands/update.js +77 -2
- package/src/scaffold.js +9 -0
package/README.md
CHANGED
|
@@ -31,17 +31,21 @@ package is named after the folder (`My Bakery` → `my-bakery`).
|
|
|
31
31
|
| `nera build` | Render `pages/` → `public/`. With `--check`, then run `nera check` on the result — one command for CI. `--json` prints the result as one JSON object (see [Machine-readable output](#machine-readable-output)). |
|
|
32
32
|
| `nera dev` | Build, serve `public/`, and rebuild on change with live reload. Watches `pages/`, `config/`, `theme/` and a local theme (`theme: ./themes/<name>`). |
|
|
33
33
|
| `nera serve` | Serve the already-built `public/` folder. |
|
|
34
|
-
| `nera update` | Update the site's Nera packages, then create a missing `AGENTS.md`/`CLAUDE.md` and refresh the Nera part of `AGENTS.md` and the `nera` skill (see [Instructions for AI assistants](#instructions-for-ai-assistants)). On a legacy cloned site, `nera update --migrate` converts it to the thin model. |
|
|
34
|
+
| `nera update` | Update the site's Nera packages and raise their caret ranges in `package.json` (and `package-lock.json`) to the installed versions, then create a missing `AGENTS.md`/`CLAUDE.md` and refresh the Nera part of `AGENTS.md` and the `nera` skill (see [Instructions for AI assistants](#instructions-for-ai-assistants)). On a legacy cloned site, `nera update --migrate` converts it to the thin model. |
|
|
35
35
|
| `nera validate` | Check the site's sources (layouts, includes, YAML) before publishing; exits non-zero on any error. `--json` for machine-readable output. |
|
|
36
36
|
| `nera check` | Check the built `public/` for accessibility, privacy and legal-notice problems (see below). Build first. `--json` for machine-readable output. |
|
|
37
37
|
|
|
38
38
|
A scaffolded site is a thin project — no vendored engine, no clone. It lists one
|
|
39
|
-
dependency
|
|
39
|
+
dependency, a caret range starting at the version of the CLI that created it,
|
|
40
|
+
and its scripts call the CLI:
|
|
40
41
|
|
|
41
42
|
```jsonc
|
|
42
43
|
{
|
|
43
|
-
"scripts": {
|
|
44
|
-
|
|
44
|
+
"scripts": {
|
|
45
|
+
"dev": "nera dev", "build": "nera build", "serve": "nera serve",
|
|
46
|
+
"validate": "nera validate", "check": "nera build --check"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": { "@nera-static/nera": "^1.14.1" }
|
|
45
49
|
}
|
|
46
50
|
```
|
|
47
51
|
|
package/package.json
CHANGED
package/src/commands/update.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from 'fs/promises'
|
|
|
3
3
|
import fssync from 'fs'
|
|
4
4
|
import path from 'path'
|
|
5
5
|
import { syncAgentFiles, SITE_NOTES_MARKER } from '../agent-files.js'
|
|
6
|
-
import { templateDir as packageTemplateDir } from '../scaffold.js'
|
|
6
|
+
import { ownRange, templateDir as packageTemplateDir } from '../scaffold.js'
|
|
7
7
|
import { installedSkillDir, skillSourceDir, syncSkill } from '../skill.js'
|
|
8
8
|
|
|
9
9
|
const defaultRunNpm = (npmArgs, cwd) =>
|
|
@@ -18,6 +18,78 @@ export function neraDeps(pkg) {
|
|
|
18
18
|
)
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// The caret range `range` raised to the installed `version`, or null when it
|
|
22
|
+
// stays: only a plain `^x.y.z` with x ≥ 1, only upwards, never across a major.
|
|
23
|
+
// Exact, tilde, tag, git and 0.x ranges are the user's choice and stay as
|
|
24
|
+
// they are. `npm update` installs within the range but never rewrites it.
|
|
25
|
+
export function raisedRange(range, version) {
|
|
26
|
+
const from = /^\^(\d+)\.(\d+)\.(\d+)$/.exec(range ?? '')
|
|
27
|
+
const to = /^(\d+)\.(\d+)\.(\d+)$/.exec(version ?? '')
|
|
28
|
+
if (!from || !to) return null
|
|
29
|
+
const [a, b] = [from, to].map((m) => m.slice(1).map(Number))
|
|
30
|
+
if (a[0] < 1 || a[0] !== b[0]) return null
|
|
31
|
+
const newer = b[1] > a[1] || (b[1] === a[1] && b[2] > a[2])
|
|
32
|
+
return newer ? `^${version}` : null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const indentOf = (text) => /^[ \t]+(?=")/m.exec(text)?.[0] ?? ' '
|
|
36
|
+
|
|
37
|
+
// Raise the site's caret ranges of `names` to what is installed in its
|
|
38
|
+
// node_modules, keeping the file's own indentation, and the same ranges in
|
|
39
|
+
// the root entry of package-lock.json, so the two stay in sync. Returns the
|
|
40
|
+
// changes as [name, from, to].
|
|
41
|
+
export async function raiseRanges(cwd, names) {
|
|
42
|
+
const pkgPath = path.join(cwd, 'package.json')
|
|
43
|
+
const text = await fs.readFile(pkgPath, 'utf-8')
|
|
44
|
+
const pkg = JSON.parse(text)
|
|
45
|
+
const raised = {}
|
|
46
|
+
const changes = []
|
|
47
|
+
for (const field of ['dependencies', 'devDependencies']) {
|
|
48
|
+
for (const name of names) {
|
|
49
|
+
const range = pkg[field]?.[name]
|
|
50
|
+
if (typeof range !== 'string') continue
|
|
51
|
+
let version
|
|
52
|
+
try {
|
|
53
|
+
const installed = path.join(cwd, 'node_modules', ...name.split('/'), 'package.json')
|
|
54
|
+
version = JSON.parse(await fs.readFile(installed, 'utf-8')).version
|
|
55
|
+
} catch {
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
const to = raisedRange(range, version)
|
|
59
|
+
if (to) {
|
|
60
|
+
pkg[field][name] = to
|
|
61
|
+
raised[field] = { ...raised[field], [name]: [range, to] }
|
|
62
|
+
changes.push([name, range, to])
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (changes.length === 0) return changes
|
|
67
|
+
await fs.writeFile(pkgPath, `${JSON.stringify(pkg, null, indentOf(text))}\n`)
|
|
68
|
+
|
|
69
|
+
const lockPath = path.join(cwd, 'package-lock.json')
|
|
70
|
+
let lockText
|
|
71
|
+
try {
|
|
72
|
+
lockText = await fs.readFile(lockPath, 'utf-8')
|
|
73
|
+
} catch {
|
|
74
|
+
return changes
|
|
75
|
+
}
|
|
76
|
+
const lock = JSON.parse(lockText)
|
|
77
|
+
const root = lock.packages?.['']
|
|
78
|
+
let lockChanged = false
|
|
79
|
+
for (const [field, entries] of Object.entries(raised)) {
|
|
80
|
+
for (const [name, [from, to]] of Object.entries(entries)) {
|
|
81
|
+
if (root?.[field]?.[name] === from) {
|
|
82
|
+
root[field][name] = to
|
|
83
|
+
lockChanged = true
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (lockChanged) {
|
|
88
|
+
await fs.writeFile(lockPath, `${JSON.stringify(lock, null, indentOf(lockText))}\n`)
|
|
89
|
+
}
|
|
90
|
+
return changes
|
|
91
|
+
}
|
|
92
|
+
|
|
21
93
|
// A THIN site depends on @nera-static/nera and carries no engine source.
|
|
22
94
|
// A LEGACY site is a clone of the old generator: it vendors the engine under
|
|
23
95
|
// src/ and has no @nera-static/nera dependency — the case `nera update` migrates.
|
|
@@ -107,7 +179,7 @@ async function migrateLegacy(cwd, pkg, runNpm, templateDir, skillDir) {
|
|
|
107
179
|
|
|
108
180
|
pkg.dependencies = {
|
|
109
181
|
...pkg.dependencies,
|
|
110
|
-
'@nera-static/nera':
|
|
182
|
+
'@nera-static/nera': ownRange(),
|
|
111
183
|
}
|
|
112
184
|
|
|
113
185
|
pkg.scripts = {
|
|
@@ -193,6 +265,9 @@ export async function runUpdate(args = [], options = {}) {
|
|
|
193
265
|
const targets = neraDeps(pkg)
|
|
194
266
|
console.log(`🔄 Updating Nera packages: ${targets.join(', ')}`)
|
|
195
267
|
runNpm(['update', ...targets], cwd)
|
|
268
|
+
for (const [name, from, to] of await raiseRanges(cwd, targets)) {
|
|
269
|
+
console.log(` ✓ ${name}: ${from} → ${to} in package.json`)
|
|
270
|
+
}
|
|
196
271
|
await syncAndReportAgentFiles(cwd, templateDir)
|
|
197
272
|
await syncAndReportSkill(cwd, skillDir)
|
|
198
273
|
console.log('✅ Up to date.')
|
package/src/scaffold.js
CHANGED
|
@@ -138,6 +138,14 @@ const isStarterTemplate = async (file) =>
|
|
|
138
138
|
export const templateDir = () =>
|
|
139
139
|
path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'template')
|
|
140
140
|
|
|
141
|
+
// The range a new or migrated site depends on: this CLI's own version, so the
|
|
142
|
+
// site never claims to work with an older one that lacks what set it up
|
|
143
|
+
// (starters, `nera check`, the `validate`/`check` scripts).
|
|
144
|
+
export const ownRange = () => {
|
|
145
|
+
const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json')
|
|
146
|
+
return `^${JSON.parse(fssync.readFileSync(pkgPath, 'utf-8')).version}`
|
|
147
|
+
}
|
|
148
|
+
|
|
141
149
|
async function copyDir(src, dest, { skipStarters = false } = {}) {
|
|
142
150
|
await fs.mkdir(dest, { recursive: true })
|
|
143
151
|
for (const entry of await fs.readdir(src, { withFileTypes: true })) {
|
|
@@ -165,6 +173,7 @@ async function personalize(targetDir, projectName) {
|
|
|
165
173
|
const pkgPath = path.join(targetDir, 'package.json')
|
|
166
174
|
const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'))
|
|
167
175
|
pkg.name = projectName
|
|
176
|
+
pkg.dependencies = { ...pkg.dependencies, '@nera-static/nera': ownRange() }
|
|
168
177
|
await fs.writeFile(pkgPath, `${JSON.stringify(pkg, null, 4)}\n`)
|
|
169
178
|
console.log(` ✓ Configured project as "${projectName}"`)
|
|
170
179
|
}
|