npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nera-static/plugin-utils

v1.6.0

Published

Shared utility functions for Nera plugins (config loading, template publishing)

Readme

@nera-static/plugin-utils

Test npm version

🛠 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 __dirname used 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:

  1. Load Configuration: Simplify loading YAML configuration files
  2. Publish Templates: Standardize template publishing across plugins
  3. 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 run and npm run lint must pass (npm test is watch mode).
  • Bump the version and update CHANGELOG.md in 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 run npm publish.

🧑‍💻 Maintainers

Created and maintained by @seebaermichi


🧩 Compatibility

  • Node.js: >= 20.0.0
  • Runtime dependency: js-yaml ^4.1.0 only
  • Nera: no direct dependency on the generator — this package is used by plugins, not by the generator itself

📦 License

MIT