@nera-static/plugin-utils
v1.6.0
Published
Shared utility functions for Nera plugins (config loading, template publishing)
Maintainers
Readme
@nera-static/plugin-utils
🛠 Utility helpers for developing plugins for the Nera static site generator.
📖 Documentation: nera.js.org
📦 Installation
npm install @nera-static/plugin-utils📚 Features
Configuration Loading
getConfig(filePath: string): object
Reads and parses a YAML config file, returning its contents as a JavaScript object.
Returns {} if the file does not exist or is empty — it never throws. Supply a JS fallback for every key you read.
Plugins read config from the user's site, not from the package. The config/<name>.yaml shipped inside a plugin is documentation only; it is never merged with the user's copy. Resolve against process.cwd():
import path from 'path'
import { getConfig } from '@nera-static/plugin-utils'
const config = getConfig(path.resolve(process.cwd(), 'config/my-plugin.yaml'))
const title = config.title || 'Default title'Slugs
slugify(text: string): string
Turns a user-authored string into a URL-safe slug, for anything that becomes a path segment, an HTML id, or a URL fragment.
import { slugify } from '@nera-static/plugin-utils'
slugify('Web Development') // 'web-development'
slugify('Über uns') // 'uber-uns'
slugify('Straße') // 'strasse'
slugify('Café') // 'cafe'
slugify('About Us!') // 'about-us'
slugify('日本語') // ''Use this rather than writing your own. The obvious one-liner is ASCII-only and produces broken output:
text.toLowerCase().replace(/[^\w]+/g, '-') // ❌ don't\w is [A-Za-z0-9_], so Über uns becomes -ber-uns and Straße becomes stra-e. A leading hyphen is legal in an HTML id but is not a valid CSS identifier — #-ber-uns matches nothing in a stylesheet and document.querySelector('#-ber-uns') throws. That defect shipped in plugin-one-page until v3.0.0.
The rule: ß → ss (it has no NFKD decomposition, so it would otherwise become a hyphen mid-word), then NFKD with combining marks dropped, then lowercase, then every run of non-alphanumerics collapsed to a single -, then leading and trailing hyphens trimmed. It is idempotent.
A string with no Latin letters or digits returns ''. Decide what that means at the call site — omitting the element is usually better than emitting id="":
const id = slugify(heading)
return id ? `<a id="${id}"></a>` : ''Template Publishing
validateNeraProject(expectedPackageName?: string): boolean
Validates if the current working directory is a valid Nera project.
A package.json must be present. Given that, the directory qualifies if it looks like a Nera project — it contains both config/app.yaml and pages/ — or if its package.json name matches expectedPackageName or starts with nera. The shape check means a project can be named anything, but it does not remove the package.json requirement.
import { validateNeraProject } from '@nera-static/plugin-utils'
if (validateNeraProject()) {
console.log('Valid Nera project!')
}publishTemplates(options): boolean
Publishes specific template files from a plugin to a Nera project.
The destination is theme-aware, resolved exactly as the generator resolves its views folder (see resolveViewsDir below): theme/views/vendor/<pluginName>/ on a themed site, the deprecated root views/vendor/<pluginName>/ otherwise. If it already exists, publishing is skipped and the function returns true — this protects the customizations you have made to previously published templates. Pass force: true to overwrite them.
import { publishTemplates } from '@nera-static/plugin-utils'
const result = publishTemplates({
pluginName: 'plugin-my-awesome-plugin',
sourceDir: path.resolve(__dirname, '../views/'),
templateFiles: ['template.pug', 'another-template.pug'], // or single file as string
expectedPackageName: 'dummy', // optional, for testing
force: false, // optional, re-publish over an existing destination
})Returns false if any listed template is missing from sourceDir. Sources are all verified before anything is copied, so a failure never leaves a partially published destination.
publishAllTemplates(options): boolean
Publishes all .pug template files from a plugin's views directory to a Nera project, including those in subdirectories, preserving their structure. A template that does include partials/nav therefore ships together with the partial it depends on.
import { publishAllTemplates } from '@nera-static/plugin-utils'
const result = publishAllTemplates({
pluginName: 'plugin-my-awesome-plugin',
sourceDir: path.resolve(__dirname, '../views/'),
expectedPackageName: 'dummy', // optional, for testing
force: false, // optional, re-publish over an existing destination
})Returns true and logs a warning if sourceDir contains no .pug files; an unreadable sourceDir returns false. A publish script following the process.exit(result ? 0 : 1) pattern below therefore exits 0 having copied nothing — check your sourceDir if a plugin's templates never appear.
publishAsset(options): boolean
Publishes a single support file — a plugin's client script, say — into the project's assets folder, theme-aware (theme/assets/ on a themed site, deprecated root assets/ otherwise) and with the same skip-if-exists rule as publishTemplates. Use it for plugins that ship a runtime asset alongside their templates, so the asset lands where a themed build actually serves it rather than a root assets/ copy the build ignores.
import { publishAsset } from '@nera-static/plugin-utils'
const result = publishAsset({
sourceFile: path.resolve(__dirname, '../views/my-plugin.js'),
targetPath: 'js/my-plugin.js', // relative to the resolved assets root
expectedPackageName: 'dummy', // optional, for testing
force: false, // optional, overwrite an existing file
})Returns false outside a Nera project or when sourceFile is missing; a deliberate skip of an existing file returns true.
resolveViewsDir(cwd?): string / resolveAssetsDir(cwd?): string
Return the project's views / assets directory as an absolute path, resolved the same way the generator does: an explicit folders.<key> in config/app.yaml wins; otherwise theme/views / theme/assets when a local theme/ folder exists; otherwise the deprecated root folder. publishTemplates and publishAsset use these internally, but they are exported for plugins that need to report or reason about where their files went.
The
__dirnameused in these option examples is not defined in ESM. See the CLI Integration example below for the two lines that define it.
Template Publishing CLI Integration
These functions are designed to be used in bin/publish-template.js scripts that can be executed via npm scripts:
#!/usr/bin/env node
import path from 'path'
import { fileURLToPath } from 'url'
import { publishAllTemplates } from '@nera-static/plugin-utils'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const pluginName = 'plugin-my-awesome-plugin'
const sourceDir = path.resolve(__dirname, '../views/')
const result = publishAllTemplates({
pluginName,
sourceDir,
expectedPackageName: 'dummy', // for test-only override
})
process.exit(result ? 0 : 1)🧱 Use Cases
This package is intended for use inside Nera plugins to:
- Load Configuration: Simplify loading YAML configuration files
- Publish Templates: Standardize template publishing across plugins
- Validate Projects: Ensure commands run in valid Nera projects
Example Plugin Structure
// index.js - Main plugin file
import path from 'path'
import { getConfig } from '@nera-static/plugin-utils'
export function getAppData(data) {
const config = getConfig(
path.resolve(process.cwd(), 'config/my-plugin.yaml')
)
// Spread the incoming app. The return value REPLACES `app` wholesale --
// returning a bare object discards every other plugin's data, silently.
return {
...data.app,
myPlugin: {
// ... plugin logic using config
},
}
}A plugin exports getAppData and/or getMetaData. Both receive a single object — { app, pagesData } — and both must be synchronous. getAppData must return a plain object, getMetaData an array; a wrong return type is skipped with a console warning while the build still succeeds, so a plugin that appears to "do nothing" is usually a return-type problem.
See the Nera contributing guide for the full plugin contract.
// bin/publish-template.js - Template publishing script
#!/usr/bin/env node
import path from 'path'
import { fileURLToPath } from 'url'
import { publishAllTemplates } from '@nera-static/plugin-utils'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const result = publishAllTemplates({
pluginName: 'plugin-my-awesome-plugin',
sourceDir: path.resolve(__dirname, '../views/')
})
process.exit(result ? 0 : 1)🧪 Development
npx vitest run # single pass -- `npm test` is watch mode
npm run lint🤝 Contributing
Issues and pull requests are welcome. See the Nera contributing guide for plugin development, the hook contract, and local setup.
For this repo specifically:
npx vitest runandnpm run lintmust pass (npm testis watch mode).- Bump the version and update
CHANGELOG.mdin the same commit as the change. - Every plugin in the fleet depends on this package, so treat the exported function signatures as a public contract — changing one is a major bump.
- Releases publish from CI on a pushed
v*tag. Never runnpm publish.
🧑💻 Maintainers
Created and maintained by @seebaermichi
🧩 Compatibility
- Node.js: >= 20.0.0
- Runtime dependency:
js-yaml ^4.1.0only - Nera: no direct dependency on the generator — this package is used by plugins, not by the generator itself
📦 License
MIT
