Skip to content

Publishing Helper Modules

Language: JavaScript

How a module's package.json, npmrc, test directory, and dependencies must be configured for the unified CI/CD pipeline to test and publish it. Publishing is CI-only - bumping the version field in a module's package.json and pushing to main triggers the publish automatically. There is no manual npm publish.

Companion docs.

  • ../../dev/cicd-publishing.md - operational walkthrough of the unified ci-publish-helper-modules.yml workflow, GITHUB_TOKEN permissions, fresh-state recovery, failure modes, and CI-side troubleshooting (401 / 403 / registry URL).
  • versioning/bump-checklist.md - the step-by-step version-bump procedure including SemVer classification, commit format, multi-module bumps, and post-publish verification.
  • module-docs.md - what every module README must contain (badges, sections, ordering). This file scopes only the package.json and _test/ layout.

This page scopes package-configuration rules; the companion docs scope how to run the pipeline and how to bump a version.

On This Page


Package Identity

FieldRequired Value
name@your-org/js-helper-* or @your-org/js-server-helper-*
licenseMIT
privatefalse
publishConfig.registryhttps://npm.pkg.github.com (no trailing slash, no scope suffix)
type"module" (mandatory - all helper modules are ESM)
exportsAn exports map pointing at the entry file (replaces the legacy "main" field)

The "exports" Map

Every helper module is an ES Module ("type": "module") and uses an "exports" map instead of the legacy "main" field. The "exports" map controls what consumers can import and is the single source of truth for the package's entry point.

Required shape:

json
{
  "type": "module",
  "exports": {
    ".": "./[module].js",
    "./package.json": "./package.json"
  }
}

Rules:

  • "." points at the module's entry file (e.g. "./utils.js", "./adapter.js").
  • "./package.json" is always exported so tooling can read the package metadata.
  • Companion files consumed by the entry file (e.g. [module].config.js, [module].errors.js, parts/*.js) are not listed as separate export paths. They are resolved relative to the entry file through normal ESM relative imports. The "exports" map exposes only the public entry point.
  • parts/ subdirectories are never exported. They are internal implementation files consumed by the entry file's relative imports, not by consumers. See module-structure.md → Parts Pattern.
  • The legacy "main" field is not used. A package with both "main" and "exports" is misconfigured - remove "main".
  • Every export path must include the .js extension. ESM resolution requires explicit extensions for relative paths.

Dependency Rules

Foundation Modules (Zero Dependencies)

js-helper-utils and js-helper-debug are fully self-contained. They must never depend on each other or on any other helper module.

Other Modules

All other modules may depend on the foundation modules via peer dependencies. This avoids duplicate installations and ensures a single shared instance. The full strategy is in dependencies.md.

npmrc Configuration

Use machine-level ~/.npmrc with environment variable support. Do not create per-module .npmrc files.

bash
npm config set @your-org:registry https://npm.pkg.github.com
npm config set //npm.pkg.github.com/:_authToken '${GITHUB_READ_PACKAGES_TOKEN}'
npm config set registry https://registry.npmjs.org/

Load the token via the project environment script:

bash
source init-env.sh
# Select: 1) dev

See ../../dev/npmrc-setup.md for the complete setup guide and ../../dev/onboarding-github-packages.md for the GitHub token side.

Registry Ignore File (.npmignore)

Every published JS module must have a .npmignore file at its root.

Without it, npm pack includes everything that is not in .gitignore - meaning _test/, eslint.config.js, .github/, and other dev-only files all land in the published tarball. This bloats the package and exposes internal structure that consumers have no use for.

Rules:

  • Place .npmignore at the module root (alongside package.json).
  • It must exclude at minimum: _test/, eslint.config.js, .github/, THOUGHTS.md.
  • It must include (i.e. not accidentally exclude): the main entry file, README.md, ROBOTS.md, docs/, package.json, and any parts/ or src/ subdirectories that contain production code.
  • README.md, ROBOTS.md, and docs/ ship intentionally - README.md links to both, and those links must resolve for anyone reading the package page or consuming the package.
  • THOUGHTS.md is an internal engineering decision journal for contributors. It must never ship in the published tarball. See THOUGHTS.md convention.
  • Verify with npm pack --dry-run from the module root before publishing. The output lists every file that would be included.

Canonical reference: For the exact file contents and the complete exclusion list, refer to js-helper-utils - it is the simplest base module and serves as the reference implementation for all JS helper modules. Copy its .npmignore as your starting point and adjust only for module-specific additions (e.g. parts/ subdirectory modules need no adjustment; adapter modules with a schema/ folder may need to verify it is included).

Note: other languages have their own equivalent mechanisms (Python: MANIFEST.in or pyproject.toml exclude patterns; Java: Maven/Gradle publish configuration). Each language's publishing documentation covers its own convention. The principle is the same: every published package must explicitly control what ships.

Linter Configuration

Every JS module must have an eslint.config.js file at its root (ESLint flat config, required for ESLint v10+).

Linter config is a three-line re-export of the shared @superloomdev/js-helper-eslint-config package. The shared config is the single source of truth for all lint rules; per-module overrides are not permitted. The config must be present for npm run lint to work as part of the pre-publish gate.

Canonical consumer form:

javascript
import { base } from '@superloomdev/js-helper-eslint-config';
export default [ ...base ];

Application-tier repos with JSX and browser globals use the app preset instead of base. See code-formatting.md → Shared ESLint Configuration for the full preset table.

Three properties of the shared config are policy, not preference:

  • ecmaVersion: 2022 - matches the Node.js 24+ engine floor.
  • No argsIgnorePattern / caughtErrorsIgnorePattern. Underscore-prefixed parameters (_param, catch (_err)) are forbidden; lint must flag them. Parity parameters keep their real name with // eslint-disable-line no-unused-vars; unused catch bindings use catch {. See code-formatting.md -> Parameter Naming.
  • js-helper-eslint-config is the head of the CI chain. It must publish before any other module. The CI workflow (ci-publish-helper-modules.yml) chains test-eslint-config and publish-eslint-config ahead of all other test/publish jobs.

Note: other languages have their own linter conventions (Python: ruff or pylint config; etc.). Each language's module structure documentation covers its own convention.

Required Scripts

Every module package.json must include:

json
{
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "test": "node --test _test/test.js"
  }
}

The pre-publish gate requires both npm run lint (from the module root) and npm test (from _test/) to exit 0 locally before the version bump is pushed. See ../../dev/testing-local-modules.md → Pre-Publish Checklist.

Test Directory Structure

module-name/
  _test/
    test.js           # Tests using node:test and node:assert/strict
    package.json       # private: true, references module as "file:../"
    mock-data/         # (optional) JSON fixtures
  • _test/package.json must have "private": true
  • Reference the module under test as "file:../" in dependencies
  • Reference published @your-org packages by version for peer dependencies (never file: for siblings - that breaks in CI; see pitfalls.md entry 8)
  • No .npmrc in _test/ - use global npmrc

Same-Version Republish for Generated Packages

A generated reference theme package (e.g., Material, Carbon) completes its schemes from the base template at generation time. When the base template is republished at the same version with corrected values, the generated package must be regenerated and republished at the same version. The procedure:

  1. Regenerate all schemes from the corrected, registry-installed base.
  2. Verify the installed base shasum equals the registry shasum before writing.
  3. Byte-compare regenerated output against committed data files.
  4. Run the module's full test suite (including provenance and exact typography tests).
  5. Delete the exact queried 1.0.0 version ID through gh api immediately before the push that publishes it.
  6. Push the known publishing commit.
  7. Compare local pack shasum to registry shasum after CI publishes.
  8. Regenerate every consumer lockfile that resolves the republished package.

Never delete a package version the active plan does not name. Never bump a version to clear a checksum mismatch. See pitfalls-migration.md - Generated profile retains stale values.

Further Reading

Released under the MIT License.