NOTE fst is in early beta, docs are subject to change and may not be accurate.
Quickstart:
npm install --save-dev @acrontum/fst
Basic usage:
npx fst [options] <path_to_recipe_file>
See npx fst --help
for more options, or see API.
interface RecipeSchema {
/**
* unique name of the recipe (for use with depends and passing data between files)
*/
name?: string;
/**
* file, recipe, path to file(s), url, or repository to fetch templates from
*/
from?: string;
/**
* output folder
*/
to?: string;
/**
* arbitrary data store
*/
data?: any;
/**
* array of names of other recipes which must be built first
*/
depends?: string[];
/**
* lifecycle cli / shell commands
*/
scripts?: { before?: string; after?: string };
/**
* path to render handler js file or handler function
*/
fileHandler?: string | RenderFunction;
/**
* dependent / sub recipes
*/
recipes?: RecipeSchema[];
/**
* list of folders to skip when copying or generating
*/
excludeDirs?: string[];
/**
* when cloning, only clone certain folders
*/
includeDirs?: string[];
}
{
"recipes": [
{
"name": "openapi-spec",
"to": "spec",
"scripts": {
"after": "npm run build"
}
},
{
"name": "server",
"from": "https://mytemplates.test/server-template",
"to": "backend",
"fileHandler": "server-builder.js",
"recipes": [
{
"name": "client",
"from": "./client",
"to": "./client",
"scripts": {
"after": "node link-client.js"
}
}
]
},
{
"name": "docker-compose",
"from": "https://fstr-std.com/compose",
"to": "docker",
"depends": ["openapi-spec", "server", "client"],
"excludeDirs": ["volumes"]
}
]
}
All recipe block paths are relative to the parent recipe's "to"
section (defaulting to './'
when not present. In the case of the root, it's relative to where the recipe was called from (cwd).
Eg:
For this file setup:
.
├── demo.fstr.json
├── scripts
│ ├── child-script.js
│ └── parent-script.js
└── sub-template
└── readme.txt
With demo.fstr.json
:
{
"name": "demo",
"to": "./demo-output",
"fileHandler": "scripts/parent-script.js",
"recipes": [
{
"name": "demo-sub-recipe",
"from": "../sub-template",
"to": "sub-folder",
"fileHandler": "scripts/child-script.js",
"scripts": {
"before": "touch readme.txt"
}
}
]
}
Note that for demo-sub-recipe
, both "from"
and "scripts"
are relative to the parent "to"
, wheras the top-level script path ('scripts/parent-script.js'
) is relative to the recipe file (in this case, './'
).
For the sub-recipe, since both "from"
and "scripts"
read from folders in the root, their paths are parsed as ./demo-output/../sub-template
and ./demo-output/../scripts/child-script.js
(respectively).
Running the recipe file would then generate this folder structure:
.
├── demo.fstr.json
├── demo-output <-- generated
│ └── sub-folder
│ └── readme.txt
├── scripts
│ ├── child-script.js
│ └── parent-script.js
└── sub-template
└── readme.txt
Import and run scripts at different points in the runtime.
Runs after source material exists on disk (after pulling from remote, or on finding local path), but before the render.
{
// ...
"scripts": {
"before": "rm -rf old-files"
}
}
Runs after render and all files copied. Useful for cleanup or logging.
{
// ...
"scripts": {
"after": "./build.sh && cp output ../release"
}
}
Used when registering callbacks for during render.
fileHandler?: (recipe: Recipe, renderer: Renderer) => void | Promise<void>;
FST will search in the current directory of the recipe for the file handler script and, if that does not exist, in the temp template source folder.
import * as nunjucks from 'nunjucks';
import { promises } from 'fs';
import { RenderFunction, Recipe, Renderer, VirtualFile } from '@acrontum/filesystem-template';
const render: RenderFunction = (recipe: Recipe, renderer: Renderer) => {
// recipe contains a name-indexed map of other recipes that have run
const parentRecipeData = recipe.map['parent-name'].data;
renderer.onFile(async (node: VirtualFile) => {
if (node.name === 'ignore-me.txt') {
node.skip = true; // skip generating this file
return;
}
if (!node.name.endsWith('.njk')) {
// if node is not skipped, and has no outputs, the default rendering will be to copy the file or folder
return;
}
const njkTemplate = await promises.readFile(node.fullSourcePath, 'utf8');
// the parent node may be a folder which generated in multiple places, meaning the source file will have multple outputs
for (const output of node.getGenerationTargets()) {
const content = await nunjunks.renderFile(njkTemplate, { ...parentRecipeData, ...recipe.data });
await promises.writeFile(output, content);
node.outputs.push(output);
}
});
};
fst <options | RECIPE>
options:
-c, --cache Do not delete fetched files between runs
-o, --output PATH Folder to output files
-p, --parallel COUNT Max number of concurrent recipe parsing (default 10)
-r, --recipe RECIPE Add recipe to builder (file or URL)
-s, --silent Log less verbosely
-v, --verbose Log more verbosely
-e, --exclude PATHS Skip PATHS in template generation (default node_modules,.git)
-b, --no-buffer Disable log buffering (default true unless not tty, CI=true, or TERM=dumb)
Try:
npx fst --help
- write more tests
- update docs
- re-implement cli params (include / exclude files, cache, parallel, etc)
- re-implement recipe params (include / exclude files, cache, parallel, etc)