Skip to content

Blocks

A Block defines the logic to create a portion of a repository. Each Block is associated with a parent Base. Blocks can then be listed in Presets associated with the same Base.

Blocks can be individually added and removed by users when customizing their repository.

Production

Blocks define their logic for created repository portions in a produce() function. produce() returns a Creation describing any produced output.

When create scaffolds a repository from a Preset, it merges together the produced outputs from its listed Blocks.

For example, this Block describes creating a .nvmrc file:

import { base } from "./base";
export const blockNvmrc = base.createBlock({
produce() {
return {
files: {
".nvmrc": "20.12.2",
},
};
},
});

That blockNvmrc can then be listed in a Preset’s blocks array:

import { base } from "./base";
import { blockNvmrc } from "./blockNvmrc";
export const presetVersioned = base.createPreset({
blocks: [
blockNvmrc,
// ...
],
});

That presetVersioned would then produce an .nvmrc file with text content 20.12.2 when run.

.nvmrc
20.12.2

Options

Each Block runs with the options defined by its parent Base.

For example, a Base with a name option could create a Block that generates part of a README.md file:

import { base } from "./base";
export const blockREADME = base.createBlock({
produce({ options }) {
return {
files: {
"README.md": `# ${options.name}`,
},
};
},
});

If create is run with --name My Repository, a README.md would be generated with that as its heading:

README.md
# My Repository

Addons

Blocks can define additional optional data called “Addons” that they can be receive from other Blocks. Blocks define Addons as the properties for a Zod object schema and then receive them in their context.

For example, this Block takes in a string array under a names Addon, to be printed in a names.txt file:

import { z } from "zod";
import { base } from "./base";
export const blockNames = base.createBlock({
addons: {
names: z.array(z.string()).default([]),
},
async produce({ addons }) {
return {
files: {
"names.txt": addons.names.join("\n"),
},
};
},
});

Other Blocks may produce Addons to be given to any other Blocks. These Addons will be merged together when a Preset containing the Blocks is run.

For example, the following Names Block receives composed Addons from the FruitNames Block:

import { z } from "zod";
import { base } from "./base";
export const blockNames = base.createBlock({
addons: {
names: z.array(z.string()).default([]),
},
async produce({ addons }) {
return {
files: {
"names.txt": addons.names.join("\n"),
},
};
},
});
export const blockFruitNames = base.createBlock({
async produce() {
return {
addons: [
blockNames({
names: ["apple", "banana", "cherry"],
}),
],
};
},
});

A Preset containing both Blocks would then produce a names.txt file with those three names as lines in its text:

names.txt
apple
banana
cherry

Merging

At runtime, the create engine will often need to re-run Blocks continuously as they receive Addons from other Blocks. Blocks will be re-run whenever other Blocks signal new Addon data to them that they haven’t yet seen. This allows Blocks to not need any explicit indication of what order to run in.

Addons are merged by concatenating arrays and removing duplicate elements. Duplicates are detected by hash-object object hashing.

For example, given the following two Addons to be merged:

[
{ name: "First", steps: ["a", "b"] },
{ name: "Second", steps: ["c", "d"] },
];
[
{ name: "Second", steps: ["c", "d"] },
{ name: "Third", steps: ["e", "f"] },
],

The merged result would be:

[
{ name: "First", steps: ["a", "b"] },
{ name: "Second", steps: ["c", "d"] },
{ name: "Third", steps: ["e", "f"] },
];
Made with 💝 in Boston by Josh Goldberg.