A CLI for bootstrapping and keeping project tooling up-to-date.
- About
- How it works
- A simple preset
- Design principles
- Making presets
- A full example
- Types
- Future work & ideas
When you work on lots of packages, you end up making project templates to spin up projects faster and faster. The issue arises when you come back to an older project, the tooling is outdated, has vunrebilities or is deprecated. Then you have to spend time refreshing your memory on the setup, manually updating dependencies and re-testing everything again.
I made puggle to solve this problem, to quickly bootstrap a project with the ability to auto-upgrade it later when the template changes.
My idea is that you have your source code then you use puggle to hoist the meta-packages around it.
For example, adding prettier
or moving from husky
to yorkie
You setup presets in puggle which are a programatic way of bootstrapping directories. This creates a virtual version of your desired directory and then it gets written to disk.
When you come to update a project, puggle re-generates that virtual directory and differentially updates they files it can.
Using a preset to setup a project
# Install puggle globally
npm install -g puggle
# Initialize a project into a new folder 'new-project'
puggle init new-project
Upon returning to a project later and wanting to update it:
# Go to the project - it will have a puggle.json
cd to/your/project
# Run the update
puggle update
Below is a simple preset, it adds a folder called src
and puts an index.js
in it.
const { VDir, VFile } = require('puggle')
const indexJs = (name) => `
// App entrypoint
console.log('Hello, ${name}!')
`
module.exports = {
name: 'robb-j:sample',
version: '0.1.2',
plugins: [],
async apply(root, { targetName }) {
// Create a virtual file and fill it with a template
const file = new VFile('index.js', indexJs(targetName))
// Create a directory and put the file in it
const dir = new VDir('src', [file])
// Add the directory (and its file) to the virtual directory
root.addChild(dir)
},
}
- Be framework and language agnostic, with implementations built ontop of a common base
- Be composable with plugins to share functionality
- Allow seamless-ish upgrades to avoid updating project configs over and over again
- Be developer agnostics, you have your own presets and I have mine.
Here's how to create and test a preset locally
# Create a directory for your preset and go into it
mkdir my-preset
cd my-preset
# Create your preset file
touch index.js
# Make an empty package.json
npm init
# Add puggle as a global dependency
npm i -g puggle
# 1. Edit your preset in index.js
# 2. Set your package name to something
# - Presets must start with puggle-preset
# - Preferably user namespaced, e.g. @robb_j/puggle-preset-test
# - See "Publishing presets" below for more info
# To test locally, link the module
npm link
# Try to run it with puggle and you should see it there
puggle
# Remember to unlink it when you're finished
# -> You have to be in the same directory as your package.json
npm unlink
Puggle works by creating and manipulating virtual files then writting them all
to disk at once.
There are are several types of files you can make, they all inherit from VNode
.
Nodes are added as either PatchStrategy.placeholder
or PatchStrategy.persist
,
this is used to determine how puggle update works.
persist
- the value of the virtual file will always overwrite any local changes sincepuggle init
placeholder
- any local changes will always be kept
This is a basic text file, it has a name and its contents as strings and a PatchStrategy
.
The default strategy is always placeholder
, you have to opt-in to persist changes.
const { VFile } = require('puggle')
const fileContents = `
// Some complicated javascript
console.log('Hello, world!')
`
let indexJs = new VFile('index.js', fileContents, PatchStrategy.persist)
This type of node represents a directory which you can add files to, like in a real file system.
It has a method, #addChild
, which you use to add other files/directories to it.
You should always use this to add new nodes (it internally sets node.parent
).
It has a #find
method which you can use to retrieve child nodes,
e.g. to look for src/config/init.js
.
const { VFile, VDir } = require('puggle')
let dir = new VDir('src', [
new VFile('.env', 'SECRET=pyjamas'),
new VFile('.gitignore', '.env'),
new VDir('src', [new VNode('hello.txt', 'hi')]),
])
// Add a new child
dir.addChild(new VNode('README.md', '> coming soon'))
// Find a child
dir.find('.env')
dir.find('src/hello.txt')
This represents some form of configuration file, currently json
and yaml
are supported.
Yaml files can optionally have a comment too which is inserted at the top.
const { VConfigFile, VConfigType } = require('puggle')
const json = new VConfigFile('data.json', VConfigType.json, {
url: 'https://duck.com',
})
const yaml = new VConfigFile(
'config.yaml',
VConfigType.yaml,
{ name: 'geoff' },
{ comment: 'All about geoff' }
)
patches
Config files have two ways of storing their data, there is the initial value you
pass to it and patches that can be applied later.
This allows you to have both placeholder
and persist
-ed content in the same file.
When running puggle update
it will make sure the persit
-ed patches are kept in your file,
while the placeholder
patches will prefer local changes.
// A base config file with an empty person object
const config = new VConfigFile('data.json', VConfigType.json, {
person: {},
})
// This patch will be persit on "puggle update"
// -> e.g. If you changes the name to jim, "puggle update" would set it back to geoff
// -> It will merge objects together using lodash.merge
config.addPatch('person', PatchStrategy.persist, { name: 'geoff' })
// This patch will keep local changes after a "puggle init"
// -> e.g. if it was changes to 43, it would still be 43 after a "puggle update"
// -> You can use dot.notation to set values, this uses lodash.get
config.addPatch('person.age', PatchStrategy.palceholder, 42)
This represents an ignore file like a .gitignore
.
You pass it a set of rules and a friendly comment to explain the file.
Also useful for .npmignore
, .prettierignore
or others.
It automatically merges changes from existing files when doing puggle update
.
const { VIgnoreFile } = require('puggle')
let ignore = new VIgnoreFile('.gitignore', 'Files for git to ignore', [
'node_modules',
'coverage',
'*.env',
'.DS_Store',
])
This represents a package.json
.
It is basically a VConfigFile
with some useful npm-related helper methods.
It also sorts scripts
, dependencies
and devDependencies
alphabetically on serialize,
it made sense at the time.
const { VPackageJson } = require('puggle')
let pkg = new VPackageJson()
// Set the 'main' value of the package
// -> This will force it to stay as this value
pkg.addPatch('main', PatchStrategy.persist, 'src/index.js')
// Add a placeholder patch for a lint command
// -> Lets you customise the lint command later and your change is kept
pkg.addPatch('scripts', PatchStrategy.placeholder, {
lint: 'eslint src',
})
// Add a dependancy
// -> Finds the latest version that matches your semver range
// -> IMPORTANT: this is asynchronous! It goes away to the api to fetch the version(s)
// -> There is also #addLatestDevDependencies which is the same
// -> These marked as a PatchStrategy.persist
// -> You can pass multiple packages
await pkg.addLatestDependencies({
dotenv: '^8.x',
})
You can also use it from npmPlugin
:
const { VPackageJson, npmPlugin } = require('puggle')
module.exports = {
name: 'my-preset',
version: '1.2.3',
plugins: [npmPlugin],
apply(root) {
// Get the package.json which has already been added
const pkg = VPackageJson.getOrFail(root)
},
}
Npm plugin also asks extra questions to the user to fill in bits of the VPackageJson
.
These values get stored in the generated puggle.json
so they don't need to be asked again when you do a puggle update
Once you're happy with your preset, publish it to npm registry then install it globally on your dev machine.
For naming, puggle
will pick up any packages that match the glob */puggle-preset*
, so you could call it:
@org/puggle-presets
@user/puggle-preset-nodejs
puggle-preset-geoff
I'd reccomend using user-namespaced packages as presets should represent a user/orgs personal preferences.
For example: @robb_j/puggle-presets
, not: puggle-preset-test
.
You need to have your package.json's main
set to a script which has
// Export the preset
module.exports = {
name: 'preset',
/* your_preset_here */
}
// Or, you can export an array of presets
module.exports = [
{ name: 'preset-a' /* your_preset_here */ },
{ name: 'preset-b' /* your_preset_here */ },
]
To publish a user-namespaced preset, follow below:
# Create a version of your plugin
npm version minor
# Publish to npm with a package like @robb_j/puggle-preset-test
npm publish --access=public
# Add your package globally to you dev machine
npm i -g @robb_j/puggle-preset-test
# Test puggle sees it
puggle test-dir
For a full example, check out my personal presets:
There is a presetify
function which you can use to infer types onto your preset.
Without fully using typescript you can use your IDE's type support to help making presets.
It'll infer the type of the preset and the arguments to #apply
too.
const { presetify } = require('puggle')
module.exports = presetify({
/* type-hinting goodness */
})
Puggle is written in TypeScript and you have the actual types too, if you want to write your preset in TypeScript.
override PatchStrategy.persist
Some way of locking a file in puggle.json
so that the local version is peristed, overriding a PatchStrategy.persist
{
"persistFiles": [
// Persist a specific file
"src/somefile.js",
// Persit a key on a VConfigFile ?
"package.json#prettier.semi",
{ "config": "package.json", "key": "prettier.semi" },
["package.json", "prettier.semi"]
]
}
streamline nested directories
Create multiple directories at once with new VDir('some/nested/dir')
type syntax ~ or even from a VFile
preview a puggle update
Generate a preview of what puggle update
will do
puggle update
Will create these files:
• src/new-file.txt
These files are obsolete:
• old-config.yml
Will patch package.json
• pretter.useSemi: true => false
preview PatchStrategy.placeholder
Some way of comparing the files/values from PatchStrategy.placeholder
with the live files, so you can manually update files.
extract npm & node.js logic into its own module
Extract VPackageJson and npmPlugin into a node module, making the core language-agnostic
document plugins and questions
Document how plugins work and how to ask questions in puggle init
in-project generators
puggle add route
> route name: new-route
# added src/routes/general/new-route.ts
# added src/routes/general/__test__/new-route.spec.ts
interface Generator {
name: string
apply(root: VDir, ctx: PluginContext)
}
interface PresetChanges extends Preset {
generators: Generator[]
}
integrate with post-install binaries
Run npm install
or git init
after you've done a puggle init
or puggle update
.
It should come from the preset rather than a default.
e.g. you could have a custom first-commit message for your repo.
move to use standard-version and commitlint
When this moves to 1.x, move to use standard-version and commitlint to automatically version based on commits and generate changelogs.
This project was setup with robb-j/ts-node-base