diff --git a/.gitignore b/.gitignore index 6be2fb12..6d5e6cc4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /coverage /node_modules -.presetter +/vitest.config.ts +/.prettierrc.json diff --git a/README.md b/README.md index 1d35ad51..57e6f323 100644 --- a/README.md +++ b/README.md @@ -2,309 +2,479 @@
-_Setup build settings from a template, quick and right!_ +_Set up your build configurations from templates—quickly and accurately!_ -•   [Quick Start](#quick-start)   •   [Concept](#concept)   •   [Known Limitations](#known-limitations)   •   [FAQ](#faq)   •   [About](#about)   • +•   [Quick Start](#quick-start)   •   [Concept](#concept)   •   [FAQ](#faq)   •   [About](#about)   •
-Sharing configurations for building tools across projects is painful. How many time you've copied configs for `babel`, `eslint`, `vitest`, `typescript` or the life cycle scripts in `package.json`? -How many dev dependencies you have to install before you can kick start a project? +Managing shared build configurations across projects can be tedious. How often have you copied over settings for `babel`, `eslint`, `vitest`, `typescript`, or the scripts in `package.json`? +How many dependencies did you have to install before you could even start a new project? -What's more, what if you want to update configs for all projects? :man_facepalming: +And when it’s time to update those configurations across multiple projects... 😩 -**Presetter is a utility for setting up building tools for your project from a template.** This means with just only two dev packages, namely this package and your favorite template preset, all essential development packages, such as typescript, eslint and vitest, together with their configuration files provided by the preset, are automatically setup for you upon the project's initialization. +**Presetter simplifies this process by setting up development tools from a template.** +With just two packages - Presetter and your preferred preset — all your essential development tools (e.g., TypeScript, ESLint, Vitest) and their configurations are set up automatically during initialization. ![Before and After](assets/before-and-after.jpg) +--- + ## Quick Start -1. Bootstrap your project with a preset (e.g. [presetter-preset-esm](packages/preset-esm)) +### 1. **Add Presetter and a preset to your project** -```shell -npx presetter use +Choose a preset, such as [presetter-preset-esm](packages/preset-esm), and add it along with Presetter to your `devDependencies`. Additionally, define a `bootstrap` script in your `package.json` to initialize the preset. + +```json +{ + "scripts": { + "bootstrap": "presetter bootstrap" + }, + "devDependencies": { + "presetter": "", + "presetter-preset-esm": "" + } +} +``` + +### 2. **Create a preset configuration file** + +Create a `presetter.config.ts` file in the same directory as your `package.json` to specify the preset: + +#### Basic Example + +```typescript +// presetter.config.ts + +// use a preset as is + +// replace `presetter-preset-esm` with your desired preset +export { default } from 'presetter-preset-esm'; +``` + +#### With Customization + +```typescript +// presetter.config.ts + +// use a preset with customization + +import esm from 'presetter-preset-esm'; +import { preset } from 'presetter'; + +export default preset('', { + extends: [esm], // extend your chosen preset + assets: { + 'eslint.config.ts': { + default: [ + { + rules: { + // custom rules to override the preset + }, + }, + ], + }, + }, +}); ``` -That's. One command and you're set. +> _Note:_ Yes, `presetter.config.ts` itself functions as a preset! -2. Develop and run life cycle scripts provided by the preset +### 3. **Install dependencies** -At this point, all development packages specified in the preset are installed, -and now you can try to run some example life cycle scripts provided by the preset (e.g. try `npx run test`). +Run your package manager's installation command (e.g., `npm install`) to install all necessary dependencies. + +After installation, all required configuration files will be automatically generated, enabling you to start development immediately. + +### 4. **Start developing** + +You're all set! Use the lifecycle scripts provided by the preset to begin your development workflow. For instance, try running: + +```shell +npx run test +``` ![Demo](assets/demo.gif) +--- + ## Concept -The concept comprises two part: [**presetter**](packages/presetter) (this package) and a **preset**, which you can easily [create one for your own requirement](#how-to-create-a-preset). +Presetter revolves around two main components: [**presetter**](packages/presetter) (the utility) and a **preset**. You can even [create your own custom preset](#how-to-create-a-preset). + +--- + +### **presetter** -### presetter +Presetter handles two core tasks: -Presetter is a utility for two tasks: +1. **Setting up your development environment:** -1. setting up a development environment for a project by - - installing development dependencies specified in the preset without polluting package.json - - hardlinking or symlinking configuration files (e.g. `.babelrc`) from the preset module to the project root -2. merging life-cycle scripts from the template and the local version in package.json + - Installs development dependencies defined by the preset without modifying your `package.json`. + - Generates configuration files (e.g., `.babelrc`) in your project root based on the preset. -[SEE HERE FOR THE CLI USAGE](packages/presetter#usage) +2. **Merging lifecycle scripts:** -#### Life-Cycle Scripts + - Combines lifecycle scripts from the preset with your local `package.json`. -When you run `presetter run ` (or its alias `run `), presetter will perform the following: +[Learn more about CLI usage](packages/presetter#usage). -1. Combine the local scripts and those provided by the preset -2. Backup `package.json` as `~package.json` -3. Place the combined script into a temporary `package.json` -4. Run the task via `npm run ` as usual -5. Restore the original `package.json` after running the task +#### **Lifecycle Scripts** -_PROTIPS_: -Local scripts always have a higher priority than the template scripts. -So you can always customize the life cycle script by putting your own version into `package.json`. +When you run `presetter run ` (or its alias `run `), Presetter: -Upon running a life-cycle script involving the `run ` command in `package.json`, presetter will automatically resolve the task according to the template, so that you can always use the conventional `npm run ` as usual. +1. Merges lifecycle scripts from the preset with your local `package.json`. +2. Executes the task using `@npmcli/run-script`. -For example, with the following template and local `package.json`, -presetter will generate a `package.json` with the content below before running the script. +_Pro Tip:_ Your local scripts always take priority over preset scripts, so you retain full control over customizations. -**Template** +**Example:** Given the following preset and local `package.json` files: + +**Preset** ```json { "scripts": { "build": "tsc", "prepare": "npm run lint && npm run build", - "lint": "eslint **/*.ts", + "lint": "eslint *_/_.ts", "test": "vitest" } } ``` -**Local package.json** +**Local `package.json`** ```json { "scripts": { "build": "run build", - "lint": "eslint --fix **/*.ts", + "lint": "eslint --fix *_/_.ts", "coverage": "run test -- --coverage" } } ``` -**Output** +**Resulting `package.json` during execution** ```json { "scripts": { "build": "tsc", "prepare": "npm run lint && npm run build", - "lint": "eslint --fix **/*.ts", + "lint": "eslint --fix *_/_.ts", "test": "vitest", "coverage": "vitest --coverage" } } ``` -### preset +--- + +### **preset** + +A preset is a reusable bundle of configurations and dependencies. For example, see [presetter-preset-esm](/packages/preset-esm). + +A preset typically includes: + +1. **Development dependencies:** Defined as `peerDependencies` and installed automatically by Presetter. +2. **Configuration files:** Hardlinked or symlinked to your project root. +3. **Lifecycle scripts:** Templates that integrate seamlessly with your local scripts. + +Presets are highly customizable. Use the `override` field in `presetter.config.ts` to adjust configurations dynamically during installation and execution. +[Check out an example preset](#how-to-create-a-preset) for more details. + +--- + +## FAQ + +### How to Create a Preset? + +Creating a preset is straightforward. You write a preset to configure your project, or you can either export a preset as a npm package to share with your team. Follow these steps to write an npm package that exports a default function with the required signature: + +Creating a preset is straightforward. You can write a preset to configure your project or export it as an npm package to share with your team. Follow these steps to write a preset file that exports a default function with the required signature: + +```typescript +// either presetter.config.ts for configuring your project or the entry file (e.g. index.ts) for exporting as a npm package + +import { preset } from 'presetter'; + +export default preset('preset-name', (context) => { + return { + extends: [ + // define any presets to extend here + ], + variables: { + // define your variables here + }, + scripts: { + // define your scripts here + }, + assets: { + // define your assets here + }, + override: { + // define any overrides here + variables: { + // override variables here + }, + scripts: { + // override scripts here + }, + assets: { + // override assets here + }, + }, + }; +}); +``` + +Alternatively, you can export a configuration object if the preset does not require dynamic generation. This approach is more performant for most presets: + +```typescript +// either presetter.config.ts for configuring your project or the entry file (e.g. index.ts) for exporting as an npm package + +import { preset } from 'presetter'; + +export default preset('preset-name', { + extends: [ + // define any presets to extend here + ], + variables: { + // define your variables here + }, + scripts: { + // define your scripts here + }, + assets: { + // define your assets here + }, + override: { + // define any overrides here + }, +}); +``` -A preset is a collection of configuration to be shared. -An example can be found in [presetter-preset-esm](/packages/preset-esm) which is also used for developing presetter and [other demo presets below](#demo-presets). +#### Example Preset + +Here is an example of a simple preset: + +```typescript +import { preset } from 'presetter-types'; + +export default preset('my-preset', (context) => { + return { + variables: { + root: '.', + source: 'src', + output: 'dist', + }, + scripts: { + build: 'tsc', + test: 'vitest', + }, + assets: { + 'tsconfig.json': { + compilerOptions: { + target: 'ES2020', + module: 'commonjs', + outDir: 'dist', + rootDir: 'src', + }, + }, + '.gitignore': ['node_modules', 'dist'], + }, + override: { + assets: { + 'tsconfig.json': (current, { variables }) => ({ + ...current, + include: [`${variables.source}/**/*`], + }), + }, + }, + }; +}); +``` -A preset contains three parts: +### How to Configure Presetter for Monorepos? -1. A set of development packages declared as `peerDependencies` in the `package.json`: - For a project adopting a preset, during its installation these packages will be installed by presetter automatically without making changes to the project's package.json. -2. A set of configuration files: - These configuration files are to be hardlinked (or symlinked if hardlink is not possible) to the adopting project's root. -3. A set of life cycle script template: - These scripts provide the base where the `presetter run` command will use for merging. +To create a preset for a monorepo, define a preset that sets up the configurations for the monorepo. Individual projects within the monorepo can then extend this preset to meet their specific needs. Here is an example of a monorepo preset: -For 1, the set of development packages to be installed is exported via `package.json`. -For 2 & 3, the configuration is exported via the default function in the preset package ([example](/packages/preset-esm/source/index.ts)). +```typescript +import { preset } from 'presetter'; -#### Config Extension +export default preset('monorepo', (context) => { + return context.root === import.meta.dirname ? { + // configurations for the monorepo + ... + }: { + // configurations for any child projects without a presetter.config.ts + ... + } +}); +``` -To overwrite part of the preset configuration (e.g. add a rule to the eslint config file template), -you can specify the new configuration under the `config` parameter in the configuration file (`.presetterrc` or `.presetterrc.json`). +In individual projects, you can extend the monorepo preset and override configurations as needed. Presetter will always look for the nearest presetter.config.ts file in the parent directories. If it does not find one, it will use the configurations defined in the monorepo preset. -During installation and life cycle script execution, -the content of this parameter will be passed to the configurator function provided by the preset package. -With this parameter, the preset can dynamically export customized config files and life cycle scripts. -You can [checkout the example preset to see how it work](/packages/preset-esm/source/index.ts). +```typescript +// /monorepo/path/to/project/presetter.config.ts +import { preset } from 'presetter'; -## Known Limitations +import monorepo from '../path/to/root/presetter.config.ts'; -#### Missing dependent development packages after `npm install ` +export default preset('project', { + extends: [monorepo], // extend the monorepo preset + override: { + // override configurations here + }, +}); +``` -In npm v5 & v6, any subsequent `npm install ` command will cause the installed development packages to be removed after installation. -This is due to a side effect of the introduction of `package-lock.json` in npm v5, -where the npm dedupe process begins to remove any packages not recorded in `package-lock.json` after package installation. +### How to Ignore Files? -Since the development packages are only declared as peer dependencies in the preset package, it's not recorded in `package-lock.json` and therefore the problem. +To ignore files provided by a preset, you can override the relevant asset with `null` in the override field. For example, to ignore the `.gitignore` file provided by a preset, here is how you can override it: -Currently, there are two solutions +```typescript +// presetter.config.ts +import { preset } from 'presetter'; -1. Run `presetter bootstrap` manually after each additional package installation. - This will make sure any missing dependencies will be installed again. -2. Use `yarn` to install additional packages as it won't remove any packages during the process. +import esm from 'presetter-preset-esm'; -This problem, fortunately, ~~should soon~~ has now become a history when [npm v7](https://blog.npmjs.org/post/186983646370/npm-cli-roadmap-summer-2019) was released. -The [auto peer dependencies installation](https://github.blog/2020-10-13-presenting-v7-0-0-of-the-npm-cli/) feature will now resolve this issue for good. +export default preset('project name', { + extends: [esm], + override: { + assets: { + '.gitignore': null, + }, + }, +}); +``` -## FAQ +### How to Merge a Preset with Another Preset? -#### Life cycle scripts are broken +To merge a preset with another preset, you can extend the preset in the `extends` field of the preset configuration. For example, to merge the `presetter-preset-esm` preset with another preset, here is how you can extend it: -It may be the case when a life cycle script crashed, resulting in `package.json` not be restored to its original version. -To fix the issue, you can simply replace the temporary `package.json` by its original at `~package.json`. +```typescript +// presetter.config.ts +import { preset } from 'presetter'; -#### How to create a preset? +import esm from 'presetter-preset-esm'; +import other from 'other-preset'; -It's actually rather simple. You just need to prepare an ordinary npm package with a default export with signature `(args: PresetContext) => PresetAsset | Promise`, where +export default preset('project name', { + extends: [esm, other], + override: { + // override the configuration here + }, +}); +``` -```ts -/** input for a preset configurator */ -export interface PresetContext { - /** information about the targeted project */ - target: { - /** the package name defined in the targeted project's package.json */ - name: string; - /** the root folder containing the targeted project's .presetterrc.json */ - root: string; - /** normalized package.json from the targeted project's package.json */ - package: PackageJson; - }; - /** content of .presetterrc */ - custom: PresetterConfig; -} +### What is the difference between `variables`, `scripts`, `assets` and those in `override`? -/** expected return from the configuration function from the preset */ -export interface PresetAsset { - /** list of presets to extend from */ - extends?: string[]; - /** mapping of files to be generated to its configuration template files (key: file path relative to the target project's root, value: template path) */ - template?: TemplateMap | TemplateMapGenerator; - /** list of templates that should not be created as hardlinks or symlinks */ - noSymlinks?: string[] | Generator; - /** path to the scripts template */ - scripts?: string; - /** variables to be substituted in templates */ - variable?: Record; - /** supplementary configuration applied to .presetterrc for enriching other presets */ - supplementaryConfig?: ConfigMap | ConfigMapGenerator; -} +The `variables`, `scripts`, and `assets` fields in the preset configuration object define the initial resolution. The `override` field, on the other hand, is used to customize or override the initial resolution. -/** an auxiliary type for representing a file path */ -type Path = string; -/** an auxiliary type for representing a template (either path to the template file or its content) */ -export type Template = string | Record; -/** an auxiliary type for representing a dynamic template generator */ -export type TemplateGenerator = Generator