@freelygive/npm-scaffold
v0.3.1
Published
Scaffold files from npm packages into your project structure, similar to Drupal Composer Scaffold.
Maintainers
Readme
@freelygive/npm-scaffold
Scaffold files from npm packages into your project structure. Similar to Drupal Composer Scaffold, but for npm packages.
This is useful when a shared library contains files that need to land in
specific locations across your project (e.g. components, stories, tests) rather
than being imported from node_modules.
Quick start
Install in your consuming project:
npm install @freelygive/npm-scaffold --save-devAdd a postinstall script and configure npmScaffold in your package.json:
{
"scripts": {
"postinstall": "npm-scaffold"
},
"npmScaffold": {
"allowed-packages": ["@your-org/shared-components"],
"base-path": "src"
}
}Now when you run npm install, files from @your-org/shared-components are
automatically symlinked into your src/ directory and added to .gitignore.
Configuration
Consuming project (package.json)
{
"npmScaffold": {
"allowed-packages": ["@your-org/shared-components"],
"default-mode": "symlink",
"gitignore": true,
"base-path": "src",
"file-mapping": {}
}
}| Field | Type | Default | Description |
|---|---|---|---|
| allowed-packages | string[] | [] | Packages allowed to scaffold files. Acts as a security gate. |
| default-mode | "symlink" \| "copy" | "symlink" | Default operation mode for all files. |
| gitignore | boolean | true | Auto-manage a section in .gitignore for scaffolded files. |
| base-path | string | "" | Prefix prepended to all destination paths from source packages. |
| locations | object | {} | Named location mappings for [location] prefixes (see below). |
| file-mapping | object | {} | Per-file overrides (see below). |
Source package (package.json)
Source packages declare which files should be scaffolded and where:
{
"name": "@your-org/shared-components",
"npmScaffold": {
"file-mapping": {
"components/button/index.jsx": "components/button/index.jsx",
"components/button/component.yml": "components/button/component.yml",
"stories/components/button.stories.tsx": "stories/button.stories.tsx",
"stories/tests/button.stories.tsx": "tests/button.stories.tsx"
}
}
}- Keys are destination paths, relative to
base-path(or project root ifbase-pathis not set). Keys can use a[location]prefix to target a different directory (see Location prefixes). - Values are source paths, relative to the package root. These can point to individual files or entire directories.
With "base-path": "src" configured in the consuming project, the example above
would scaffold files to:
src/components/button/index.jsx
src/components/button/component.yml
src/stories/components/button.stories.tsx
src/stories/tests/button.stories.tsxDirectory mappings
When a source path points to a directory, the behavior depends on the mode:
- Symlink mode (default): creates a single symlink to the entire directory.
- Copy mode: recursively copies all files within the directory individually.
{
"npmScaffold": {
"file-mapping": {
"components/button": "components/button",
"components/card": "components/card"
}
}
}This is useful when a component has many files — instead of listing each one,
map the whole directory. In symlink mode, the .gitignore entry uses a trailing
slash (e.g. /src/components/button/).
Location prefixes
By default, all destination paths are relative to base-path. To place files
outside of base-path, use a [location] prefix in the mapping key:
{
"npmScaffold": {
"file-mapping": {
"components/button/index.jsx": "components/button/index.jsx",
"[skills]content-management/SKILL.md": "skills/content-management/SKILL.md"
}
}
}The consuming project defines where each location maps to via the locations
config:
{
"npmScaffold": {
"base-path": "src",
"locations": {
"skills": ".claude/skills"
}
}
}With this config:
components/button/index.jsx→src/components/button/index.jsx(usesbase-path)[skills]content-management/SKILL.md→.claude/skills/content-management/SKILL.md
Special behavior:
[base]is a built-in alias forbase-path, so[base]foois equivalent tofoo.- An unknown location prefix (not in
locations) resolves from the project root.
Per-file overrides
The consuming project can override individual file mappings:
{
"npmScaffold": {
"file-mapping": {
"components/button/index.jsx": { "mode": "copy" },
"stories/tests/button.stories.tsx": false
}
}
}Override keys match the mapping key from the source package (without
base-path). For directory mappings in copy mode, you can also override
individual files within the directory using their expanded path (e.g.
"components/button/styles.css": false).
| Override | Effect |
|---|---|
| false | Skip the file entirely. |
| { "mode": "copy" } | Force copy instead of symlink (or vice versa). |
| { "overwrite": false } | Don't overwrite existing files. |
| { "path": "alt/source.js" } | Redirect to a different source file in the package. |
CLI
npm-scaffold [options]| Option | Description |
|---|---|
| --mode <symlink\|copy> | Override default mode for all files. |
| --no-gitignore | Skip .gitignore management. |
| --dry-run | Show what would be done without making changes. |
| --verbose, -v | Detailed output. |
| --force, -f | Overwrite existing non-scaffolded files. |
| --help, -h | Show help. |
| --version | Show version. |
Symlink vs copy
| | Symlink (default) | Copy | |---|---|---| | Changes propagate from package | Yes (automatically) | No (must re-run scaffold) | | File appears as regular file | No (it's a symlink) | Yes | | Directory mappings | Single directory symlink | Recursive per-file copy | | Build tool compatibility | Most tools handle symlinks | Universal |
Use symlink (default) for development — changes in node_modules are
reflected immediately. Use copy if your build tooling doesn't follow
symlinks.
.gitignore management
When gitignore is enabled (default), scaffolded files are automatically added
to a managed section in .gitignore:
# >>> npm-scaffold (managed section, do not edit) >>>
/src/components/button/
/src/components/card/
/src/stories/components/button.stories.tsx
# <<< npm-scaffold <<<This section is updated on every run. Existing .gitignore content outside the
managed section is preserved. Files removed from scaffold mappings are
automatically cleaned from the managed section on the next run.
Set "gitignore": false in the project config or pass --no-gitignore to
disable this behavior.
Conflict detection
If two allowed packages map to the same destination path, npm-scaffold will
error with a clear message. Resolve conflicts by adding a per-file override in
the consuming project's file-mapping to disable one of the conflicting
entries.
Example: multi-project component sharing
Shared package (@freelygive/casa-brugal-components):
{
"name": "@freelygive/casa-brugal-components",
"npmScaffold": {
"file-mapping": {
"components/button": "components/button",
"components/card": "components/card",
"stories/components/button.stories.tsx": "stories/button.stories.tsx"
}
}
}Consuming project:
{
"scripts": {
"postinstall": "npm-scaffold"
},
"npmScaffold": {
"allowed-packages": ["@freelygive/casa-brugal-components"],
"base-path": "src",
"locations": {
"skills": ".claude/skills"
}
}
}Result after npm install:
src/components/button/ -> node_modules/@freelygive/.../components/button/
src/components/card/ -> node_modules/@freelygive/.../components/card/
src/stories/components/button.stories.tsx -> node_modules/@freelygive/.../stories/button.stories.tsxLicense
GPL-2.0-or-later
