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;
-/** an auxiliary type for representing a collection of template (key: output path, value: template definition) */
-export type TemplateMap = Record;
-/** an auxiliary type for representing a dynamic template map generator */
-export type TemplateMapGenerator = Generator;
-/** an auxiliary type for representing a config */
-export type Config = string[] | Record;
-/** an auxiliary type for representing a dynamic config generator */
-export type ConfigGenerator = Generator;
-/** an auxiliary type for representing a config map */
-export type ConfigMap = Record;
-/** an auxiliary type for representing a dynamic config map generator */
-export type ConfigMapGenerator = Generator;
+- **Initial Resolution**: The `variables`, `scripts`, and `assets` fields are used to set up the initial configuration.
+- **Override**: The `override` field is applied after the initial resolution, allowing you to customize the configuration provided by the preset. This is useful when you need to make adjustments based on the fully resolved configuration.
+
+If you only need to provide additional configurations, you can define them directly in the preset configuration object. However, be aware that these configurations may be overridden by other presets if the user extends multiple presets. Using the `override` field ensures that your customizations are applied last and are not overridden by other presets.
+
+### How to Customize a Configuration Provided by a Preset?
+
+There are two approaches to customize a configuration (either `assets` or `scripts`) provided by a preset:
+
+1. **Generate the content via a function**: If you provide a function as the value for a configuration file, the function will receive the current content of the file and the variables defined in the preset configuration. You can then return the updated content based on the current content and variables. The content returned by the function will be used as the final content of the configuration file without being merged with the current content.
+
+1. **Provide additional object for merging**: If you want to add additional configurations to the preset, you can provide the additions either in the `assets` or in the `override.assets` field. These additional configurations will be deep merged with the preset configuration.
+
+For example, to add additional files to the `.gitignore` file provided by a preset, you can provide the additional files in the .gitignore of either the `assets` or `override.assets` field:
+
+```typescript
+// presetter.config.ts
+import { preset } from 'presetter';
+
+import esm from 'presetter-preset-esm';
+
+export default preset('project name', {
+ extends: [esm],
+ assets: {
+ '.gitignore': ['additional-file'],
+ },
+});
```
-This function is a manifest generator which will be used to inform presetter what and how use the template files. For bundling other dev tools, you only need to declare them in `peerDependencies` in the `package.json` of the preset package. Presetter will pick them up and automatically install them on your target project.
+To add additional rules to the ESLint configuration provided by a preset, you can provide the additional rules like this:
+
+```typescript
+// presetter.config.ts
+import { preset } from 'presetter';
+
+import esm from 'presetter-preset-esm';
+
+export default preset('project name', {
+ extends: [esm],
+ assets: {
+ 'eslint.config.ts': {
+ default: [
+ {
+ rules: {
+ 'additional-rule': 'error',
+ },
+ },
+ ],
+ },
+ },
+});
+```
+
+Note that for ESLint configuration, if you want to add additional rules with file filters, it is recommended to use the `override` field to ensure that the additional rules are applied last. Otherwise, the additional rules may be overridden by other extended presets.
+
+---
## Demo Presets
-There are many ways to create a preset. Checkout our example presets to learn more:
+Explore these example presets to see **Presetter** in action:
-| Preset | Description |
-|----------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| [presetter-preset-essentials](/packages/preset-essentials) | A starter preset with lots of useful dev tools (e.g. eslint, vitest etc.) bundled and configuration following the best practices for a modern ESM project. |
-| [presetter-preset-cjs](/packages/preset-esm) | An extension of `presetter-preset-essentials` but loaded with tools to help you to develop an commonjs project with ease. |
-| [presetter-preset-esm](/packages/preset-esm) | An extension of `presetter-preset-essentials` but loaded with tools to help you to develop an esm project with ease. |
-| [presetter-preset-hybrid](/packages/preset-hybrid) | Another extension of `presetter-preset-esm` aiming to help you to create a dual CommonJS/ESM package without all the pains. |
-| [presetter-preset-react](/packages/preset-react) | Want to control the output path of the generated files? or which template to use based on the context? `presetter-preset-react` is an example showing you how to generate your manifest programmatically. |
-| [presetter-preset-rollup](/packages/preset-rollup) | An advanced preset showing you how to generate a content based on consolidated configs. |
-| [presetter-preset-strict](/packages/preset-strict) | Want to build a preset on top of an existing one? Check this out, it extends `presetter-preset-esm` with extra rules. |
-| [presetter-preset-web](/packages/preset-web) | Just want a preset with tools bundled? This one has only GraphQL, PostCSS and TailwindCSS bundled, with nothing extra. |
-| [@alvis/preset-gatsby](https://github.com/alvis/preset-gatsby) | How to make a preset without publishing it? Check out my personal preset. For my case, I can just use `presetter use https://github.com/alvis/preset-gatsby` to setup my dev environment for a Gatsby project. |
+| Preset | Description |
+| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
+| [presetter-preset-essentials](/packages/preset-essentials) | A foundational preset for modern ESM projects, bundling tools like ESLint and Vitest, with best-practice configurations. |
+| [presetter-preset-esm](/packages/preset-esm) | Builds on `essentials`, adding tools optimized for ESM projects. |
+| [presetter-preset-cjs](/packages/preset-cjs) | Extends `essentials` with configurations tailored for CommonJS projects. |
+| [presetter-preset-hybrid](/packages/preset-hybrid) | Aimed at creating dual CommonJS/ESM packages with minimal hassle. |
+| [presetter-preset-react](/packages/preset-react) | An opinionated preset optimized for React development. |
+| [presetter-preset-rollup](/packages/preset-rollup) | An opinionated preset optimized for Rollup development. |
+| [presetter-preset-strict](/packages/preset-strict) | Extends `presetter-preset-esm` with additional strict rules for enhanced development workflows. |
+| [presetter-preset-web](/packages/preset-web) | Extends `presetter-preset-esm` with bundling GraphQL, PostCSS, and TailwindCSS with no extras. |
-## About
+---
-This project originated from my personal pain on maintaining a number of projects with fairly similar structure, having exactly the same build and test procedures, same `.babelrc`, `tsconfig.json` etc.
-Every time when I setup a new project, I have to copy many **identical config files** such as `.babelrc`, `.lintstagedrc`, `.npmignore`, `eslint.config.ts`, `tsconfig.json`, `vitest.config.ts` to name a few,
-together with the following **40** 😱 development dependencies!!!
-
-1. @babel/cli
-1. @babel/core
-1. @babel/node
-1. @babel/plugin-proposal-class-properties
-1. @babel/plugin-proposal-decorators
-1. @babel/plugin-proposal-nullish-coalescing-operator
-1. @babel/plugin-proposal-object-rest-spread
-1. @babel/plugin-proposal-optional-chaining
-1. @babel/preset-env
-1. @babel/preset-typescript
-1. @types/node
-1. @typescript-eslint/eslint-plugin
-1. @typescript-eslint/parser
-1. babel-plugin-transform-typescript-metadata
-1. conventional-changelog-metahub
-1. cross-env
-1. eslint
-1. eslint-config-prettier
-1. eslint-plugin-eslint-comments
-1. eslint-plugin-import
-1. eslint-plugin-jsdoc
-1. eslint-plugin-no-secrets
-1. eslint-plugin-sonarjs
-1. husky
-1. leasot
-1. lint-staged
-1. npm-run-all
-1. presetter
-1. prettier
-1. shx
-1. standard-version
-1. tsx
-1. tsc-alias
-1. tsconfig-paths
-1. typescript
-1. vitest
-
-So, I imagine, if it is possible to reduce all these 40 packages into 1?
-I tried to look for a solution but no luck.
-Therefore, I make this tool and make it available to everyone who has a similar problem as me.
+## About
-### Philosophy
+This project was born out of frustration with maintaining identical configurations across multiple projects. Every new project required copying numerous files and installing dozens of dependencies.
-Every design has a design philosophy and here are those for presetter:
+With Presetter, I consolidated **40 development dependencies** into just **1 preset**, simplifying project setup and maintenance.
+Let it save you time, too!
-- Presetter should do one and only one job, which is providing building tools for the adopting project.
-- A preset should be made flexible enough to adapt to different project need while maintaining the reusability.
-- For the adopting project, updating only the preset version should be the only thing you need to do for updating the build dev dependencies and configuration files.
-- Any changes to the local config should be preserved, even during a preset update.
+### Philosophy
-### Related Projects
+- Presetter focuses solely on providing build tools for your project.
+- Presets are flexible yet reusable.
+- Updating a preset version is all you need to refresh your tools and configs.
+- Local changes are always preserved.
-Let me know if you find any similar projects.
-It would be nice to be included here.
+---
### Contributing
-Any new ideas? or got a bug? We definitely would love to have your contribution!
-
-If you have any suggestion or issue, feel free to let the community know via [issues](../../issues).
+We’d love your ideas and contributions!
+Submit issues or suggestions via [GitHub Issues](../../issues).
+See the [Contribution Guide](CONTRIBUTING.md) for more details.
-Further, read the [contribution guide](CONTRIBUTING.md) for the detail of the code structure and useful commands etc.
+---
### License
-Copyright © 2020, [Alvis Tang](https://github.com/alvis). Released under the [MIT License](LICENSE).
+Released under the [MIT License](LICENSE).
+© 2020, [Alvis Tang](https://github.com/alvis).
-[![license](https://img.shields.io/github/license/alvis/presetter.svg?style=flat-square)](https://github.com/alvis/presetter/blob/master/LICENSE)
+[![License](https://img.shields.io/github/license/alvis/presetter.svg?style=flat-square)](LICENSE)
diff --git a/package.json b/package.json
index a3ae07ad..aece11c1 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,7 @@
},
"scripts": {
"install": "npx -y only-allow pnpm && husky",
- "postinstall": "pnpm run -r bootstrap",
+ "postinstall": "presetter bootstrap && pnpm run -r bootstrap",
"build": "pnpm --recursive run build",
"coverage": "pnpm run test --coverage",
"lint": "pnpm --recursive --no-bail run --aggregate-output lint",
@@ -41,7 +41,8 @@
"presetter": "workspace:*",
"presetter-preset-esm": "workspace:*",
"presetter-preset-essentials": "workspace:*",
- "presetter-preset-strict": "workspace:*"
+ "presetter-preset-strict": "workspace:*",
+ "presetter-types": "workspace:*"
},
"devDependencies": {
"@types/node": "^20.0.0",
diff --git a/packages/preset-cjs/README.md b/packages/preset-cjs/README.md
index f87e8848..d61e79a8 100644
--- a/packages/preset-cjs/README.md
+++ b/packages/preset-cjs/README.md
@@ -22,27 +22,55 @@ With `presetter-preset-cjs`, it provides everything bundled from presetter-pres
## Quick Start
-To kick start a ESM application, what you need is to set the following in your `package.json` and follow the guide below.
+To kickstart a CommonJS application, set the following in your `package.json` and follow the guide below.
```json
{
- "type": "module",
+ "type": "commonjs",
"main": "lib/index.js",
- "types": "lib/index.d.ts"
+ "types": "lib/index.d.ts",
+ "scripts": {
+ "prepare": "run prepare",
+ "build": "run build",
+ "clean": "run clean",
+ "test": "run test",
+ "watch": "run watch",
+ "coverage": "run coverage"
+ }
}
```
[**FULL DOCUMENTATION IS AVAILABLE HERE**](https://github.com/alvis/presetter/blob/master/README.md)
-1. Bootstrap your project with presetter-preset-cjs
+### 1. Bootstrap your project with presetter-preset-cjs
-```shell
-npx presetter use presetter-preset-cjs
+On your project root, create a `presetter.config.ts` file with the following content:
+
+```typescript
+// presetter.config.ts
+export { default } from 'presetter-preset-cjs';
+```
+
+or if customization is needed. For example, you can extend the configuration with more presets:
+
+```typescript
+// presetter.config.ts
+
+import { preset } from 'presetter';
+import cjs from 'presetter-preset-cjs';
+import other from 'other-preset';
+
+export default preset('project name', {
+ extends: [cjs, other],
+ override: {
+ // override the configuration here
+ },
+});
```
-That's. One command and you're set.
+Then, install your project as usual with `npm install` or any package manager you prefer.
-1. Develop and run life cycle scripts provided by the preset
+### 2. Develop and run life cycle scripts provided by the preset
At this point, all development packages specified in the preset are installed,
and now you can try to run some example life cycle scripts (e.g. run prepare).
@@ -51,11 +79,11 @@ and now you can try to run some example life cycle scripts (e.g. run prepare).
## Project Structure
-After installation, your project file structure should resemble the following or with more configuration files if you also installed other presets such as [`presetter-preset-rollup`](https://github.com/alvis/presetter/blob/master/packages/preset-rollup).
+After installation, your project file structure should resemble the following, or include more configuration files if you also installed other presets.
Implement your business logic under `source` and prepare tests under `spec`.
-**TIPS** You can always change the source directory to other (e.g. src) by setting the `source` variable in `.presetterrc.json`. See the [customization](https://github.com/alvis/presetter/blob/master/packages/preset-esm#customization) section below for more details.
+**TIPS** You can always change the source directory to other (e.g. src) by setting the `source` variable in `presetter.config.ts`. See the [customization](https://github.com/alvis/presetter/blob/master/packages/preset-essentials#customization) section below for more details.
```plain
(root)
@@ -64,7 +92,7 @@ Implement your business logic under `source` and prepare tests under `spec`.
├─ .lintstagedrc.json
├─ .npmignore
├─ .prettierrc.json
- ├─ .presetterrc.json
+ ├─ presetter.config.ts
├─ node_modules
├─ source
│ ├─
@@ -81,52 +109,8 @@ Implement your business logic under `source` and prepare tests under `spec`.
## Customization
-By default, this preset exports a handy configuration for rollup for a typescript project.
-But you can further customize (either extending or replacing) the configuration by specifying the change in the config file (`.presetterrc` or `.presetterrc.json`).
-
-These settings are available in the `config` field in the config file. For directories, the setting is specified in the `variable` field.
-
-The structure of `.presetterrc` should follow the interface below:
-
-```ts
-interface PresetterRC {
- /** name of the preset e.g. presetter-preset-cjs */
- name: string | string[];
- /** additional configuration passed to the preset for generating the configuration files */
- config?: {
- // ┌─ configuration for other tools via other presets (e.g. presetter-preset-rollup)
- // ...
-
- /** configuration to be merged with .eslintrc */
- eslint?: Record;
- /** configuration to be merged with .lintstagedrc */
- lintstaged?: Record;
- /** patterns to be added to .gitignore */
- gitignore?: string[];
- /** patterns to be added to .npmignore */
- npmignore?: string[];
- /** configuration to be merged with .presetterrc */
- prettier?: Record;
- /** configuration to be merged with tsconfig.json */
- tsconfig?: Record;
- /** a list of config files not to be created */
- ignores?: string[];
- };
- /** relative path to root directories for different file types */
- variable?: {
- /** the directory containing the whole repository (default: .) */
- root?: string;
- /** the directory containing all source code (default: source) */
- source?: string;
- /** the directory containing all typing files (default: types) */
- types?: string;
- /** the directory containing all output tile (default: source) */
- output?: string;
- /** the directory containing all test files (default: spec) */
- test?: string;
- };
-}
-```
+By default, this preset exports a handy configuration for a typescript project.
+You can further customize (either extending or replacing) the configuration by specifying the changes in the config file `presetter.config.ts`.
## Script Template Summary
diff --git a/packages/preset-cjs/configs/scripts.yaml b/packages/preset-cjs/overrides/scripts.yaml
similarity index 100%
rename from packages/preset-cjs/configs/scripts.yaml
rename to packages/preset-cjs/overrides/scripts.yaml
diff --git a/packages/preset-cjs/configs/tsconfig.yaml b/packages/preset-cjs/overrides/tsconfig.yaml
similarity index 100%
rename from packages/preset-cjs/configs/tsconfig.yaml
rename to packages/preset-cjs/overrides/tsconfig.yaml
diff --git a/packages/preset-cjs/package.json b/packages/preset-cjs/package.json
index 6ab97b3b..ce94bcd4 100644
--- a/packages/preset-cjs/package.json
+++ b/packages/preset-cjs/package.json
@@ -20,7 +20,7 @@
"url": "git+https://github.com/alvis/presetter.git"
},
"scripts": {
- "prepare": "tsc --declaration --moduleResolution bundler --module esnext --target esnext --skipLibCheck --outdir lib source/index.ts",
+ "prepare": "tsc --declaration --moduleResolution bundler --module esnext --target esnext --skipLibCheck --outdir lib source/index.ts && tsc-esm-fix --sourceMap --target lib",
"bootstrap": "presetter bootstrap",
"build": "run build",
"coverage": "run coverage",
diff --git a/packages/preset-cjs/source/index.ts b/packages/preset-cjs/source/index.ts
index f8b56593..cf60463d 100644
--- a/packages/preset-cjs/source/index.ts
+++ b/packages/preset-cjs/source/index.ts
@@ -1,25 +1,24 @@
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
-import type { PresetAsset } from 'presetter-types';
+import essentials from 'presetter-preset-essentials';
+import { preset } from 'presetter-types';
-export type { PresetConfig, Variable } from 'presetter-preset-essentials';
+export { DEFAULT_VARIABLES } from 'presetter-preset-essentials';
+
+export type { Variables } from 'presetter-preset-essentials';
const DIR = fileURLToPath(dirname(import.meta.url));
// paths to the template directory
-const CONFIGS = resolve(DIR, '..', 'configs');
+const OVERRIDES = resolve(DIR, '..', 'overrides');
-/**
- * get the list of templates provided by this preset
- * @returns list of preset templates
- */
-export default async function (): Promise {
- return {
- extends: ['presetter-preset-essentials'],
- supplementaryConfig: {
- tsconfig: resolve(CONFIGS, 'tsconfig.yaml'),
+export default preset('presetter-preset-cjs', {
+ extends: [essentials],
+ override: {
+ assets: {
+ 'tsconfig.json': resolve(OVERRIDES, 'tsconfig.yaml'),
},
- supplementaryScripts: resolve(CONFIGS, 'scripts.yaml'),
- };
-}
+ scripts: resolve(OVERRIDES, 'scripts.yaml'),
+ },
+});
diff --git a/packages/preset-cjs/spec/index.spec.ts b/packages/preset-cjs/spec/index.spec.ts
index 774f5a03..75194295 100644
--- a/packages/preset-cjs/spec/index.spec.ts
+++ b/packages/preset-cjs/spec/index.spec.ts
@@ -1,38 +1,43 @@
import { existsSync, readdirSync } from 'node:fs';
import { resolve } from 'node:path';
-import { loadDynamicMap, resolveContext } from 'presetter';
+import { listAssetNames, resolveAssets, resolvePreset } from 'presetter';
import { describe, expect, it, vi } from 'vitest';
-import getPresetAsset from '#';
+import preset, { DEFAULT_VARIABLES as variables } from '#';
+
+import type { PresetContext } from 'presetter-types';
vi.mock('node:path', { spy: true });
-describe('fn:getPresetAsset', () => {
- it('use all templates', async () => {
- const asset = await getPresetAsset();
- const context = await resolveContext({
- graph: [{ name: 'preset', asset, nodes: [] }],
- context: {
- target: { name: 'preset', root: '/', package: {} },
- custom: { preset: 'preset' },
- },
- });
-
- // load all potential dynamic content
- await loadDynamicMap(asset.supplementaryConfig, context);
- await loadDynamicMap(asset.template, context);
-
- const CONFIGS = resolve(import.meta.dirname, '..', 'configs');
- const configs = existsSync(CONFIGS) ? readdirSync(CONFIGS) : [];
- const TEMPLATES = resolve(import.meta.dirname, '..', 'templates');
+const OVERRIDES = resolve(import.meta.dirname, '..', 'overrides');
+const TEMPLATES = resolve(import.meta.dirname, '..', 'templates');
+
+const context = {
+ root: '/path/to/project',
+ package: {},
+} satisfies PresetContext;
+
+describe('fn:preset', () => {
+ it('should use all templates', async () => {
+ const node = await resolvePreset(preset, context);
+ listAssetNames(node, { ...context, variables });
+
+ const overrides = existsSync(OVERRIDES) ? readdirSync(OVERRIDES) : [];
const templates = existsSync(TEMPLATES) ? readdirSync(TEMPLATES) : [];
- for (const path of configs) {
- expect(vi.mocked(resolve)).toBeCalledWith(CONFIGS, path);
+ for (const path of overrides) {
+ expect(vi.mocked(resolve)).toHaveBeenCalledWith(OVERRIDES, path);
}
for (const path of templates) {
- expect(vi.mocked(resolve)).toBeCalledWith(TEMPLATES, path);
+ expect(vi.mocked(resolve)).toHaveBeenCalledWith(TEMPLATES, path);
}
});
+
+ it('should be able to resolve all assets', async () => {
+ const node = await resolvePreset(preset, context);
+ const result = resolveAssets(node, context);
+
+ await expect(result).resolves.not.toThrow();
+ });
});
diff --git a/packages/preset-esm/README.md b/packages/preset-esm/README.md
index f565b849..02a3f542 100644
--- a/packages/preset-esm/README.md
+++ b/packages/preset-esm/README.md
@@ -16,41 +16,61 @@
-**presetter-preset-esm** is an opinionated extension of [**presetter-preset-essentials**](https://github.com/alvis/presetter/tree/master/packages/preset-essentials) but loaded with tools to help you to develop an common js application with ease. As the same as presetter-preset-esm, it's designed to help you get started with a typescript project in a fraction of time you usually take via [**presetter**](https://github.com/alvis/presetter).
+**presetter-preset-esm** is an opinionated extension of [**presetter-preset-essentials**](https://github.com/alvis/presetter/tree/master/packages/preset-essentials) but loaded with tools to help you to develop an ESM application with ease. As the same as [**presetter-preset-esm**](https://github.com/alvis/presetter/tree/master/packages/preset-esm), it's designed to help you get started with a typescript project in a fraction of time you usually take via [**presetter**](https://github.com/alvis/presetter).
-With `presetter-preset-esm`, it provides everything bundled from presetter-preset-esm, plus the ease of writing an commonjs application.
-
-## Features
-
-- 🔍 Searches and replaces `__dirname` and `__filename` refs with the `import.meta` equivalent
-
-- 🥹 Forget about writing the [`.js`/`.ts` extension pain](https://github.com/microsoft/TypeScript/issues/37582) for each import
-
- With this preset, estensions are automatically added post tsc. i.e. `import {foo} from './foo'` → `import {foo} from './foo.js'`
+With `presetter-preset-esm`, it provides everything bundled from presetter-preset-essentials, plus the ease of writing an esm application.
## Quick Start
-To kick start a ESM application, what you need is to set the following in your `package.json` and follow the guide below.
+To kickstart a ESM application, set the following in your `package.json` and follow the guide below.
```json
{
"type": "module",
"main": "lib/index.js",
- "types": "lib/index.d.ts"
+ "types": "lib/index.d.ts",
+ "scripts": {
+ "prepare": "run prepare",
+ "build": "run build",
+ "clean": "run clean",
+ "test": "run test",
+ "watch": "run watch",
+ "coverage": "run coverage"
+ }
}
```
[**FULL DOCUMENTATION IS AVAILABLE HERE**](https://github.com/alvis/presetter/blob/master/README.md)
-1. Bootstrap your project with presetter-preset-esm
+### 1. Bootstrap your project with presetter-preset-esm
+
+On your project root, create a `presetter.config.ts` file with the following content:
+
+```typescript
+// presetter.config.ts
+export { default } from 'presetter-preset-esm';
+```
+
+or if customization is needed. For example, you can extend the configuration with more presets:
+
+```typescript
+// presetter.config.ts
-```shell
-npx presetter use presetter-preset-esm
+import { preset } from 'presetter';
+import esm from 'presetter-preset-esm';
+import other from 'other-preset';
+
+export default preset('project name', {
+ extends: [esm, other],
+ override: {
+ // override the configuration here
+ },
+});
```
-That's. One command and you're set.
+Then, install your project as usual with `npm install` or any package manager you prefer.
-1. Develop and run life cycle scripts provided by the preset
+### 2. Develop and run life cycle scripts provided by the preset
At this point, all development packages specified in the preset are installed,
and now you can try to run some example life cycle scripts (e.g. run prepare).
@@ -59,11 +79,11 @@ and now you can try to run some example life cycle scripts (e.g. run prepare).
## Project Structure
-After installation, your project file structure should resemble the following or with more configuration files if you also installed other presets such as [`presetter-preset-rollup`](https://github.com/alvis/presetter/blob/master/packages/preset-rollup).
+After installation, your project file structure should resemble the following, or include more configuration files if you also installed other presets.
Implement your business logic under `source` and prepare tests under `spec`.
-**TIPS** You can always change the source directory to other (e.g. src) by setting the `source` variable in `.presetterrc.json`. See the [customization](https://github.com/alvis/presetter/blob/master/packages/preset-esm#customization) section below for more details.
+**TIPS** You can always change the source directory to other (e.g. src) by setting the `source` variable in `presetter.config.ts`. See the [customization](https://github.com/alvis/presetter/blob/master/packages/preset-essentials#customization) section below for more details.
```plain
(root)
@@ -72,7 +92,7 @@ Implement your business logic under `source` and prepare tests under `spec`.
├─ .lintstagedrc.json
├─ .npmignore
├─ .prettierrc.json
- ├─ .presetterrc.json
+ ├─ presetter.config.ts
├─ node_modules
├─ source
│ ├─
@@ -89,52 +109,8 @@ Implement your business logic under `source` and prepare tests under `spec`.
## Customization
-By default, this preset exports a handy configuration for rollup for a typescript project.
-But you can further customize (either extending or replacing) the configuration by specifying the change in the config file (`.presetterrc` or `.presetterrc.json`).
-
-These settings are available in the `config` field in the config file. For directories, the setting is specified in the `variable` field.
-
-The structure of `.presetterrc` should follow the interface below:
-
-```ts
-interface PresetterRC {
- /** name of the preset e.g. presetter-preset-esm */
- name: string | string[];
- /** additional configuration passed to the preset for generating the configuration files */
- config?: {
- // ┌─ configuration for other tools via other presets (e.g. presetter-preset-rollup)
- // ...
-
- /** configuration to be merged with .eslintrc */
- eslint?: Record;
- /** configuration to be merged with .lintstagedrc */
- lintstaged?: Record;
- /** patterns to be added to .gitignore */
- gitignore?: string[];
- /** patterns to be added to .npmignore */
- npmignore?: string[];
- /** configuration to be merged with .presetterrc */
- prettier?: Record;
- /** configuration to be merged with tsconfig.json */
- tsconfig?: Record;
- /** a list of config files not to be created */
- ignores?: string[];
- };
- /** relative path to root directories for different file types */
- variable?: {
- /** the directory containing the whole repository (default: .) */
- root?: string;
- /** the directory containing all source code (default: source) */
- source?: string;
- /** the directory containing all typing files (default: types) */
- types?: string;
- /** the directory containing all output tile (default: source) */
- output?: string;
- /** the directory containing all test files (default: spec) */
- test?: string;
- };
-}
-```
+By default, this preset exports a handy configuration for a typescript project.
+You can further customize (either extending or replacing) the configuration by specifying the changes in the config file `presetter.config.ts`.
## Script Template Summary
diff --git a/packages/preset-esm/configs/scripts.yaml b/packages/preset-esm/overrides/scripts.yaml
similarity index 100%
rename from packages/preset-esm/configs/scripts.yaml
rename to packages/preset-esm/overrides/scripts.yaml
diff --git a/packages/preset-esm/configs/tsconfig.yaml b/packages/preset-esm/overrides/tsconfig.yaml
similarity index 100%
rename from packages/preset-esm/configs/tsconfig.yaml
rename to packages/preset-esm/overrides/tsconfig.yaml
diff --git a/packages/preset-esm/package.json b/packages/preset-esm/package.json
index 08ac803c..77ab9f12 100644
--- a/packages/preset-esm/package.json
+++ b/packages/preset-esm/package.json
@@ -20,7 +20,7 @@
"url": "git+https://github.com/alvis/presetter.git"
},
"scripts": {
- "prepare": "tsc --declaration --moduleResolution bundler --module esnext --target esnext --skipLibCheck --outdir lib source/index.ts",
+ "prepare": "tsc --declaration --moduleResolution bundler --module esnext --target esnext --skipLibCheck --outdir lib source/index.ts && tsc-esm-fix --sourceMap --target lib",
"bootstrap": "presetter bootstrap",
"build": "run build",
"coverage": "run coverage",
diff --git a/packages/preset-esm/source/index.ts b/packages/preset-esm/source/index.ts
index f8b56593..d59840d8 100644
--- a/packages/preset-esm/source/index.ts
+++ b/packages/preset-esm/source/index.ts
@@ -1,25 +1,24 @@
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
-import type { PresetAsset } from 'presetter-types';
+import essentials from 'presetter-preset-essentials';
+import { preset } from 'presetter-types';
-export type { PresetConfig, Variable } from 'presetter-preset-essentials';
+export { DEFAULT_VARIABLES } from 'presetter-preset-essentials';
+
+export type { Variables } from 'presetter-preset-essentials';
const DIR = fileURLToPath(dirname(import.meta.url));
// paths to the template directory
-const CONFIGS = resolve(DIR, '..', 'configs');
+const OVERRIDES = resolve(DIR, '..', 'overrides');
-/**
- * get the list of templates provided by this preset
- * @returns list of preset templates
- */
-export default async function (): Promise {
- return {
- extends: ['presetter-preset-essentials'],
- supplementaryConfig: {
- tsconfig: resolve(CONFIGS, 'tsconfig.yaml'),
+export default preset('presetter-preset-esm', {
+ extends: [essentials],
+ override: {
+ assets: {
+ 'tsconfig.json': resolve(OVERRIDES, 'tsconfig.yaml'),
},
- supplementaryScripts: resolve(CONFIGS, 'scripts.yaml'),
- };
-}
+ scripts: resolve(OVERRIDES, 'scripts.yaml'),
+ },
+});
diff --git a/packages/preset-esm/spec/index.spec.ts b/packages/preset-esm/spec/index.spec.ts
index 774f5a03..75194295 100644
--- a/packages/preset-esm/spec/index.spec.ts
+++ b/packages/preset-esm/spec/index.spec.ts
@@ -1,38 +1,43 @@
import { existsSync, readdirSync } from 'node:fs';
import { resolve } from 'node:path';
-import { loadDynamicMap, resolveContext } from 'presetter';
+import { listAssetNames, resolveAssets, resolvePreset } from 'presetter';
import { describe, expect, it, vi } from 'vitest';
-import getPresetAsset from '#';
+import preset, { DEFAULT_VARIABLES as variables } from '#';
+
+import type { PresetContext } from 'presetter-types';
vi.mock('node:path', { spy: true });
-describe('fn:getPresetAsset', () => {
- it('use all templates', async () => {
- const asset = await getPresetAsset();
- const context = await resolveContext({
- graph: [{ name: 'preset', asset, nodes: [] }],
- context: {
- target: { name: 'preset', root: '/', package: {} },
- custom: { preset: 'preset' },
- },
- });
-
- // load all potential dynamic content
- await loadDynamicMap(asset.supplementaryConfig, context);
- await loadDynamicMap(asset.template, context);
-
- const CONFIGS = resolve(import.meta.dirname, '..', 'configs');
- const configs = existsSync(CONFIGS) ? readdirSync(CONFIGS) : [];
- const TEMPLATES = resolve(import.meta.dirname, '..', 'templates');
+const OVERRIDES = resolve(import.meta.dirname, '..', 'overrides');
+const TEMPLATES = resolve(import.meta.dirname, '..', 'templates');
+
+const context = {
+ root: '/path/to/project',
+ package: {},
+} satisfies PresetContext;
+
+describe('fn:preset', () => {
+ it('should use all templates', async () => {
+ const node = await resolvePreset(preset, context);
+ listAssetNames(node, { ...context, variables });
+
+ const overrides = existsSync(OVERRIDES) ? readdirSync(OVERRIDES) : [];
const templates = existsSync(TEMPLATES) ? readdirSync(TEMPLATES) : [];
- for (const path of configs) {
- expect(vi.mocked(resolve)).toBeCalledWith(CONFIGS, path);
+ for (const path of overrides) {
+ expect(vi.mocked(resolve)).toHaveBeenCalledWith(OVERRIDES, path);
}
for (const path of templates) {
- expect(vi.mocked(resolve)).toBeCalledWith(TEMPLATES, path);
+ expect(vi.mocked(resolve)).toHaveBeenCalledWith(TEMPLATES, path);
}
});
+
+ it('should be able to resolve all assets', async () => {
+ const node = await resolvePreset(preset, context);
+ const result = resolveAssets(node, context);
+
+ await expect(result).resolves.not.toThrow();
+ });
});
diff --git a/packages/preset-essentials/README.md b/packages/preset-essentials/README.md
index aaff113a..37f607a5 100644
--- a/packages/preset-essentials/README.md
+++ b/packages/preset-essentials/README.md
@@ -28,17 +28,51 @@
## Quick Start
+To kickstart a ESM application, set the following in your `package.json` and follow the guide below.
+
+```json
+{
+ "type": "module",
+ "main": "lib/index.js",
+ "types": "lib/index.d.ts",
+ "scripts": {
+ "prepare": "run prepare",
+ "build": "run build",
+ "clean": "run clean",
+ "test": "run test",
+ "watch": "run watch",
+ "coverage": "run coverage"
+ }
+}
+```
+
[**FULL DOCUMENTATION IS AVAILABLE HERE**](https://github.com/alvis/presetter/blob/master/README.md)
-1. Bootstrap your project with [presetter-preset-esm](https://github.com/alvis/presetter/tree/master/packages/preset-esm) or [presetter-preset-cjs](https://github.com/alvis/presetter/tree/master/packages/preset-cjs)
+```typescript
+// presetter.config.ts
+export { default } from 'presetter-preset-esm';
+```
+
+or if customization is needed. For example, you can extend the configuration with more presets:
+
+```typescript
+// presetter.config.ts
-```shell
-npx presetter use presetter-preset-esm
+import { preset } from 'presetter';
+import esm from 'presetter-preset-esm';
+import other from 'other-preset';
+
+export default preset('project name', {
+ extends: [esm, other],
+ override: {
+ // override the configuration here
+ },
+});
```
-That's. One command and you're set.
+Then, install your project as usual with `npm install` or any package manager you prefer.
-2. Develop and run life cycle scripts provided by the preset
+### 2. Develop and run life cycle scripts provided by the preset
At this point, all development packages specified in the preset are installed,
and now you can try to run some example life cycle scripts (e.g. run prepare).
@@ -47,19 +81,20 @@ and now you can try to run some example life cycle scripts (e.g. run prepare).
## Project Structure
-After installation, your project file structure should resemble the following or with more configuration files if you also installed other presets such as [`presetter-preset-rollup`](https://github.com/alvis/presetter/blob/master/packages/preset-rollup).
+After installation, your project file structure should resemble the following, or include more configuration files if you also installed other presets.
Implement your business logic under `source` and prepare tests under `spec`.
-**TIPS** You can always change the source directory to other (e.g. src) by setting the `source` variable in `.presetterrc.json`. See the [customization](https://github.com/alvis/presetter/blob/master/packages/preset-essentials#customization) section below for more details.
+**TIPS** You can always change the source directory to other (e.g. src) by setting the `source` variable in `presetter.config.ts`. See the [customization](https://github.com/alvis/presetter/blob/master/packages/preset-essentials#customization) section below for more details.
-```
+```plain
(root)
├─ .git
├─ .husky
+ ├─ .lintstagedrc.json
├─ .npmignore
├─ .prettierrc.json
- ├─ .presetterrc.json
+ ├─ presetter.config.ts
├─ node_modules
├─ source
│ ├─
@@ -76,54 +111,8 @@ Implement your business logic under `source` and prepare tests under `spec`.
## Customization
-By default, this preset exports a handy configuration for rollup for a typescript project.
-But you can further customize (either extending or replacing) the configuration by specifying the change in the config file (`.presetterrc` or `.presetterrc.json`).
-
-These settings are available in the `config` field in the config file. For directories, the setting is specified in the `variable` field.
-
-The structure of `.presetterrc` should follow the interface below:
-
-```ts
-interface PresetterRC {
- /** name of the preset e.g. presetter-preset-essentials */
- name: string | string[];
- /** additional configuration passed to the preset for generating the configuration files */
- config?: {
- // ┌─ configuration for other tools via other presets (e.g. presetter-preset-rollup)
- // ...
-
- /** configuration to be merged with .eslintrc */
- eslint?: Record;
- /** configuration to be merged with .lintstagedrc */
- lintstaged?: Record;
- /** patterns to be added to .gitignore */
- gitignore?: string[];
- /** patterns to be added to .npmignore */
- npmignore?: string[];
- /** configuration to be merged with .presetterrc */
- prettier?: Record;
- /** configuration to be merged with tsconfig.json */
- tsconfig?: Record;
- /** a list of config files not to be created */
- ignores?: string[];
- };
- /** relative path to root directories for different file types */
- variable?: {
- /** the directory containing the whole repository (default: .) */
- root?: string;
- /** the directory containing all generated code (default: generated) */
- generated?: string;
- /** the directory containing all source code (default: source) */
- source?: string;
- /** the directory containing all typing files (default: types) */
- types?: string;
- /** the directory containing all output tile (default: source) */
- output?: string;
- /** the directory containing all test files (default: spec) */
- test?: string;
- };
-}
-```
+By default, this preset exports a handy configuration for a typescript project.
+You can further customize (either extending or replacing) the configuration by specifying the changes in the config file `presetter.config.ts`.
## Script Template Summary
@@ -135,7 +124,3 @@ interface PresetterRC {
- **`run coverage`**: Run all test with coverage report
- **`run release`**: Bump the version and automatically generate a change log
- **`run release -- --prerelease `**: Release with a prerelease tag
-
-## Notes
-
-- Since git 2.32 ([released on 2020-06-06](https://lore.kernel.org/lkml/xmqqa6o3xj2e.fsf@gitster.g/T/#u)), git no longer follows `.gitignore` as a symlink. Therefore, the packaged `.gitignore` will no longer symlinked but created on the root directory of the project instead.
diff --git a/packages/preset-essentials/package.json b/packages/preset-essentials/package.json
index 2b8c6c8a..816ad0fb 100644
--- a/packages/preset-essentials/package.json
+++ b/packages/preset-essentials/package.json
@@ -19,14 +19,6 @@
".": {
"default": "./lib/index.js",
"types": "./lib/index.d.ts"
- },
- "./eslint.config": {
- "default": "./lib/eslint.config.js",
- "types": "./lib/eslint.config.d.ts"
- },
- "./vitest.config": {
- "default": "./lib/vitest.config.js",
- "types": "./lib/vitest.config.d.ts"
}
},
"repository": {
@@ -34,7 +26,7 @@
"url": "git+https://github.com/alvis/presetter.git"
},
"scripts": {
- "prepare": "tsc --declaration --moduleResolution bundler --module esnext --target esnext --skipLibCheck --outdir lib source/index.ts source/eslint.config.ts source/vitest.config.ts",
+ "prepare": "tsc --declaration --moduleResolution bundler --module esnext --target esnext --skipLibCheck --outdir lib source/index.ts && tsc-esm-fix --sourceMap --target lib",
"bootstrap": "presetter bootstrap",
"build": "run build",
"coverage": "run coverage --",
diff --git a/packages/preset-essentials/source/eslint.config.ts b/packages/preset-essentials/source/eslint.config.ts
deleted file mode 100644
index fbc17184..00000000
--- a/packages/preset-essentials/source/eslint.config.ts
+++ /dev/null
@@ -1,295 +0,0 @@
-/* eslint-disable max-lines */
-
-import jsdoc from 'eslint-plugin-jsdoc';
-import tseslint from 'typescript-eslint';
-
-import eslint from '@eslint/js';
-import comments from '@eslint-community/eslint-plugin-eslint-comments/configs';
-import prettier from 'eslint-config-prettier';
-import imports from 'eslint-plugin-import';
-
-import type { Linter } from 'eslint';
-
-// NOTE: don't specify the files to be linted or ignored here as it will case ts eslint rules to fail
-
-const DOUBLE_OR_HALVE = 2;
-
-export default tseslint.config(
- eslint.configs.recommended, // eslint recommended rules
- ...tseslint.configs.recommendedTypeChecked, // typescript-specific rules
- ...tseslint.configs.stylisticTypeChecked, // typescript-specific rules
- comments.recommended, // comment formatting
- jsdoc.configs['flat/recommended'], // documentation
- prettier, // ignore formatting issue
- {
- name: 'presetter-preset-esm',
- languageOptions: {
- parserOptions: {
- projectService: true,
- tsconfigRootDir: import.meta.dirname,
- },
- },
- plugins: { jsdoc, import: imports },
- settings: {
- 'import/internal-regex': /^#/,
- 'import/resolver': {
- typescript: true,
- node: true,
- },
- 'jsdoc': {
- mode: 'typescript',
- },
- },
- rules: {
- // Extension Rules //
-
- ...imports.flatConfigs.recommended.rules, // import/export
- ...imports.flatConfigs.typescript.rules, // import/export
-
- // Documentation //
-
- 'jsdoc/tag-lines': ['error', 'never'], // doesn't need a new line after each description
- 'jsdoc/require-description': [
- // a description is a must
- 'warn',
- {
- checkConstructors: false,
- checkGetters: false,
- checkSetters: false,
- },
- ],
- 'jsdoc/require-jsdoc': [
- // all functions must be documented
- 'warn',
- {
- require: {
- ClassDeclaration: true,
- ClassExpression: true,
- FunctionDeclaration: false,
- FunctionExpression: true,
- MethodDefinition: false,
- },
- exemptEmptyConstructors: true,
- contexts: [
- // for non-exported functions
- 'Program > TSDeclareFunction:not(TSDeclareFunction + TSDeclareFunction)',
- 'Program > FunctionDeclaration:not(TSDeclareFunction + FunctionDeclaration)',
- // for exported functions
- 'ExportNamedDeclaration[declaration.type=TSDeclareFunction]:not(ExportNamedDeclaration[declaration.type=TSDeclareFunction] + ExportNamedDeclaration[declaration.type=TSDeclareFunction])',
- 'ExportNamedDeclaration[declaration.type=FunctionDeclaration]:not(ExportNamedDeclaration[declaration.type=TSDeclareFunction] + ExportNamedDeclaration[declaration.type=FunctionDeclaration])',
- // for class methods
- 'MethodDefinition[value.body=null]:not(MethodDefinition[value.body=null] + MethodDefinition[value.body=null])',
- 'MethodDefinition[value.body]:not(MethodDefinition[value.body=null] + MethodDefinition)',
- ],
- },
- ],
- 'jsdoc/require-param-type': 'off', // don't need type information as we use typescript
- 'jsdoc/require-property-type': 'off', // don't need type information as we use typescript
- 'jsdoc/require-returns': [
- // tell us what the function is expected to return
- 'error',
- {
- checkGetters: false,
- },
- ],
- 'jsdoc/require-returns-type': 'off', // don't need type information as we use typescript
-
- // ECMAScript //
- '@typescript-eslint/prefer-for-of': 'warn', // use `for of` if possible
- '@typescript-eslint/prefer-optional-chain': 'warn', // simplify a logic by using the optional chain operator
- 'object-shorthand': 'warn', // reduce {x:x} to {x}
- 'prefer-const': [
- 'warn',
- {
- destructuring: 'all', // enforce destructuring with const
- },
- ],
- 'prefer-destructuring': [
- 'warn',
- {
- AssignmentExpression: {
- array: false, // allow const foo = array[100];
- },
- },
- ],
- 'prefer-rest-params': 'error', // use a rested variable instead of arguments
- 'prefer-spread': 'error', // instead of using fn.apply(this, args), simply use fn(...args)
- 'symbol-description': 'error', // describe a symbol
-
- // Stylistic Issues //
- '@typescript-eslint/member-ordering': 'error', // enforce member ordering within classes
- '@typescript-eslint/naming-convention': [
- 'error',
- {
- selector: 'default',
- format: ['camelCase'], // default naming convention
- leadingUnderscore: 'allow',
- trailingUnderscore: 'allow',
- },
- {
- selector: 'import',
- format: ['camelCase', 'PascalCase'], // camelCase for functions and PascalCase for classes
- },
- {
- selector: 'objectLiteralMethod',
- format: null, // disable for third-party parameter assignment
- },
- {
- selector: 'objectLiteralProperty',
- format: null, // disable for third-party parameter assignment
- },
- {
- selector: 'variable',
- format: ['camelCase', 'UPPER_CASE'], // camelCase for variables, UPPER_CASE for constants
- leadingUnderscore: 'allow',
- trailingUnderscore: 'allow',
- },
- {
- selector: 'typeLike',
- format: ['PascalCase'], // PascalCase for types
- },
- ],
- 'capitalized-comments': [
- 'warn',
- 'never',
- {
- ignorePattern: '\\*|\\/|/|\\s', // ignore specific characters in comments
- },
- ],
- 'one-var': ['warn', 'never'], // enforce one declaration per line
- 'spaced-comment': 'error', // enforce consistent spacing in comments
-
- // Best Practices //
- '@typescript-eslint/array-type': [
- 'warn',
- {
- default: 'array-simple', // enforce T[] over Array for simple types
- },
- ],
- '@typescript-eslint/consistent-type-imports': [
- 'warn',
- { disallowTypeAnnotations: false }, // enforce type-only imports where possible
- ],
- '@typescript-eslint/explicit-function-return-type': [
- 'warn',
- {
- allowExpressions: true, // allow function return type inference in expressions
- },
- ],
- '@typescript-eslint/explicit-member-accessibility': [
- 'error',
- {
- overrides: {
- constructors: 'no-public', // no public access modifier for constructors
- },
- },
- ],
- '@typescript-eslint/no-empty-object-type': 'off', // type {} is often used to match an empty object that can't be simply replaced by Record
- '@typescript-eslint/no-magic-numbers': [
- 'warn',
- {
- ignore: [-1, 0, 1, DOUBLE_OR_HALVE], // ignore common literals
- ignoreArrayIndexes: true,
- ignoreNumericLiteralTypes: true,
- ignoreReadonlyClassProperties: true,
- ignoreEnums: true,
- ignoreTypeIndexes: true,
- },
- ],
- '@typescript-eslint/require-await': 'off', // allow async functions with no await
- 'import/consistent-type-specifier-style': [
- 'warn',
- 'prefer-top-level', // enforce `import type` specifier style
- ],
- 'import/first': 'warn', // ensure all imports are at the top
- 'import/no-deprecated': 'warn', // avoid deprecated methods
- 'import/no-duplicates': 'warn', // merge multiple imports from the same module
- 'import/no-named-as-default-member': 'off',
- 'import/newline-after-import': 'warn', // add newline after imports
- 'import/order': [
- 'warn',
- {
- 'alphabetize': {
- order: 'asc', // alphabetical order for imports
- caseInsensitive: true,
- },
- 'groups': [
- 'builtin', // e.g. import fs from 'node:fs';
- 'external', // e.g. import foo from 'foo';
- 'internal', // e.g. import { foo } from '#foo';
- 'parent', // e.g. import foo from '../foo';
- [
- 'index', // e.g. import foo from '.';
- 'sibling', // e.g. import foo from './foo';
- ],
- 'object', // e.g. import bar = foo.bar;
- 'unknown', // anything else
- 'type', // e.g. import type { Foo } from 'foo';
- ],
- 'newlines-between': 'always-and-inside-groups', // enable a newline within import groups
- 'pathGroups': [
- {
- group: 'type',
- pattern: 'node:*', // handle Node.js modules
- position: 'before',
- },
- {
- group: 'type',
- pattern:
- '{#*,#*/**,@/**,..,../{,..,../..,../../..,../../../..,../../../../..}/**,.,./**}',
- position: 'after',
- patternOptions: {
- dot: true, // handle dot-based imports
- },
- },
- ],
- 'pathGroupsExcludedImportTypes': [
- 'builtin',
- 'external',
- 'internal',
- 'parent',
- 'index',
- 'sibling',
- 'object',
- 'unknown',
- ],
- },
- ],
- 'eqeqeq': 'error', // enforce type-safe equality operators
- 'max-classes-per-file': 'error', // restrict files to a single class
- 'no-async-promise-executor': 'off', // allow async executors in Promises
- 'no-magic-numbers': 'off', // use @typescript-eslint/no-magic-numbers instead
- 'no-return-await': 'off', // use @typescript-eslint/return-await instead
- 'no-throw-literal': 'warn', // enforce throwing Error instances
- 'no-var': 'error', // prefer const or let
- 'padding-line-between-statements': [
- 'warn',
- {
- blankLine: 'always',
- prev: '*',
- next: 'return', // enforce newline before return statements
- },
- ],
- 'prefer-object-spread': 'warn', // prefer object spread syntax over Object.assign
-
- // Code Quality //
- '@typescript-eslint/no-explicit-any': 'off', // allow usage of any type
- '@typescript-eslint/no-unused-vars': [
- 'warn',
- {
- varsIgnorePattern: '^_',
- argsIgnorePattern: '^_', // use underscore for unused variables
- },
- ],
-
- // Error Prevention //
- '@typescript-eslint/no-non-null-assertion': 'off', // allow non-null assertions
- 'block-scoped-var': 'error', // prevent scoped variable usage outside its scope
- 'no-param-reassign': 'error', // prevent parameter reassignment
- 'no-sparse-arrays': 'warn', // avoid sparse arrays (e.g., [1,,2])
- 'no-template-curly-in-string': 'warn', // warn if template literal syntax is misused
- },
- },
-) as Linter.Config[];
-
-/* eslint-enable max-lines */
diff --git a/packages/preset-essentials/source/eslint.override.ts b/packages/preset-essentials/source/eslint.override.ts
new file mode 100644
index 00000000..7c56d40b
--- /dev/null
+++ b/packages/preset-essentials/source/eslint.override.ts
@@ -0,0 +1,22 @@
+/* v8 ignore start */
+
+import { asset } from 'presetter-types';
+
+import type { Linter } from 'eslint';
+
+export default asset<{ default: Linter.Config[] }>(
+ (current, { variables }) => ({
+ default: [
+ ...(current?.default ?? []),
+ {
+ name: 'presetter-preset-essentials:override:ignore-files',
+ ignores: [
+ `${variables.output!}/**`,
+ `${variables.test!}/**`,
+ `${variables.types!}/**`,
+ `${variables.generated!}/**`,
+ ],
+ },
+ ],
+ }),
+);
diff --git a/packages/preset-essentials/source/eslint.template.ts b/packages/preset-essentials/source/eslint.template.ts
new file mode 100644
index 00000000..383535d9
--- /dev/null
+++ b/packages/preset-essentials/source/eslint.template.ts
@@ -0,0 +1,342 @@
+/* v8 ignore start */
+
+import eslint from '@eslint/js';
+
+import jsdoc from 'eslint-plugin-jsdoc';
+
+import { asset } from 'presetter-types';
+
+import tseslint from 'typescript-eslint';
+
+import comments from '@eslint-community/eslint-plugin-eslint-comments/configs';
+import prettier from 'eslint-config-prettier';
+import imports from 'eslint-plugin-import';
+
+import type { Linter } from 'eslint';
+
+// NOTE: don't specify the files to be linted or ignored here as it will case ts eslint rules to fail
+
+const DOUBLE_OR_HALVE = 2;
+
+export default asset<{ default: Linter.Config[] }>(() => ({
+ default: tseslint.config(
+ eslint.configs.recommended, // eslint recommended rules
+ ...tseslint.configs.recommendedTypeChecked, // typescript-specific rules
+ ...tseslint.configs.stylisticTypeChecked, // typescript-specific rules
+ comments.recommended, // comment formatting
+ jsdoc.configs['flat/recommended'], // documentation
+ prettier, // ignore formatting issue
+ {
+ name: 'presetter-preset-essentials',
+ languageOptions: {
+ parserOptions: {
+ projectService: true,
+ tsconfigRootDir: import.meta.dirname,
+ },
+ },
+ plugins: { jsdoc, import: imports },
+ settings: {
+ 'import/internal-regex': /^#/,
+ 'import/resolver': {
+ typescript: true,
+ node: true,
+ },
+ 'jsdoc': {
+ mode: 'typescript',
+ },
+ },
+ rules: {
+ // Extension Rules //
+
+ // rules for import statements
+ ...imports.flatConfigs.recommended.rules,
+ ...imports.flatConfigs.typescript.rules,
+
+ // Documentation //
+
+ 'jsdoc/tag-lines': ['error', 'never'], // doesn't need a new line after each description
+ 'jsdoc/require-description': [
+ // a description is a must
+ 'warn',
+ {
+ checkConstructors: false,
+ checkGetters: false,
+ checkSetters: false,
+ },
+ ],
+ 'jsdoc/require-jsdoc': [
+ // all functions must be documented
+ 'warn',
+ {
+ require: {
+ ClassDeclaration: true,
+ ClassExpression: true,
+ FunctionDeclaration: false,
+ FunctionExpression: true,
+ MethodDefinition: false,
+ },
+ exemptEmptyConstructors: true,
+ contexts: [
+ // for non-exported functions
+ 'Program > TSDeclareFunction:not(TSDeclareFunction + TSDeclareFunction)',
+ 'Program > FunctionDeclaration:not(TSDeclareFunction + FunctionDeclaration)',
+ // for exported functions
+ 'ExportNamedDeclaration[declaration.type=TSDeclareFunction]:not(ExportNamedDeclaration[declaration.type=TSDeclareFunction] + ExportNamedDeclaration[declaration.type=TSDeclareFunction])',
+ 'ExportNamedDeclaration[declaration.type=FunctionDeclaration]:not(ExportNamedDeclaration[declaration.type=TSDeclareFunction] + ExportNamedDeclaration[declaration.type=FunctionDeclaration])',
+ // for class methods
+ 'MethodDefinition[value.body=null]:not(MethodDefinition[value.body=null] + MethodDefinition[value.body=null])',
+ 'MethodDefinition[value.body]:not(MethodDefinition[value.body=null] + MethodDefinition)',
+ ],
+ },
+ ],
+ 'jsdoc/require-param-type': 'off', // don't need type information as we use typescript
+ 'jsdoc/require-property-type': 'off', // don't need type information as we use typescript
+ 'jsdoc/require-returns': [
+ // tell us what the function is expected to return
+ 'warn',
+ {
+ checkGetters: false,
+ },
+ ],
+ 'jsdoc/require-returns-type': 'off', // don't need type information as we use typescript
+
+ // ECMAScript //
+ '@typescript-eslint/prefer-for-of': 'warn', // use `for of` if possible
+ '@typescript-eslint/prefer-optional-chain': 'warn', // simplify a logic by using the optional chain operator
+ 'object-shorthand': 'warn', // reduce {x:x} to {x}
+ 'prefer-const': [
+ 'warn',
+ {
+ destructuring: 'all', // enforce destructuring with const
+ },
+ ],
+ 'prefer-destructuring': [
+ 'warn',
+ {
+ AssignmentExpression: {
+ array: false, // allow const foo = array[100];
+ },
+ },
+ ],
+ 'prefer-rest-params': 'error', // use a rested variable instead of arguments
+ 'prefer-spread': 'error', // instead of using fn.apply(this, args), simply use fn(...args)
+ 'symbol-description': 'error', // describe a symbol
+
+ // Stylistic Issues //
+ '@typescript-eslint/member-ordering': [
+ // enforce member ordering within classes
+ 'error',
+ {
+ default: {
+ memberTypes: [
+ // fields
+ 'public-static-field',
+ 'public-field',
+ 'protected-static-field',
+ 'protected-field',
+ 'private-static-field',
+ 'private-field',
+ // constructors
+ 'public-constructor',
+ 'protected-constructor',
+ 'private-constructor',
+ // getters & setters
+ ['public-get', 'public-set'],
+ ['protected-get', 'protected-set'],
+ ['private-get', 'private-set'],
+ // index signatures
+ 'signature',
+ // methods
+ 'public-static-method',
+ 'public-method',
+ 'protected-static-method',
+ 'protected-method',
+ 'private-static-method',
+ 'private-method',
+ ],
+ },
+ },
+ ],
+ '@typescript-eslint/naming-convention': [
+ 'error',
+ {
+ selector: 'default',
+ format: ['camelCase'], // default naming convention
+ leadingUnderscore: 'allow',
+ trailingUnderscore: 'allow',
+ },
+ {
+ selector: 'import',
+ format: ['camelCase', 'PascalCase'], // camelCase for functions and PascalCase for classes
+ },
+ {
+ selector: 'objectLiteralMethod',
+ format: null, // disable for third-party parameter assignment
+ },
+ {
+ selector: 'objectLiteralProperty',
+ format: null, // disable for third-party parameter assignment
+ },
+ {
+ selector: 'variable',
+ format: ['camelCase', 'UPPER_CASE'], // camelCase for variables, UPPER_CASE for constants
+ leadingUnderscore: 'allow',
+ trailingUnderscore: 'allow',
+ },
+ {
+ selector: 'typeLike',
+ format: ['PascalCase'], // PascalCase for types
+ },
+ ],
+ 'capitalized-comments': [
+ 'warn',
+ 'never',
+ {
+ ignorePattern: '\\*|\\/|/|\\s', // ignore specific characters in comments
+ },
+ ],
+ 'one-var': ['warn', 'never'], // enforce one declaration per line
+ 'spaced-comment': 'error', // enforce consistent spacing in comments
+
+ // Best Practices //
+ '@typescript-eslint/array-type': [
+ 'warn',
+ {
+ default: 'array-simple', // enforce T[] over Array for simple types
+ },
+ ],
+ '@typescript-eslint/consistent-type-imports': [
+ 'warn',
+ { disallowTypeAnnotations: false }, // enforce type-only imports where possible
+ ],
+ '@typescript-eslint/explicit-function-return-type': [
+ 'warn',
+ {
+ allowExpressions: true, // allow function return type inference in expressions
+ },
+ ],
+ '@typescript-eslint/explicit-member-accessibility': [
+ 'error',
+ {
+ overrides: {
+ constructors: 'no-public', // no public access modifier for constructors
+ },
+ },
+ ],
+ '@typescript-eslint/no-empty-object-type': 'off', // type {} is often used to match an empty object that can't be simply replaced by Record
+ '@typescript-eslint/no-magic-numbers': [
+ 'warn',
+ {
+ ignore: [-1, 0, 1, DOUBLE_OR_HALVE], // ignore common literals
+ ignoreArrayIndexes: true,
+ ignoreNumericLiteralTypes: true,
+ ignoreReadonlyClassProperties: true,
+ ignoreEnums: true,
+ ignoreTypeIndexes: true,
+ },
+ ],
+ '@typescript-eslint/require-await': 'off', // allow async functions with no await
+ 'import/consistent-type-specifier-style': [
+ 'warn',
+ 'prefer-top-level', // enforce `import type` specifier style
+ ],
+ 'import/first': 'warn', // ensure all imports are at the top
+ 'import/no-deprecated': 'warn', // avoid deprecated methods
+ 'import/no-duplicates': 'warn', // merge multiple imports from the same module
+ 'import/no-named-as-default-member': 'off',
+ 'import/newline-after-import': 'warn', // add newline after imports
+ 'import/order': [
+ 'warn',
+ {
+ 'alphabetize': {
+ order: 'asc', // alphabetical order for imports
+ caseInsensitive: true,
+ },
+ 'groups': [
+ 'builtin', // e.g. import fs from 'node:fs';
+ 'external', // e.g. import foo from 'foo';
+ 'internal', // e.g. import { foo } from '#foo';
+ 'parent', // e.g. import foo from '../foo';
+ [
+ 'index', // e.g. import foo from '.';
+ 'sibling', // e.g. import foo from './foo';
+ ],
+ 'object', // e.g. import bar = foo.bar;
+ 'unknown', // anything else
+ 'type', // e.g. import type { Foo } from 'foo';
+ ],
+ 'newlines-between': 'always-and-inside-groups', // enable a newline within import groups
+ 'pathGroups': [
+ {
+ group: 'type',
+ pattern: 'node:*', // handle Node.js modules
+ position: 'before',
+ },
+ {
+ group: 'type',
+ pattern:
+ '{#*,#*/**,@/**,..,../{,..,../..,../../..,../../../..,../../../../..}/**,.,./**}',
+ position: 'after',
+ patternOptions: {
+ dot: true, // handle dot-based imports
+ },
+ },
+ ],
+ 'pathGroupsExcludedImportTypes': [
+ 'builtin',
+ 'external',
+ 'internal',
+ 'parent',
+ 'index',
+ 'sibling',
+ 'object',
+ 'unknown',
+ ],
+ },
+ ],
+ 'eqeqeq': 'error', // enforce type-safe equality operators
+ 'max-classes-per-file': 'error', // restrict files to a single class
+ 'no-async-promise-executor': 'off', // allow async executors in Promises
+ 'no-magic-numbers': 'off', // use @typescript-eslint/no-magic-numbers instead
+ 'no-return-await': 'off', // use @typescript-eslint/return-await instead
+ 'no-throw-literal': 'off', // use @typescript-eslint/no-throw-literal instead
+ 'no-var': 'error', // prefer const or let
+ 'padding-line-between-statements': [
+ 'warn',
+ {
+ blankLine: 'always',
+ prev: '*',
+ next: 'return', // enforce newline before return statements
+ },
+ ],
+ 'prefer-object-spread': 'warn', // prefer object spread syntax over Object.assign
+
+ // Code Quality //
+ '@typescript-eslint/no-explicit-any': 'off', // allow usage of any type
+ '@typescript-eslint/no-unused-vars': [
+ 'warn',
+ {
+ varsIgnorePattern: '^_',
+ argsIgnorePattern: '^_', // use underscore for unused variables
+ },
+ ],
+ '@typescript-eslint/no-unsafe-return': 'off', // skip the hassle of casting return to its expected types all the time
+
+ // Error Prevention //
+ '@typescript-eslint/no-non-null-assertion': 'off', // allow non-null assertions
+ '@typescript-eslint/unbound-method': [
+ 'error', // invoking an unbound method is error-prone
+ {
+ ignoreStatic: true, // but static methods are fine
+ },
+ ],
+ 'import/export': 'off', // ignore as typescript already handles export duplication detection
+ 'import/no-unresolved': 'off', // off as it's handled by typescript
+ 'block-scoped-var': 'error', // prevent scoped variable usage outside its scope
+ 'no-param-reassign': 'error', // prevent parameter reassignment
+ 'no-sparse-arrays': 'warn', // avoid sparse arrays (e.g., [1,,2])
+ 'no-template-curly-in-string': 'warn', // warn if template literal syntax is misused
+ },
+ },
+ ) as Linter.Config[],
+}));
diff --git a/packages/preset-essentials/source/index.ts b/packages/preset-essentials/source/index.ts
index e5a3dc2f..dd79c775 100644
--- a/packages/preset-essentials/source/index.ts
+++ b/packages/preset-essentials/source/index.ts
@@ -2,31 +2,19 @@ import { existsSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
-import type { PresetAsset } from 'presetter-types';
+import { preset } from 'presetter-types';
+
+import eslintOverride from './eslint.override';
+import eslintTemplate from './eslint.template';
+import vitest from './vitest.template';
const DIR = dirname(fileURLToPath(import.meta.url));
// paths to the template directory
const TEMPLATES = resolve(DIR, '..', 'templates');
-/** config for this preset */
-export interface PresetConfig {
- /** configuration to be merged with .eslintrc */
- eslint?: Record;
- /** patterns to be added to .gitignore */
- gitignore?: string[];
- /** configuration to be merged with .lintstagedrc */
- lintstaged?: Record;
- /** patterns to be added to .npmignore */
- npmignore?: string[];
- /** configuration to be merged with .presetterrc */
- prettier?: Record;
- /** configuration to be merged with tsconfig.json */
- tsconfig?: Record;
-}
-
/** list of configurable variables */
-export interface Variable {
+export interface Variables {
/** the directory containing the whole repository (default: .) */
root: string;
/** the directory containing all source code (default: generated) */
@@ -41,39 +29,42 @@ export interface Variable {
test: string;
}
-export const DEFAULT_VARIABLE = {
+export const DEFAULT_VARIABLES = {
root: '.',
generated: 'generated',
source: 'source',
types: 'types',
output: 'lib',
test: 'spec',
-} satisfies Variable;
+} satisfies Variables;
/**
* get the list of templates provided by this preset
* @returns list of preset templates
*/
-export default async function (): Promise {
- return {
- template: ({ target }) => {
- const isGitRoot = existsSync(resolve(target.root, '.git'));
+export default preset('presetter-preset-essentials', ({ root }) => {
+ const isGitRoot = existsSync(resolve(root, '.git'));
- return {
- ...(isGitRoot
- ? { '.husky/pre-commit': resolve(TEMPLATES, 'pre-commit') }
- : {}),
- '.gitignore': resolve(TEMPLATES, 'gitignore'),
- '.lintstagedrc.json': resolve(TEMPLATES, 'lintstagedrc.yaml'),
- '.npmignore': resolve(TEMPLATES, 'npmignore'),
- '.prettierrc.json': resolve(TEMPLATES, 'prettierrc.yaml'),
- 'eslint.config.ts': resolve(TEMPLATES, 'eslint.config.ts'),
- 'tsconfig.json': resolve(TEMPLATES, 'tsconfig.yaml'),
- 'tsconfig.build.json': resolve(TEMPLATES, 'tsconfig.build.yaml'),
- 'vitest.config.ts': resolve(TEMPLATES, 'vitest.config.ts'),
- };
- },
+ return {
+ variables: DEFAULT_VARIABLES,
scripts: resolve(TEMPLATES, 'scripts.yaml'),
- variable: DEFAULT_VARIABLE,
+ assets: {
+ ...(isGitRoot
+ ? { '.husky/pre-commit': resolve(TEMPLATES, 'pre-commit') }
+ : {}),
+ '.gitignore': resolve(TEMPLATES, 'gitignore'),
+ '.lintstagedrc.json': resolve(TEMPLATES, 'lintstagedrc.yaml'),
+ '.npmignore': resolve(TEMPLATES, 'npmignore'),
+ '.prettierrc.json': resolve(TEMPLATES, 'prettierrc.yaml'),
+ 'eslint.config.ts': eslintTemplate,
+ 'tsconfig.json': resolve(TEMPLATES, 'tsconfig.yaml'),
+ 'tsconfig.build.json': resolve(TEMPLATES, 'tsconfig.build.yaml'),
+ 'vitest.config.ts': vitest,
+ },
+ override: {
+ assets: {
+ 'eslint.config.ts': eslintOverride,
+ },
+ },
};
-}
+});
diff --git a/packages/preset-essentials/source/vitest.config.ts b/packages/preset-essentials/source/vitest.config.ts
deleted file mode 100644
index dae2e06f..00000000
--- a/packages/preset-essentials/source/vitest.config.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import tsconfigPaths from 'vite-tsconfig-paths';
-import { defineConfig } from 'vitest/config';
-
-export default defineConfig({
- esbuild: { target: 'es2022' }, // required for using `using` statement
- plugins: [tsconfigPaths()],
- test: {
- passWithNoTests: true,
- typecheck: {
- enabled: true,
- },
- watch: false,
- // auto clear mocks and stubs
- clearMocks: true,
- unstubEnvs: true,
- unstubGlobals: true,
- coverage: {
- all: true,
- clean: true,
- ignoreEmptyLines: true,
- include: ['**/source/**'],
- provider: 'v8',
- reporter: ['text', 'html', 'clover', 'json', 'lcov'],
- thresholds: {
- branches: 100,
- functions: 100,
- lines: 100,
- statements: 100,
- },
- },
- },
-});
diff --git a/packages/preset-essentials/source/vitest.template.ts b/packages/preset-essentials/source/vitest.template.ts
new file mode 100644
index 00000000..44d22a7e
--- /dev/null
+++ b/packages/preset-essentials/source/vitest.template.ts
@@ -0,0 +1,40 @@
+/* v8 ignore start */
+
+import { asset } from 'presetter-types';
+import tsconfigPaths from 'vite-tsconfig-paths';
+
+import { mergeConfig } from 'vitest/config';
+
+import type { ViteUserConfig } from 'vitest/config';
+
+const plugins = [tsconfigPaths()];
+
+export default asset<{ default: ViteUserConfig }>((current, { variables }) => {
+ const { plugins: incomingPlugins = [], ...rest } = current?.default ?? {};
+
+ return {
+ default: mergeConfig(rest, {
+ esbuild: { target: 'es2022' }, // required for using `using` statement
+ plugins: [...new Set([...plugins, ...incomingPlugins])], // make sure there are no duplicates
+ test: {
+ passWithNoTests: true,
+ typecheck: {
+ enabled: true,
+ },
+ watch: false,
+ // auto clear mocks and stubs
+ clearMocks: true,
+ unstubEnvs: true,
+ unstubGlobals: true,
+ coverage: {
+ all: true,
+ clean: true,
+ ignoreEmptyLines: true,
+ include: [`${variables.source!}/**`],
+ provider: 'v8',
+ reporter: ['text', 'html', 'clover', 'json', 'lcov'],
+ },
+ },
+ }),
+ };
+});
diff --git a/packages/preset-essentials/spec/index.spec.ts b/packages/preset-essentials/spec/index.spec.ts
index a6559afa..5aee2edc 100644
--- a/packages/preset-essentials/spec/index.spec.ts
+++ b/packages/preset-essentials/spec/index.spec.ts
@@ -1,61 +1,67 @@
import { existsSync, readdirSync } from 'node:fs';
import { resolve } from 'node:path';
-import { loadDynamicMap, resolveContext } from 'presetter';
-import { describe, expect, it, vi } from 'vitest';
+import { listAssetNames, resolveAssets, resolvePreset } from 'presetter';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
-import getPresetAsset from '#';
+import preset, { DEFAULT_VARIABLES as variables } from '#';
-vi.mock('node:fs', () => ({
- existsSync: (path: string) => path === '/.git',
-}));
+import type { PresetContext } from 'presetter-types';
+
+vi.mock('node:fs', async (importActual) => {
+ const fs = await importActual();
+
+ return {
+ ...fs,
+ existsSync: (path: string) =>
+ fs.existsSync(path) || path === resolve('/path/to/project/.git'),
+ };
+});
vi.mock('node:path', { spy: true });
-describe('fn:getPresetAsset', () => {
- it('use all templates', async () => {
- const asset = await getPresetAsset();
- const context = await resolveContext({
- graph: [{ name: 'preset', asset, nodes: [] }],
- context: {
- target: { name: 'preset', root: '/', package: {} },
- custom: { preset: 'preset' },
- },
- });
-
- // load all potential dynamic content
- await loadDynamicMap(asset.supplementaryConfig, context);
- await loadDynamicMap(asset.template, context);
-
- const CONFIGS = resolve(import.meta.dirname, '..', 'configs');
- const configs = existsSync(CONFIGS) ? readdirSync(CONFIGS) : [];
- const TEMPLATES = resolve(import.meta.dirname, '..', 'templates');
+const OVERRIDES = resolve(import.meta.dirname, '..', 'overrides');
+const TEMPLATES = resolve(import.meta.dirname, '..', 'templates');
+
+const context = {
+ root: '/path/to/project',
+ package: {},
+} satisfies PresetContext;
+
+describe('fn:preset', () => {
+ beforeEach(() => vi.clearAllMocks());
+
+ it('should use all templates', async () => {
+ const node = await resolvePreset(preset, context);
+ listAssetNames(node, { ...context, variables });
+
+ const overrides = existsSync(OVERRIDES) ? readdirSync(OVERRIDES) : [];
const templates = existsSync(TEMPLATES) ? readdirSync(TEMPLATES) : [];
- for (const path of configs) {
- expect(vi.mocked(resolve)).toBeCalledWith(CONFIGS, path);
+ for (const path of overrides) {
+ expect(vi.mocked(resolve)).toHaveBeenCalledWith(OVERRIDES, path);
}
for (const path of templates) {
- expect(vi.mocked(resolve)).toBeCalledWith(TEMPLATES, path);
+ expect(vi.mocked(resolve)).toHaveBeenCalledWith(TEMPLATES, path);
}
});
+ it('should be able to resolve all assets', async () => {
+ const node = await resolvePreset(preset, context);
+ const result = resolveAssets(node, context);
+
+ await expect(result).resolves.not.toThrow();
+ });
+
it('should skip .husky/pre-commit if .git is not present', async () => {
- const asset = await getPresetAsset();
- const context = await resolveContext({
- graph: [{ name: 'preset', asset, nodes: [] }],
- context: {
- target: { name: 'preset', root: '/packages/project', package: {} },
- custom: { preset: 'preset' },
- },
- });
-
- // load all potential dynamic content
- await loadDynamicMap(asset.supplementaryConfig, context);
- await loadDynamicMap(asset.template, context);
-
- const TEMPLATES = resolve(import.meta.dirname, '..', 'templates');
-
- expect(vi.mocked(resolve)).not.toBeCalledWith(TEMPLATES, 'pre-commit');
+ const context = { root: '/packages/project', package: {} };
+
+ const node = await resolvePreset(preset, context);
+ listAssetNames(node, { ...context, variables });
+
+ expect(vi.mocked(resolve)).not.toHaveBeenCalledWith(
+ TEMPLATES,
+ 'pre-commit',
+ );
});
});
diff --git a/packages/preset-essentials/templates/eslint.config.ts b/packages/preset-essentials/templates/eslint.config.ts
deleted file mode 100644
index 07dd25cd..00000000
--- a/packages/preset-essentials/templates/eslint.config.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import presetConfig from 'presetter-preset-essentials/eslint.config';
-
-export default [
- ...presetConfig,
- {
- ignores: ['{test}/**', 'types/**', 'generated/**', '{output}/**'],
- },
-];
diff --git a/packages/preset-essentials/templates/eslintrc.yaml b/packages/preset-essentials/templates/eslintrc.yaml
deleted file mode 100644
index c8ab505a..00000000
--- a/packages/preset-essentials/templates/eslintrc.yaml
+++ /dev/null
@@ -1,218 +0,0 @@
-root: true
-extends:
- - eslint:recommended
- - plugin:@typescript-eslint/recommended-type-checked # typescript specific rules
- - plugin:eslint-comments/recommended # ESLint rules for ESLint directive comments
- - plugin:jsdoc/recommended # documentation
- - plugin:import/typescript # support linting import/export statements in typescript
- - prettier # ignore formatting issue
-env:
- es6: true
- node: true
-parser: '@typescript-eslint/parser'
-parserOptions:
- project: true
- sourceType: module
-plugins:
- - '@typescript-eslint' # typescript support
- - import # enforce best practices on import/export statements
- - jsdoc # enforce documentation
-settings:
- import/internal-regex: '^#'
- jsdoc:
- mode: typescript
-ignorePatterns:
- - '{output}'
- - '{test}'
- - types
- - '**/*.js'
-rules:
- # Documentation
- jsdoc/tag-lines: # doesn't need a new line after each description
- - error
- - never
- jsdoc/require-description: # a description is a must
- - warn
- - checkConstructors: false
- checkGetters: false
- checkSetters: false
- jsdoc/require-jsdoc: # all functions must be documented
- - warn
- - require:
- ClassDeclaration: true
- ClassExpression: true
- FunctionDeclaration: false
- FunctionExpression: true
- MethodDefinition: false
- exemptEmptyConstructors: true
- contexts:
- # for non-exported functions
- - Program > TSDeclareFunction:not(TSDeclareFunction + TSDeclareFunction)
- - Program > FunctionDeclaration:not(TSDeclareFunction + FunctionDeclaration)
- # for exported functions
- - ExportNamedDeclaration[declaration.type=TSDeclareFunction]:not(ExportNamedDeclaration[declaration.type=TSDeclareFunction]
- + ExportNamedDeclaration[declaration.type=TSDeclareFunction])
- - ExportNamedDeclaration[declaration.type=FunctionDeclaration]:not(ExportNamedDeclaration[declaration.type=TSDeclareFunction]
- + ExportNamedDeclaration[declaration.type=FunctionDeclaration])
- # for class methods
- - MethodDefinition[value.body=null]:not(MethodDefinition[value.body=null] + MethodDefinition[value.body=null])
- - MethodDefinition[value.body]:not(MethodDefinition[value.body=null] + MethodDefinition)
- jsdoc/require-param-type: off # don't need type information as we use typescript
- jsdoc/require-property-type: off # don't need type information as we use typescript
- jsdoc/require-returns: # tell us what the function is expected to return
- - error
- - checkGetters: false
- jsdoc/require-returns-type: off # don't need type information as we use typescript
-
- # ECMAScript
- '@typescript-eslint/prefer-for-of': warn # use for of if possible
- '@typescript-eslint/prefer-optional-chain': warn # simplify a logic by using the optional chain operator
- object-shorthand: warn # reduce {x:x} to {x}
- prefer-const: # if a variable is never reassigned, using the const declaration is better
- - warn
- - destructuring: all
- prefer-destructuring: # get variables via destructuring for better readability
- - warn
- - AssignmentExpression:
- array: false # allow const foo = array[100];
- prefer-rest-params: error # use a rested variable instead of arguments
- prefer-spread: error # instead of using fn.apply(this, args), simply use fn(...args)
- symbol-description: error # describe a symbol
-
- # Stylistic Issues
- '@typescript-eslint/member-ordering': error # member group in a class should be in order
- '@typescript-eslint/naming-convention': # use JS/TS naming convention
- - error
- - selector: default
- format:
- - camelCase # default
- leadingUnderscore: allow # default
- trailingUnderscore: allow # default
- - selector: import
- format:
- - camelCase # default, for functions and variables
- - PascalCase # default, for classes
- - selector: objectLiteralMethod
- format: null # disable as an object literal is likely used for assigning parameters to a third-party library
- - selector: objectLiteralProperty
- format: null # disable as an object literal is likely used for assigning parameters to a third-party library
- - selector: variable
- format:
- - camelCase # default
- - UPPER_CASE # default, for constants
- leadingUnderscore: allow
- trailingUnderscore: allow
- - selector: typeLike
- format:
- - PascalCase # default
-
- capitalized-comments: # use lower case in comments
- - warn
- - never
- - ignorePattern: '\*|\/|/|\s'
- one-var: # require variable to be declared only one per line
- - warn
- - never
- spaced-comment: error # enforce consistent spacing after the `//` or `/*` in a comment
-
- # Best Practices
- '@typescript-eslint/array-type': # requires using either T[] or Array depending on the type complexity
- - warn
- - default: array-simple
- '@typescript-eslint/consistent-type-imports': # enforce import type whenever possible
- - warn
- '@typescript-eslint/explicit-function-return-type': # tell us what is expected in return
- - warn
- - allowExpressions: true
- '@typescript-eslint/explicit-member-accessibility': # whether it's protected, private or public, tell us
- - error
- - overrides:
- constructors: 'no-public'
- '@typescript-eslint/no-magic-numbers': # only allow obvious cases
- - warn
- - ignore:
- - -1
- - 0
- - 1
- ignoreArrayIndexes: true
- ignoreNumericLiteralTypes: true
- ignoreReadonlyClassProperties: true
- ignoreEnums: true
- ignoreTypeIndexes: true
- '@typescript-eslint/require-await': off # sometimes you need an async function to fulfil the signature inherited from an abstract class
- import/first: warn # ensure that all import statements are on the top of a file
- import/no-deprecated: warn # use no deprecated method
- import/no-duplicates: warn # merge multiple imports into one
- import/newline-after-import: warn # leave a line after all imports
- import/order: # order and group import statements
- - warn
- - alphabetize: # sort imports
- order: asc
- caseInsensitive: true
- groups: # group imports
- - builtin
- - external
- - - index
- - internal
- - - parent
- - sibling
- - object
- - unknown
- - type
- newlines-between: always-and-inside-groups # enable a space line between import type statements for different module types
- pathGroups: # make type import ordered as the same as the other groups
- - group: type
- pattern: node:*
- position: before
- - group: type
- pattern: '{#*,#*/**,../{,..,../..,../../..,../../../..,../../../../..}/**,./**}'
- position: after
- patternOptions:
- dot: true
- pathGroupsExcludedImportTypes: # exclude other groups so that patterns in pathGroups only affect type imports
- - builtin
- - external
- - index
- - internal
- - parent
- - sibling
- - object
- - unknown
- eqeqeq: error # enforce type-safe equality operators
- max-classes-per-file: error # only one class max for a file
- no-async-promise-executor: off # turn off in order to use await in the executor function
- no-magic-numbers: off # use @typescript-eslint/no-magic-numbers instead
- no-return-await: off # use @typescript-eslint/return-await instead
- no-throw-literal: warn # only an error instance should be thrown
- no-var: error # use let or const instead
- padding-line-between-statements: # always have a blank line before return for better readability
- - warn
- - blankLine: always
- prev: '*'
- next: return
- prefer-object-spread: warn # use {...obj} for cloning instead of Object.assign({}, obj)
- sort-imports: # sort members within each import statement
- - warn
- - ignoreDeclarationSort: true # use import/order for sorting import statements instead
- memberSyntaxSortOrder:
- - multiple
- - single
- - all
- - none
-
- # Code Quality
- '@typescript-eslint/no-explicit-any': off # allow any type
- '@typescript-eslint/no-unused-vars': # use a name starting with _ as a placeholder instead
- - warn
- - varsIgnorePattern: ^_
- argsIgnorePattern: ^_
-
- # Error Prevention
- '@typescript-eslint/no-non-null-assertion': off # sometimes typescript fails to detect that a variable is non-null
- '@typescript-eslint/no-use-before-define': # things should be used only after they're declared
- - warn
- - functions: false
- block-scoped-var: error # disallow usage of a scoped variable outside of its scope
- no-param-reassign: error # it may be useful only when you're testing, not something finished
- no-sparse-arrays: warn # very unlikely you want an undefined in something like [va1, , var2]
- no-template-curly-in-string: warn # unlikely you have any use case with the ${} syntax in a string
diff --git a/packages/preset-essentials/templates/scripts.yaml b/packages/preset-essentials/templates/scripts.yaml
index bd928ede..49895e60 100644
--- a/packages/preset-essentials/templates/scripts.yaml
+++ b/packages/preset-essentials/templates/scripts.yaml
@@ -15,7 +15,7 @@ lint:eslint: eslint --flag unstable_ts_config --fix --format stylish --no-error-
lint:fixme: leasot --skip-unsupported --exit-nicely package.json
lint:prettier: prettier --write --no-error-on-unmatched-pattern
prepare: run-s setup build
-prepublishOnly: run-s coverage lint
+prepublishOnly: true
release: standard-version --preset metahub
setup: run-s setup:*
setup:husky: husky || true
diff --git a/packages/preset-essentials/templates/tsconfig.yaml b/packages/preset-essentials/templates/tsconfig.yaml
index dd9c7f9d..64747d5a 100644
--- a/packages/preset-essentials/templates/tsconfig.yaml
+++ b/packages/preset-essentials/templates/tsconfig.yaml
@@ -39,7 +39,6 @@ compilerOptions:
# module resolution
moduleResolution: bundler
resolveJsonModule: true
- rootDir: .
baseUrl: .
paths:
'#':
diff --git a/packages/preset-essentials/templates/vitest.config.ts b/packages/preset-essentials/templates/vitest.config.ts
deleted file mode 100644
index 7a1328a8..00000000
--- a/packages/preset-essentials/templates/vitest.config.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from 'presetter-preset-essentials/vitest.config';
diff --git a/packages/preset-essentials/types/@eslint/js/index.d.ts b/packages/preset-essentials/types/@eslint/js/index.d.ts
deleted file mode 100644
index 5f179bf1..00000000
--- a/packages/preset-essentials/types/@eslint/js/index.d.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import type { Linter } from 'eslint';
-
-export default declare as {
- configs: {
- all: {
- rules: Linter.RulesRecord;
- };
- recommended: {
- rules: Linter.RulesRecord;
- };
- };
- rules: Linter.RulesRecord;
-};
diff --git a/packages/preset-hybrid/README.md b/packages/preset-hybrid/README.md
index c1638953..961e4f82 100644
--- a/packages/preset-hybrid/README.md
+++ b/packages/preset-hybrid/README.md
@@ -16,9 +16,9 @@
-**presetter-preset-hybrid** is an opinionated extension of [**presetter-preset-esm**](https://github.com/alvis/presetter/tree/master/packages/preset-esm) but aims to help you to create a dual CommonJS/ESM package without all the pains. As the same as presetter-preset-esm, it's designed to help you get started with a typescript project in a fraction of time you usually take via [**presetter**](https://github.com/alvis/presetter).
+**presetter-preset-hybrid** is an opinionated extension of [**presetter-preset-essentials**](https://github.com/alvis/presetter/tree/master/packages/preset-essentials) but aims to help you to create a dual CommonJS/ESM package without all the pains. As the same as presetter-preset-essentials, it's designed to help you get started with a typescript project in a fraction of time you usually take via [**presetter**](https://github.com/alvis/presetter).
-With `presetter-preset-hybrid`, it provides everything bundled from presetter-preset-esm, plus the ease of writing a hybrid CommonJS/ESM package.
+With `presetter-preset-hybrid`, it provides everything bundled from presetter-preset-essentials, plus the ease of writing a hybrid CommonJS/ESM package.
## Features
@@ -32,7 +32,7 @@ With `presetter-preset-hybrid`, it provides everything bundled from presetter-p
## Quick Start
-To kick start a hybrid CommonJS/ESM package, what you need is to set the following in your `package.json` and follow the guide below.
+To kickstart a hybrid CommonJS/ESM package, set the following in your `package.json` and follow the guide below.
```json
{
@@ -51,15 +51,35 @@ To kick start a hybrid CommonJS/ESM package, what you need is to set the followi
[**FULL DOCUMENTATION IS AVAILABLE HERE**](https://github.com/alvis/presetter/blob/master/README.md)
-1. Bootstrap your project with presetter-preset-hybrid
+### 1. Bootstrap your project with presetter-preset-hybrid
-```shell
-npx presetter use presetter-preset-hybrid
+On your project root, create a `presetter.config.ts` file with the following content:
+
+```typescript
+// presetter.config.ts
+export { default } from 'presetter-preset-hybrid';
+```
+
+or if customization is needed. For example, you can extend the configuration with more presets:
+
+```typescript
+// presetter.config.ts
+
+import { preset } from 'presetter';
+import hybrid from 'presetter-preset-hybrid';
+import other from 'other-preset';
+
+export default preset('project name', {
+ extends: [hybrid, other],
+ override: {
+ // override the configuration here
+ },
+});
```
-That's. One command and you're set.
+Then, install your project as usual with `npm install` or any package manager you prefer.
-2. Develop and run life cycle scripts provided by the preset
+### 2. Develop and run life cycle scripts provided by the preset
At this point, all development packages specified in the preset are installed,
and now you can try to run some example life cycle scripts (e.g. run prepare).
@@ -68,11 +88,11 @@ and now you can try to run some example life cycle scripts (e.g. run prepare).
## Project Structure
-After installation, your project file structure should resemble the following or with more configuration files if you also installed other presets such as [`presetter-preset-rollup`](https://github.com/alvis/presetter/blob/master/packages/preset-rollup).
+After installation, your project file structure should resemble the following, or include more configuration files if you also installed other presets.
Implement your business logic under `source` and prepare tests under `spec`.
-**TIPS** You can always change the source directory to other (e.g. src) by setting the `source` variable in `.presetterrc.json`. See the [customization](https://github.com/alvis/presetter/blob/master/packages/preset-hybrid#customization) section below for more details.
+**TIPS** You can always change the source directory to other (e.g. src) by setting the `source` variable in `presetter.config.ts`. See the [customization](https://github.com/alvis/presetter/blob/master/packages/preset-essentials#customization) section below for more details.
```
(root)
@@ -81,7 +101,7 @@ Implement your business logic under `source` and prepare tests under `spec`.
├─ .lintstagedrc.json
├─ .npmignore
├─ .prettierrc.json
- ├─ .presetterrc.json
+ ├─ presetter.config.ts
├─ node_modules
├─ source
│ ├─
@@ -100,50 +120,8 @@ Implement your business logic under `source` and prepare tests under `spec`.
## Customization
-By default, this preset exports a handy configuration for rollup for a typescript project.
-But you can further customize (either extending or replacing) the configuration by specifying the change in the config file (`.presetterrc` or `.presetterrc.json`).
-
-These settings are available in the `config` field in the config file. For directories, the setting is specified in the `variable` field.
-
-The structure of `.presetterrc` should follow the interface below:
-
-```ts
-interface PresetterRC {
- /** name of the preset e.g. presetter-preset-hybrid */
- name: string | string[];
- /** additional configuration passed to the preset for generating the configuration files */
- config?: {
- // ┌─ configuration for other tools via other presets (e.g. presetter-preset-rollup)
- // ...
-
- /** configuration to be merged with .eslintrc */
- eslint?: Record;
- /** configuration to be merged with .lintstagedrc */
- lintstaged?: Record;
- /** patterns to be added to .npmignore */
- npmignore?: string[];
- /** configuration to be merged with .presetterrc */
- prettier?: Record;
- /** configuration to be merged with tsconfig.json */
- tsconfig?: Record;
- /** a list of config files not to be created */
- ignores?: string[];
- };
- /** relative path to root directories for different file types */
- variable?: {
- /** the directory containing the whole repository (default: .) */
- root?: string;
- /** the directory containing all source code (default: source) */
- source?: string;
- /** the directory containing all typing files (default: types) */
- types?: string;
- /** the directory containing all output tile (default: source) */
- output?: string;
- /** the directory containing all test files (default: spec) */
- test?: string;
- };
-}
-```
+By default, this preset exports a handy configuration for a typescript project.
+You can further customize (either extending or replacing) the configuration by specifying the changes in the config file `presetter.config.ts`.
## Script Template Summary
diff --git a/packages/preset-hybrid/configs/scripts.yaml b/packages/preset-hybrid/overrides/scripts.yaml
similarity index 100%
rename from packages/preset-hybrid/configs/scripts.yaml
rename to packages/preset-hybrid/overrides/scripts.yaml
diff --git a/packages/preset-hybrid/source/index.ts b/packages/preset-hybrid/source/index.ts
index eba49ce5..e6370cd9 100644
--- a/packages/preset-hybrid/source/index.ts
+++ b/packages/preset-hybrid/source/index.ts
@@ -1,30 +1,29 @@
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
-import type { PresetAsset } from 'presetter-types';
+import essentials from 'presetter-preset-essentials';
+import { preset } from 'presetter-types';
-export type { PresetConfig, Variable } from 'presetter-preset-essentials';
+export { DEFAULT_VARIABLES } from 'presetter-preset-essentials';
+
+export type { Variables } from 'presetter-preset-essentials';
const DIR = fileURLToPath(dirname(import.meta.url));
// paths to the template directory
-const CONFIGS = resolve(DIR, '..', 'configs');
+const OVERRIDES = resolve(DIR, '..', 'overrides');
const TEMPLATES = resolve(DIR, '..', 'templates');
-/**
- * get the list of templates provided by this preset
- * @returns list of preset templates
- */
-export default async function (): Promise {
- return {
- extends: ['presetter-preset-essentials'],
- supplementaryScripts: resolve(CONFIGS, 'scripts.yaml'),
- supplementaryConfig: {
- gitignore: ['tsconfig.cjs.json', 'tsconfig.mjs.json'],
- },
- template: {
- 'tsconfig.cjs.json': resolve(TEMPLATES, 'tsconfig.cjs.yaml'),
- 'tsconfig.mjs.json': resolve(TEMPLATES, 'tsconfig.mjs.yaml'),
+export default preset('presetter-preset-hybrid', {
+ extends: [essentials],
+ assets: {
+ 'tsconfig.cjs.json': resolve(TEMPLATES, 'tsconfig.cjs.yaml'),
+ 'tsconfig.mjs.json': resolve(TEMPLATES, 'tsconfig.mjs.yaml'),
+ },
+ override: {
+ scripts: resolve(OVERRIDES, 'scripts.yaml'),
+ assets: {
+ '.gitignore': ['tsconfig.cjs.json', 'tsconfig.mjs.json'],
},
- };
-}
+ },
+});
diff --git a/packages/preset-hybrid/spec/index.spec.ts b/packages/preset-hybrid/spec/index.spec.ts
index 774f5a03..75194295 100644
--- a/packages/preset-hybrid/spec/index.spec.ts
+++ b/packages/preset-hybrid/spec/index.spec.ts
@@ -1,38 +1,43 @@
import { existsSync, readdirSync } from 'node:fs';
import { resolve } from 'node:path';
-import { loadDynamicMap, resolveContext } from 'presetter';
+import { listAssetNames, resolveAssets, resolvePreset } from 'presetter';
import { describe, expect, it, vi } from 'vitest';
-import getPresetAsset from '#';
+import preset, { DEFAULT_VARIABLES as variables } from '#';
+
+import type { PresetContext } from 'presetter-types';
vi.mock('node:path', { spy: true });
-describe('fn:getPresetAsset', () => {
- it('use all templates', async () => {
- const asset = await getPresetAsset();
- const context = await resolveContext({
- graph: [{ name: 'preset', asset, nodes: [] }],
- context: {
- target: { name: 'preset', root: '/', package: {} },
- custom: { preset: 'preset' },
- },
- });
-
- // load all potential dynamic content
- await loadDynamicMap(asset.supplementaryConfig, context);
- await loadDynamicMap(asset.template, context);
-
- const CONFIGS = resolve(import.meta.dirname, '..', 'configs');
- const configs = existsSync(CONFIGS) ? readdirSync(CONFIGS) : [];
- const TEMPLATES = resolve(import.meta.dirname, '..', 'templates');
+const OVERRIDES = resolve(import.meta.dirname, '..', 'overrides');
+const TEMPLATES = resolve(import.meta.dirname, '..', 'templates');
+
+const context = {
+ root: '/path/to/project',
+ package: {},
+} satisfies PresetContext;
+
+describe('fn:preset', () => {
+ it('should use all templates', async () => {
+ const node = await resolvePreset(preset, context);
+ listAssetNames(node, { ...context, variables });
+
+ const overrides = existsSync(OVERRIDES) ? readdirSync(OVERRIDES) : [];
const templates = existsSync(TEMPLATES) ? readdirSync(TEMPLATES) : [];
- for (const path of configs) {
- expect(vi.mocked(resolve)).toBeCalledWith(CONFIGS, path);
+ for (const path of overrides) {
+ expect(vi.mocked(resolve)).toHaveBeenCalledWith(OVERRIDES, path);
}
for (const path of templates) {
- expect(vi.mocked(resolve)).toBeCalledWith(TEMPLATES, path);
+ expect(vi.mocked(resolve)).toHaveBeenCalledWith(TEMPLATES, path);
}
});
+
+ it('should be able to resolve all assets', async () => {
+ const node = await resolvePreset(preset, context);
+ const result = resolveAssets(node, context);
+
+ await expect(result).resolves.not.toThrow();
+ });
});
diff --git a/packages/preset-react/README.md b/packages/preset-react/README.md
index 92d60a73..90106588 100644
--- a/packages/preset-react/README.md
+++ b/packages/preset-react/README.md
@@ -18,7 +18,7 @@
## Features
-**presetter-preset-react** is an opinionated preset for you to setup a React project in a fraction of time you usually take via [**presetter**](https://github.com/alvis/presetter).
+**presetter-preset-react** is an extension of [**presetter-preset-web**](https://github.com/alvis/presetter) with additional tools to help you to develop a React project with ease via [**presetter**](https://github.com/alvis/presetter).
- ✨ TSX support
- 🧪 @testing-library/react
@@ -28,15 +28,31 @@
[**FULL DOCUMENTATION IS AVAILABLE HERE**](https://github.com/alvis/presetter/blob/master/README.md)
-1. Bootstrap your project with `presetter-preset-esm` & `presetter-preset-react`
+### 1. Bootstrap your project with presetter-preset-react
-```shell
-npx presetter use presetter-preset presetter-preset-react
+On your project root, create a `presetter.config.ts` file with the following content:
+
+```typescript
+// presetter.config.ts
+
+import { preset } from 'presetter';
+import esm from 'presetter-preset-esm';
+import react from 'presetter-preset-react';
+
+export default preset('project name', {
+ // NOTE
+ // you don't need to extends presetter-preset-web presets here since they are already included in the react preset
+ // however, you may need an additional preset like presetter-preset-esm for ESM support and other basic toolings
+ extends: [esm, react],
+ override: {
+ // override the configuration here
+ },
+});
```
-That's. One command and you're set.
+Then, install your project as usual with `npm install` or any package manager you prefer.
-2. Develop and run life cycle scripts provided by the preset
+### 2. Develop and run life cycle scripts provided by the preset
At this point, all development packages specified in the preset are installed,
and now you can try to run some example life cycle scripts (e.g. run prepare).
@@ -45,16 +61,20 @@ and now you can try to run some example life cycle scripts (e.g. run prepare).
## Project Structure
-After installation, your project file structure should resemble the following or with more configuration files if you also installed other presets such as [`presetter-preset-esm`](https://github.com/alvis/presetter/blob/master/packages/preset-esm).
+After installation, your project file structure should resemble the following, or include more configuration files if you also installed other presets.
+
+Implement your business logic under `source` and prepare tests under `spec`.
+The `.d.ts` files are handy type definitions for you to import `.css` or image files in typescript.
-Implement your business logic under `source` and prepare tests under `spec`. The `.d.ts` files are handy type definitions for you to import `.css` or image files in typescript.
+**NOTE** You will notice there's no additional configuration file on your root folder like other presets such as [`presetter-preset-esm`](https://github.com/alvis/presetter/blob/master/packages/preset-esm).
+It's because `presetter-preset-react` extends `presetter-preset-web` which is a bundle only preset, meaning it only helps you to install the development packages specified in this preset only.
-**TIPS** You can always change the source directory to other (e.g. src) by setting the `source` variable in `.presetterrc.json`. See the [customization](https://github.com/alvis/presetter/blob/master/packages/preset-react#customization) section below for more details.
+**TIPS** You can always change the source directory to other (e.g. src) by setting the `source` variable in `presetter.config.ts`. See the [customization](https://github.com/alvis/presetter/blob/master/packages/preset-essentials#customization) section below for more details.
```
(root)
├─ .git
- ├─ .presetterrc.json
+ ├─ presetter.config.ts
├─ node_modules
├─ source
│ ├─
@@ -74,34 +94,16 @@ Implement your business logic under `source` and prepare tests under `spec`. The
## Customization
By default, this preset exports a handy configuration set for a React project written in typescript.
-But you can further customize (either extending or replacing) the configuration by specifying the change in the config file (`.presetterrc` or `.presetterrc.json`).
-
-These settings are available in the `config` field in the config file. For directories, the setting is specified in the `variable` field.
-
-The structure of `.presetterrc` should follow the interface below:
-
-```ts
-interface PresetterRC {
- /** name(s) of the preset e.g. presetter-preset-react */
- name: string | string[];
- /** additional configuration passed to the preset for generating the configuration files */
- config?: {
- // ┌─ configuration for other tools via other presets (e.g. presetter-preset-esm)
- // ...
-
- /** configuration to be merged with .eslintrc */
- eslint?: Record;
- /** configuration to be merged with tsconfig.json */
- tsconfig?: Record;
- /** variables to be substituted in templates */
- variable?: {
- /** the directory containing all source code (default: source) */
- source?: string;
- /** the directory containing all typing files (default: types) */
- types?: string;
- /** the directory containing all output tile (default: source) */
- output?: string;
- };
- };
-}
-```
+
+You can further customize (either extending or replacing) the configuration by specifying the changes in the config file `presetter.config.ts`.
+
+## Script Template Summary
+
+- **`run build`**: Transpile source code from typescript and replace any mapped paths
+- **`run clean`**: Clean up any previously transpiled code
+- **`run develop -- `**: Create a service that run the specified file whenever the source has changed
+- **`run test`**: Run all tests
+- **`run watch`**: Rerun all tests whenever the source has change
+- **`run coverage`**: Run all test with coverage report
+- **`run release`**: Bump the version and automatically generate a change log
+- **`run release -- --prerelease `**: Release with a prerelease tag
diff --git a/packages/preset-react/configs/lintstaged.yaml b/packages/preset-react/overrides/lintstaged.yaml
similarity index 100%
rename from packages/preset-react/configs/lintstaged.yaml
rename to packages/preset-react/overrides/lintstaged.yaml
diff --git a/packages/preset-react/package.json b/packages/preset-react/package.json
index 70dbac8f..4c299388 100644
--- a/packages/preset-react/package.json
+++ b/packages/preset-react/package.json
@@ -19,10 +19,6 @@
".": {
"default": "./lib/index.js",
"types": "./lib/index.d.ts"
- },
- "./eslint.config": {
- "default": "./lib/eslint.config.js",
- "types": "./lib/eslint.config.d.ts"
}
},
"repository": {
diff --git a/packages/preset-react/source/eslint.config.ts b/packages/preset-react/source/eslint.config.ts
deleted file mode 100644
index 38d2630f..00000000
--- a/packages/preset-react/source/eslint.config.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-import sonarjs from 'eslint-plugin-sonarjs';
-import presetESMConfig from 'presetter-preset-essentials/eslint.config';
-
-import react from 'eslint-plugin-react';
-import testing from 'eslint-plugin-testing-library';
-
-import type { Linter } from 'eslint';
-
-export default [
- ...presetESMConfig,
- react.configs.flat.recommended,
- react.configs.flat['jsx-runtime'],
- sonarjs.configs.recommended,
- {
- name: 'presetter-preset-react',
- plugins: { react },
- languageOptions: {
- parserOptions: {
- ecmaFeatures: {
- jsx: true,
- },
- },
- },
- rules: {
- 'react/boolean-prop-naming': 'warn', // enforce consistent naming for boolean props
- 'react/button-has-type': 'warn', // enforce button elements to contain a type attribute
- 'react/destructuring-assignment': 'warn', // enforce usage of destructuring assignment in component
- 'react/prop-types': 'off', // we use TypeScript for prop types
- 'react/sort-comp': 'warn', // enforce component methods order
- 'react/jsx-sort-props': [
- 'warn', // enforce props order
- {
- callbacksLast: true,
- shorthandFirst: true,
- },
- ],
- },
- overrides: [
- {
- files: ['**/__tests__/**/*.[jt]sx', '**/?(*.)+(spec|test).[jt]sx'],
- ...testing.configs['flat/react'],
- },
- {
- 'files': ['**/*.[jt]sx'],
- 'rules': {
- 'max-lines-per-function': [
- 'warn',
- {
- max: 120, // extend the default to 120 lines for functional components
- },
- ],
- '@typescript-eslint/naming-convention': [
- 'error', // add PascalCase to the list for functional components
- {
- selector: 'default',
- format: [
- 'camelCase', // default
- ],
- leadingUnderscore: 'allow', // default
- trailingUnderscore: 'allow', // default
- },
- {
- selector: 'import',
- format: [
- 'camelCase', // default, for functions and variables
- 'PascalCase', // default, for classes
- ],
- },
- {
- selector: 'function',
- format: [
- 'camelCase', // default
- 'PascalCase', // for react components
- ],
- },
- {
- selector: 'objectLiteralMethod',
- format: null, // disable as an object literal is likely used for assigning parameters to a third-party library
- },
- {
- selector: 'objectLiteralProperty',
- format: null, // disable as an object literal is likely used for assigning parameters to a third-party library
- },
- {
- selector: 'parameter',
- format: [
- 'camelCase', // default
- 'PascalCase', // for react components
- ],
- leadingUnderscore: 'allow', // default
- trailingUnderscore: 'allow', // default
- },
- {
- selector: 'variable',
- format: [
- 'PascalCase', // for react functional components
- 'camelCase', // default, for variables
- 'UPPER_CASE', // default, for constants
- ],
- leadingUnderscore: 'allow', // add _prefix to ignore the rule
- trailingUnderscore: 'allow', // add _suffix to ignore the rule
- },
- {
- selector: 'typeLike',
- format: [
- 'PascalCase', // default
- ],
- },
- ],
- },
- 'jsdoc/require-returns': [
- 'error', // tell us what the function is expected to return unless it's a JSX element
- {
- checkGetters: false,
- contexts: [
- "FunctionDeclaration:has(BlockStatement > ReturnStatement:not([argument.type='JSXElement']))",
- "ArrowFunctionExpression:has(BlockStatement > ReturnStatement:not([argument.type='JSXElement']))",
- ],
- },
- ],
- },
- ],
- },
-] as Linter.Config[];
diff --git a/packages/preset-react/source/eslint.override.ts b/packages/preset-react/source/eslint.override.ts
new file mode 100644
index 00000000..edf2599e
--- /dev/null
+++ b/packages/preset-react/source/eslint.override.ts
@@ -0,0 +1,112 @@
+/* v8 ignore start */
+
+import { asset } from 'presetter';
+
+import testing from 'eslint-plugin-testing-library';
+
+import type { Linter } from 'eslint';
+
+export default asset<{ default: Linter.Config[] }>((current) => {
+ const configs = current?.default ?? [];
+
+ const hasTypescriptEslint = configs.some(
+ (config) => !!config.plugins?.['@typescript-eslint'],
+ );
+
+ const hasJsDoc = configs.some((config) => !!config.plugins?.jsdoc);
+
+ return {
+ default: [
+ ...configs,
+ {
+ name: 'presetter-preset-react:override:test-files',
+ files: ['**/__tests__/**/*.[jt]sx', '**/?(*.)+(spec|test).[jt]sx'],
+ ...testing.configs['flat/react'],
+ },
+ {
+ name: 'presetter-preset-react:override:react-files',
+ files: ['**/*.[jt]sx'],
+ rules: {
+ 'max-lines-per-function': [
+ 'warn',
+ {
+ max: 120, // extend the default to 120 lines for functional components
+ },
+ ],
+ ...(hasTypescriptEslint && {
+ '@typescript-eslint/naming-convention': [
+ 'error', // add PascalCase to the list for functional components
+ {
+ selector: 'default',
+ format: [
+ 'camelCase', // default
+ ],
+ leadingUnderscore: 'allow', // default
+ trailingUnderscore: 'allow', // default
+ },
+ {
+ selector: 'import',
+ format: [
+ 'camelCase', // default, for functions and variables
+ 'PascalCase', // default, for classes
+ ],
+ },
+ {
+ selector: 'function',
+ format: [
+ 'camelCase', // default
+ 'PascalCase', // for react components
+ ],
+ },
+ {
+ selector: 'objectLiteralMethod',
+ format: null, // disable as an object literal is likely used for assigning parameters to a third-party library
+ },
+ {
+ selector: 'objectLiteralProperty',
+ format: null, // disable as an object literal is likely used for assigning parameters to a third-party library
+ },
+ {
+ selector: 'parameter',
+ format: [
+ 'camelCase', // default
+ 'PascalCase', // for react components
+ ],
+ leadingUnderscore: 'allow', // default
+ trailingUnderscore: 'allow', // default
+ },
+ {
+ selector: 'variable',
+ format: [
+ 'PascalCase', // for react functional components
+ 'camelCase', // default, for variables
+ 'UPPER_CASE', // default, for constants
+ ],
+ leadingUnderscore: 'allow', // add _prefix to ignore the rule
+ trailingUnderscore: 'allow', // add _suffix to ignore the rule
+ },
+ {
+ selector: 'typeLike',
+ format: [
+ 'PascalCase', // default
+ ],
+ },
+ ],
+ }),
+ ...(hasJsDoc && {
+ 'jsdoc/require-returns': [
+ 'error', // tell us what the function is expected to return unless it's a JSX element
+ {
+ checkGetters: false,
+ contexts: [
+ "FunctionDeclaration:has(BlockStatement > ReturnStatement:not([argument.type='JSXElement']))",
+ "ArrowFunctionExpression:has(BlockStatement > ReturnStatement:not([argument.type='JSXElement']))",
+ ],
+ },
+ ],
+ }),
+ },
+ },
+ ] as Linter.Config[],
+ };
+});
diff --git a/packages/preset-react/source/eslint.template.ts b/packages/preset-react/source/eslint.template.ts
new file mode 100644
index 00000000..163eecad
--- /dev/null
+++ b/packages/preset-react/source/eslint.template.ts
@@ -0,0 +1,35 @@
+/* v8 ignore start */
+
+import react from 'eslint-plugin-react';
+
+import type { Linter } from 'eslint';
+
+export default [
+ react.configs.flat.recommended,
+ react.configs.flat['jsx-runtime'],
+ {
+ name: 'presetter-preset-react',
+ plugins: { react },
+ languageOptions: {
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ },
+ rules: {
+ 'react/boolean-prop-naming': 'warn', // enforce consistent naming for boolean props
+ 'react/button-has-type': 'warn', // enforce button elements to contain a type attribute
+ 'react/destructuring-assignment': 'warn', // enforce usage of destructuring assignment in component
+ 'react/prop-types': 'off', // we use TypeScript for prop types
+ 'react/sort-comp': 'warn', // enforce component methods order
+ 'react/jsx-sort-props': [
+ 'warn', // enforce props order
+ {
+ callbacksLast: true,
+ shorthandFirst: true,
+ },
+ ],
+ },
+ },
+] as Linter.Config[];
diff --git a/packages/preset-react/source/index.ts b/packages/preset-react/source/index.ts
index d40536e1..8517ff0b 100644
--- a/packages/preset-react/source/index.ts
+++ b/packages/preset-react/source/index.ts
@@ -1,13 +1,17 @@
-import { dirname, join, resolve } from 'node:path';
+import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
-import type { PresetAsset } from 'presetter-types';
+import web from 'presetter-preset-web';
+import { preset } from 'presetter-types';
+
+import eslintOverride from './eslint.override';
+import * as eslint from './eslint.template';
const DIR = fileURLToPath(dirname(import.meta.url));
// paths to the template directory
const TEMPLATES = resolve(DIR, '..', 'templates');
-const CONFIGS = resolve(DIR, '..', 'configs');
+const OVERRIDES = resolve(DIR, '..', 'overrides');
/** config for this preset */
export interface PresetConfig {
@@ -18,7 +22,7 @@ export interface PresetConfig {
}
/** list of configurable variables */
-export interface Variable {
+export interface Variables {
/** the directory containing all source code (default: source) */
source: string;
/** the directory containing all extra typing files (default: types) */
@@ -27,54 +31,41 @@ export interface Variable {
output: string;
}
-export const DEFAULT_VARIABLE = {
+export const DEFAULT_VARIABLES = {
source: 'source',
types: 'types',
output: 'lib',
-} satisfies Variable;
+} satisfies Variables;
const IMAGE_TYPE = 'image.d.ts';
const STYLE_TYPE = 'style.d.ts';
-const template: PresetAsset['template'] = ({
- custom: {
- variable: { types },
- },
-}) => ({
- 'eslint.config.ts': resolve(TEMPLATES, 'eslint.config.ts'),
- 'tsconfig.json': resolve(TEMPLATES, 'tsconfig.yaml'),
- 'tsconfig.build.json': resolve(TEMPLATES, 'tsconfig.build.yaml'),
- [join(types, IMAGE_TYPE)]: resolve(TEMPLATES, IMAGE_TYPE),
- [join(types, STYLE_TYPE)]: resolve(TEMPLATES, STYLE_TYPE),
-});
-
-const noSymlinks: PresetAsset['noSymlinks'] = ({
- custom: {
- variable: { types },
- },
-}) => [join(types, IMAGE_TYPE), join(types, STYLE_TYPE)];
-
-const supplementaryConfig: PresetAsset['supplementaryConfig'] = () => ({
- eslint: resolve(CONFIGS, 'eslint.yaml'),
- gitignore: ({
- custom: {
- variable: { types },
+export default preset('presetter-preset-react', {
+ extends: [web],
+ variables: DEFAULT_VARIABLES,
+ scripts: resolve(TEMPLATES, 'scripts.yaml'),
+ assets: ({ variables }) => ({
+ '.gitignore': (current, { variables }) => [
+ ...(current ?? []),
+ `/${variables.types!}/${IMAGE_TYPE}`,
+ `/${variables.types!}/${STYLE_TYPE}`,
+ ],
+ 'eslint.config.ts': eslint,
+ 'tsconfig.json': resolve(TEMPLATES, 'tsconfig.yaml'),
+ 'tsconfig.build.json': resolve(TEMPLATES, 'tsconfig.build.yaml'),
+ [`${variables.types!}/${IMAGE_TYPE}` as 'image.d.ts']: resolve(
+ TEMPLATES,
+ IMAGE_TYPE,
+ ),
+ [`${variables.types!}/${STYLE_TYPE}` as 'style.d.ts']: resolve(
+ TEMPLATES,
+ STYLE_TYPE,
+ ),
+ }),
+ override: {
+ assets: {
+ '.lintstagedrc.json': resolve(OVERRIDES, 'lintstaged.yaml'),
+ 'eslint.config.ts': eslintOverride,
},
- }) => [join('/', types, IMAGE_TYPE), join('/', types, STYLE_TYPE)],
- lintstaged: resolve(CONFIGS, 'lintstaged.yaml'),
+ },
});
-
-/**
- * get the list of templates provided by this preset
- * @returns list of preset templates
- */
-export default async function (): Promise {
- return {
- extends: ['presetter-preset-web'],
- scripts: resolve(TEMPLATES, 'scripts.yaml'),
- template,
- noSymlinks,
- supplementaryConfig,
- variable: DEFAULT_VARIABLE,
- };
-}
diff --git a/packages/preset-react/spec/index.spec.ts b/packages/preset-react/spec/index.spec.ts
index 774f5a03..4c391d66 100644
--- a/packages/preset-react/spec/index.spec.ts
+++ b/packages/preset-react/spec/index.spec.ts
@@ -1,38 +1,51 @@
import { existsSync, readdirSync } from 'node:fs';
import { resolve } from 'node:path';
-import { loadDynamicMap, resolveContext } from 'presetter';
+import { listAssetNames, resolveAssets, resolvePreset } from 'presetter';
import { describe, expect, it, vi } from 'vitest';
-import getPresetAsset from '#';
+import preset, { DEFAULT_VARIABLES as variables } from '#';
+
+import type { PresetContext } from 'presetter-types';
vi.mock('node:path', { spy: true });
-describe('fn:getPresetAsset', () => {
- it('use all templates', async () => {
- const asset = await getPresetAsset();
- const context = await resolveContext({
- graph: [{ name: 'preset', asset, nodes: [] }],
- context: {
- target: { name: 'preset', root: '/', package: {} },
- custom: { preset: 'preset' },
- },
- });
-
- // load all potential dynamic content
- await loadDynamicMap(asset.supplementaryConfig, context);
- await loadDynamicMap(asset.template, context);
-
- const CONFIGS = resolve(import.meta.dirname, '..', 'configs');
- const configs = existsSync(CONFIGS) ? readdirSync(CONFIGS) : [];
- const TEMPLATES = resolve(import.meta.dirname, '..', 'templates');
+const OVERRIDES = resolve(import.meta.dirname, '..', 'overrides');
+const TEMPLATES = resolve(import.meta.dirname, '..', 'templates');
+
+const context = {
+ root: '/path/to/project',
+ package: {},
+} satisfies PresetContext;
+
+describe('fn:preset', () => {
+ it('should use all templates', async () => {
+ const node = await resolvePreset(preset, context);
+ listAssetNames(node, { ...context, variables });
+
+ const overrides = existsSync(OVERRIDES) ? readdirSync(OVERRIDES) : [];
const templates = existsSync(TEMPLATES) ? readdirSync(TEMPLATES) : [];
- for (const path of configs) {
- expect(vi.mocked(resolve)).toBeCalledWith(CONFIGS, path);
+ for (const path of overrides) {
+ expect(vi.mocked(resolve)).toHaveBeenCalledWith(OVERRIDES, path);
}
for (const path of templates) {
- expect(vi.mocked(resolve)).toBeCalledWith(TEMPLATES, path);
+ expect(vi.mocked(resolve)).toHaveBeenCalledWith(TEMPLATES, path);
}
});
+
+ it('should be able to resolve all assets', async () => {
+ const node = await resolvePreset(preset, context);
+ const result = resolveAssets(node, context);
+
+ await expect(result).resolves.not.toThrow();
+ });
+
+ it('should add add additional files to existing ignore list', async () => {
+ const node = await resolvePreset(preset, context);
+ const result = await resolveAssets(node, context);
+ const expected = ['/types/image.d.ts', '/types/style.d.ts'];
+
+ expect(result['.gitignore']).toEqual(expect.arrayContaining(expected));
+ });
});
diff --git a/packages/preset-react/templates/eslint.config.ts b/packages/preset-react/templates/eslint.config.ts
deleted file mode 100644
index fdc218d9..00000000
--- a/packages/preset-react/templates/eslint.config.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import presetConfig from 'presetter-preset-react/eslint.config';
-
-export default [
- ...presetConfig,
- {
- ignores: ['{test}/**', 'types/**', 'generated/**', '{output}/**'],
- },
-];
diff --git a/packages/preset-rollup/README.md b/packages/preset-rollup/README.md
index b100ee7e..cbede745 100644
--- a/packages/preset-rollup/README.md
+++ b/packages/preset-rollup/README.md
@@ -20,7 +20,7 @@
**presetter-preset-rollup** is an opinionated preset for you to setup rollup in a fraction of time you usually take via [**presetter**](https://github.com/alvis/presetter).
-- 🗞️ Rollup 2
+- 🗞️ Rollup 4
- 2️⃣ Dual CJS and ESM modules export by default
- 🍄 Common rollup packages included as one single bundle
- `@rollup/plugin-commonjs`
@@ -36,27 +36,7 @@
## Quick Start
-[**FULL DOCUMENTATION IS AVAILABLE HERE**](https://github.com/alvis/presetter/blob/master/README.md)
-
-1. Bootstrap your project with `presetter-preset-esm` & `presetter-preset-rollup`
-
-```shell
-npx presetter use presetter-preset presetter-preset-rollup
-```
-
-That's. One command and you're set.
-
-After bootstrapping, you would see a lot of configuration files generated, including a `rollup.config.ts` that has all plugins configured properly for you.
-
-2. Develop and run life cycle scripts provided by the preset
-
-At this point, all development packages specified in the preset are installed,
-and now you can try to run some example life cycle scripts (e.g. run prepare).
-
-![Demo](https://raw.githubusercontent.com/alvis/presetter/master/assets/demo.gif)
-
-**IMPORTANT**
-For NodeJS to import the correct export, remember to specify the following in your project's package.json too!
+To kickstart a library, set the following in your `package.json` and follow the guide below.
```json
{
@@ -66,22 +46,62 @@ For NodeJS to import the correct export, remember to specify the following in yo
"exports": {
"require": "./lib/index.js",
"import": "./lib/index.mjs"
+ },
+ "scripts": {
+ "prepare": "run prepare",
+ "build": "run build",
+ "clean": "run clean",
+ "test": "run test",
+ "watch": "run watch",
+ "coverage": "run coverage"
}
}
```
+[**FULL DOCUMENTATION IS AVAILABLE HERE**](https://github.com/alvis/presetter/blob/master/README.md)
+
+### 1. Bootstrap your project with presetter-preset-web
+
+On your project root, create a `presetter.config.ts` file with the following content:
+
+```typescript
+// presetter.config.ts
+
+import { preset } from 'presetter';
+import essentials from 'presetter-preset-essentials';
+import rollup from 'presetter-preset-rollup';
+
+export default preset('project name', {
+ // NOTE
+ // you may need an additional preset like presetter-preset-essentials for typescript support and other basic toolings
+ extends: [essentials, rollup],
+ override: {
+ // override the configuration here
+ },
+});
+```
+
+Then, install your project as usual with `npm install` or any package manager you prefer.
+
+### 2. Develop and run life cycle scripts provided by the preset
+
+At this point, all development packages specified in the preset are installed,
+and now you can try to run some example life cycle scripts (e.g. run prepare).
+
+![Demo](https://raw.githubusercontent.com/alvis/presetter/master/assets/demo.gif)
+
## Project Structure
After installation, your project file structure should resemble the following or with more configuration files if you also installed other presets such as [`presetter-preset-esm`](https://github.com/alvis/presetter/blob/master/packages/preset-esm).
Implement your business logic under `source` and prepare tests under `spec`.
-**TIPS** You can always change the source directory to other (e.g. src) by setting the `source` variable in `.presetterrc.json`. See the [customization](https://github.com/alvis/presetter/blob/master/packages/preset-rollup#customization) section below for more details.
+**TIPS** You can always change the source directory to other (e.g. src) by setting the `source` variable in `presetter.config.ts`. See the [customization](https://github.com/alvis/presetter/blob/master/packages/preset-rollup#customization) section below for more details.
```
(root)
├─ .git
- ├─ .presetterrc.json
+ ├─ presetter.config.ts
├─ node_modules
├─ source
│ ├─
@@ -95,98 +115,16 @@ Implement your business logic under `source` and prepare tests under `spec`.
## Customization
-By default, this preset exports a handy configuration for rollup for a typescript project.
-But you can further customize (either extending or replacing) the configuration by specifying the change in the config file (`.presetterrc` or `.presetterrc.json`).
-
-These settings are available in the `config` field in the config file. For directories, the setting is specified in the `variable` field.
-
-The structure of `.presetterrc` should follow the interface below:
-
-```ts
-interface PresetterRC {
- /** name(s) of the preset e.g. presetter-preset-rollup */
- name: string | string[];
- /** additional configuration passed to the preset for generating the configuration files */
- config?: {
- // ┌─ configuration for other tools via other presets (e.g. presetter-preset-esm)
- // ...
-
- /** additional configuration for rollup */
- rollup?: {
- // ┌─ any configuration supported by rollup, see https://rollupjs.org/guide/en/#configuration-files
- // ...
-
- /** list of plugin and its options */
- plugins?:
- | NormalizedRollupConfig['plugins']
- | Array<
- | string
- | [name: string]
- | [
- name: string,
- options:
- | Record
- | `@apply ${string}`
- | `@import ${string}`
- | null,
- ]
- >;
- };
- };
- /** variables to be substituted in templates */
- variable?: {
- /** the directory containing all source code (default: source) */
- source?: string;
- /** the directory containing all output tile (default: source) */
- output?: string;
- };
-}
-```
-
-For generating `rollup.config.ts`, this preset also support the `@apply` and `@import` directives such that you can also import configuration from other packages or ts/js files.
-
-The usage of the directives is simple. In any part of the configuration for rollup, you can simply put
-`@apply package_name` or `@import package_name` and the preset will automatically replace the content with an imported variable. For example:
-
-```json
-{
- "rollup": {
- "plugins": [
- [
- "@apply rollup-plugin-postcss[default]",
- { "plugins": "@import ./postcss.config[default.plugins]" }
- ]
- ]
- }
-}
-```
-
-will create a `rollup.config.ts` file with the following content:
-
-```ts
-import * as import0 from 'rollup-plugin-postcss';
-import * as import1 from './postcss.config';
-
-export default {
- plugins: [import0.default(...[{ plugins: import1.default.plugins }])],
-};
-```
-
-The syntax for both the directives is quite similar.
-Use `@apply` in a situation that you have to invoke a function from an imported package,
-such as `rollup-plugin-postcss` in the above example.
-You can also specify the arguments for the invoked function in the form of `["@apply package", options]`
-
-For `@import`, use it if you want to import value from another package or ts/js file.
-For example, `@import ./postcss.config[default.plugins]` would allow you to refer `default.plugins` from `./postcss.config` in the above example.
-
-In addition to the directives, to specify the plugins for rollup, you can write it in three ways similar to babel.
-
-1. A object with plugin name as the key and its options as its value e.g. `{'@apply @rollup/plugin-typescript[default]': {options}}`
-2. Name of a plugin in an array e.g. `['@apply @rollup/plugin-typescript[default]']`
-3. Doublet of `[plugin name, options]` in an array e.g. `[['@apply @rollup/plugin-typescript[default]', {options}]]`
+By default, this preset exports a handy configuration for a typescript project.
+You can further customize (either extending or replacing) the configuration by specifying the changes in the config file `presetter.config.ts`.
## Script Template Summary
-- **`run build`**: Bundle your code via rollup
-- **`run develop`**: Continuous code build and watch
+- **`run build`**: Transpile source code from typescript and replace any mapped paths
+- **`run clean`**: Clean up any previously transpiled code
+- **`run develop -- `**: Create a service that run the specified file whenever the source has changed
+- **`run test`**: Run all tests
+- **`run watch`**: Rerun all tests whenever the source has change
+- **`run coverage`**: Run all test with coverage report
+- **`run release`**: Bump the version and automatically generate a change log
+- **`run release -- --prerelease `**: Release with a prerelease tag
diff --git a/packages/preset-rollup/configs/rollup.yaml b/packages/preset-rollup/configs/rollup.yaml
deleted file mode 100644
index 34d74721..00000000
--- a/packages/preset-rollup/configs/rollup.yaml
+++ /dev/null
@@ -1,26 +0,0 @@
-input: '{source}/index.ts'
-output:
- - file: '{output}/index.js'
- format: cjs
- sourcemap: true
- - file: '{output}/index.mjs'
- format: es
- sourcemap: true
-plugins:
- - '@apply @rollup/plugin-typescript[default]'
- - '@apply rollup-plugin-tsconfig-paths[default]'
- - '@apply @rollup/plugin-node-resolve[default]'
- - - '@apply @rollup/plugin-commonjs[default]'
- - extensions:
- - .js
- - .jsx
- - .ts
- - .tsx
- - '@apply @rollup/plugin-json[default]'
- - '@apply @rollup/plugin-graphql[default]'
- - '@apply @rollup/plugin-image[default]'
- - '@apply @rollup/plugin-yaml[default]'
- - - '@apply rollup-plugin-postcss[default]'
- - inject:
- insertAt: top
- - '@apply rollup-plugin-visualizer[visualizer]'
diff --git a/packages/preset-rollup/source/index.ts b/packages/preset-rollup/source/index.ts
index 0ba1fa6a..22422af5 100644
--- a/packages/preset-rollup/source/index.ts
+++ b/packages/preset-rollup/source/index.ts
@@ -1,61 +1,37 @@
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
-import { loadFile, substitute } from 'presetter';
+import { preset } from 'presetter-types';
-import { getRollupParameter } from './rollup';
-
-import type { PresetAsset } from 'presetter-types';
-
-import type { RollupConfig } from './rollup';
+import rollup from './rollup.template';
const DIR = fileURLToPath(dirname(import.meta.url));
// paths to the template directory
const TEMPLATES = resolve(DIR, '..', 'templates');
-const CONFIGS = resolve(DIR, '..', 'configs');
-
-/** config for this preset */
-export interface PresetConfig {
- rollup?: RollupConfig;
-}
/** list of configurable variables */
-export interface Variable {
+export interface Variables {
/** the directory containing all source code (default: source) */
source: string;
/** the directory containing all the compiled files (default: lib) */
output: string;
}
-export const DEFAULT_VARIABLE = {
+export const DEFAULT_VARIABLES = {
source: 'source',
output: 'lib',
-} satisfies Variable;
-
-/**
- * get the list of templates provided by this preset
- * @returns list of preset templates
- */
-export default function (): PresetAsset {
- return {
- template: {
- 'rollup.config.ts': (context) => {
- const content = loadFile(
- resolve(TEMPLATES, 'rollup.config.ts'),
- 'text',
- );
- const variable = getRollupParameter(context);
-
- return substitute(content, variable);
- },
+} satisfies Variables;
+
+export default preset('presetter-preset-rollup', {
+ variables: DEFAULT_VARIABLES,
+ scripts: resolve(TEMPLATES, 'scripts.yaml'),
+ assets: {
+ 'rollup.config.ts': rollup,
+ },
+ override: {
+ assets: {
+ '.gitignore': ['/rollup.config.ts'],
},
- scripts: resolve(TEMPLATES, 'scripts.yaml'),
- noSymlinks: ['rollup.config.ts'],
- supplementaryConfig: {
- gitignore: ['/rollup.config.ts'],
- rollup: resolve(CONFIGS, 'rollup.yaml'),
- },
- variable: DEFAULT_VARIABLE,
- };
-}
+ },
+});
diff --git a/packages/preset-rollup/source/plugin.ts b/packages/preset-rollup/source/plugin.ts
deleted file mode 100644
index b59e1d9e..00000000
--- a/packages/preset-rollup/source/plugin.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import { isDirective } from 'presetter';
-
-import type { ApplyDirective, ImportDirective } from 'presetter';
-import type { JsonObject, LiteralUnion } from 'type-fest';
-
-/** full configuration about a plugin */
-export type PluginConfiguration =
- | [name: PluginHeader]
- | [name: PluginHeader, options: PluginOptions | null];
-
-/** specification of a plugin name and its handling direction (e.g. by invoking the function or just simply specify the name) */
-export type PluginHeader = LiteralUnion;
-
-/** options for a plugin */
-export type PluginOptions = JsonObject | ApplyDirective | ImportDirective;
-
-/** plugin configuration as an object */
-export type PluginObject = Record;
-
-/** plugin configuration as an array */
-export type PluginList = PluginListItem[];
-
-/** possible types for individual item in a PluginList */
-type PluginListItem = PluginHeader | [name: PluginHeader] | PluginConfiguration;
-
-/** all possible configuration form for a collection of plugins */
-export type PluginManifest = PluginList | PluginObject;
-
-/**
- * ensure that the given value is a valid PluginManifest
- * @param value value to be tested
- * @returns nothing if it's a pass
- */
-export function assertPluginManifest(
- value: unknown,
-): asserts value is PluginManifest {
- if (typeof value === 'object') {
- if (Array.isArray(value)) {
- return assertPluginList(value);
- } else if (value !== null) {
- return assertPluginObject(value as Record);
- }
- }
-
- throw new TypeError('plugin manifest is not in a supported format');
-}
-
-/**
- * ensure that the given value is a valid PluginObject
- * @param value value to be tested
- */
-export function assertPluginObject(
- value: Record,
-): asserts value is PluginObject {
- // all values must be an object
- if (
- [...Object.values(value)].some(
- (opt) => typeof opt !== 'object' && !isDirective(opt),
- )
- ) {
- throw new TypeError('all plugin options must be a object');
- }
-}
-
-/**
- * ensure that the given value is a valid PluginList
- * @param value value to be tested
- */
-export function assertPluginList(
- value: unknown[],
-): asserts value is PluginList {
- for (const plugin of value) {
- assertPluginListItem(plugin);
- }
-}
-
-const PLUGIN_LIST_MAX_ITEMS = 2;
-
-/**
- * ensure that the given value is a valid PluginListItem
- * @param value value to be tested
- */
-export function assertPluginListItem(
- value: unknown,
-): asserts value is PluginListItem {
- if (
- typeof value !== 'string' &&
- !(
- Array.isArray(value) &&
- value.length <= PLUGIN_LIST_MAX_ITEMS &&
- typeof value[0] === 'string' &&
- (isDirective(value[1]) ||
- ['undefined', 'object'].includes(typeof value[1]))
- )
- ) {
- throw new TypeError(
- 'a plugin manifest in an array form must be in either one of the following forms: string, [string], [string, object]',
- );
- }
-}
diff --git a/packages/preset-rollup/source/rollup.template.ts b/packages/preset-rollup/source/rollup.template.ts
new file mode 100644
index 00000000..298c666b
--- /dev/null
+++ b/packages/preset-rollup/source/rollup.template.ts
@@ -0,0 +1,65 @@
+/* v8 ignore start */
+
+import commonjs from '@rollup/plugin-commonjs';
+import graphql from '@rollup/plugin-graphql';
+import image from '@rollup/plugin-image';
+import json from '@rollup/plugin-json';
+import resolve from '@rollup/plugin-node-resolve';
+import typescript from '@rollup/plugin-typescript';
+import yaml from '@rollup/plugin-yaml';
+import { asset } from 'presetter-types';
+import postcss from 'rollup-plugin-postcss';
+import tsconfigPaths from 'rollup-plugin-tsconfig-paths';
+import visualizer from 'rollup-plugin-visualizer';
+
+import type { RollupOptions } from 'rollup';
+
+const typescriptPlugin = typescript();
+const tsconfigPathsPlugin = tsconfigPaths();
+const resolvePlugin = resolve();
+const commonjsPlugin = commonjs({
+ extensions: ['.js', '.jsx', '.ts', '.tsx'],
+});
+const jsonPlugin = json();
+const graphqlPlugin = graphql();
+const imagePlugin = image();
+const yamlPlugin = yaml();
+const postcssPlugin = postcss({
+ inject: {
+ insertAt: 'top',
+ },
+});
+const visualizerPlugin = visualizer();
+
+const plugins = [
+ typescriptPlugin,
+ tsconfigPathsPlugin,
+ resolvePlugin,
+ commonjsPlugin,
+ jsonPlugin,
+ graphqlPlugin,
+ imagePlugin,
+ yamlPlugin,
+ postcssPlugin,
+ visualizerPlugin,
+];
+
+export default asset<{ default: RollupOptions }>((current, { variables }) => ({
+ default: {
+ ...current,
+ input: `${variables.source!}/index.ts`,
+ output: [
+ {
+ file: `${variables.outout!}/index.js`,
+ format: 'cjs',
+ sourcemap: true,
+ },
+ {
+ file: `${variables.outout!}/index.mjs`,
+ format: 'es',
+ sourcemap: true,
+ },
+ ],
+ plugins,
+ },
+}));
diff --git a/packages/preset-rollup/source/rollup.ts b/packages/preset-rollup/source/rollup.ts
deleted file mode 100644
index 7e8b916d..00000000
--- a/packages/preset-rollup/source/rollup.ts
+++ /dev/null
@@ -1,201 +0,0 @@
-import {
- isDirective,
- isJsonObject,
- merge,
- resolveDirective,
- substitute,
-} from 'presetter';
-
-import { assertPluginManifest } from './plugin';
-
-import type { ApplyDirective, ImportDirective } from 'presetter';
-import type { ResolvedPresetContext } from 'presetter-types';
-
-import type {
- PluginConfiguration,
- PluginList,
- PluginManifest,
- PluginObject,
-} from './plugin';
-
-/** preset configuration for rollup */
-export interface RollupConfig {
- [index: string]: unknown;
- /** list of plugin and its options */
- plugins?: PluginManifest | ApplyDirective | ImportDirective;
-}
-
-/** genuine configuration that rollup would take, making sure all plugins are a list */
-interface TrueRollupConfig {
- [index: string]: unknown;
- /** list of plugin and its options */
- plugins?: PluginConfiguration[];
-}
-
-/** transformed configuration for rollup, with all plugins represented by an object */
-interface IntermediateRollupConfig {
- [index: string]: unknown;
- /** list of plugin and its options */
- plugins?: PluginObject;
-}
-
-/**
- * get template parameters for rollup
- * @param context context about the build environment
- * @returns template parameter related to rollup
- */
-export function getRollupParameter(
- context: ResolvedPresetContext,
-): Record<'rollupImport' | 'rollupExport', string> {
- const { config, variable } = context.custom;
-
- const normalizedConfig = substitute(
- normalizeConfig(transformConfig({ ...config.rollup })),
- variable,
- );
-
- return generateRollupParameter(normalizedConfig, context);
-}
-
-/**
- * generate template parameters for rollup
- * @param config normalized rollup config
- * @param context context about the build environment
- * @returns template parameter related to rollup
- */
-function generateRollupParameter(
- config: TrueRollupConfig,
- context: ResolvedPresetContext,
-): Record<'rollupImport' | 'rollupExport', string> {
- const { importMap, stringifiedConfig } = resolveDirective(config, context);
-
- // generate import statements
- const rollupImport = Object.entries(importMap)
- .map(([name, resolved]) => `import * as ${resolved} from '${name}';`)
- .join('\n');
-
- // generate export statements
- const rollupExport = `export default ${stringifiedConfig}`;
-
- return { rollupImport, rollupExport };
-}
-
-/**
- * normalize rollup config with all plugins represented as a list
- * @param config transformed config
- * @returns config that rollup would take
- */
-function normalizeConfig(config: IntermediateRollupConfig): TrueRollupConfig {
- return Object.fromEntries(
- Object.entries(config).map(([key, value]): [string, unknown] => {
- return [
- key,
- isDirective(value) ? value : normalizeConfigValue(key, value),
- ];
- }),
- );
-}
-
-/**
- * try to normalize any nested configuration
- * @param key field name
- * @param value value of a field
- * @returns normalized value
- */
-function normalizeConfigValue(key: string, value: unknown): unknown {
- switch (key) {
- case 'plugins':
- return [
- ...Object.entries(value as PluginObject)
- .filter(([_, options]) => options !== null)
- .map(([plugin, options]) =>
- [plugin, normalizeConfigValue(plugin, options)].filter(
- (element) => element !== undefined,
- ),
- ),
- ];
- default:
- return isJsonObject(value)
- ? normalizeConfig(value as IntermediateRollupConfig)
- : value;
- }
-}
-
-/**
- * transform rollup config with plugins represented by an object for better merging
- * @param config rollup config in .presetterrc
- * @returns transformed config
- */
-function transformConfig(
- config: Record,
-): IntermediateRollupConfig {
- return Object.fromEntries(
- Object.entries(config).map(([key, value]): [string, unknown] => {
- return [
- key,
- isDirective(value) ? value : transformConfigValue(key, value),
- ];
- }),
- );
-}
-
-/**
- * try to transform any nested configuration
- * @param key field name
- * @param value value of a field
- * @returns transformed value
- */
-function transformConfigValue(key: string, value: unknown): unknown {
- switch (key) {
- case 'plugins':
- assertPluginManifest(value);
-
- return objectifyPlugins(value);
-
- default:
- return isJsonObject(value) ? transformConfig(value) : value;
- }
-}
-
-/**
- * objectify rollup plugins
- * @param plugins rollup plugin config
- * @returns normalized plugin config
- */
-function objectifyPlugins(
- plugins: PluginManifest,
-): IntermediateRollupConfig['plugins'] {
- const pluginList: PluginConfiguration[] = Array.isArray(plugins)
- ? arrayToPluginConfiguration(plugins)
- : objectToPluginConfiguration(plugins);
-
- return pluginList.reduce(
- (normalizedPlugin, [name, options]) =>
- merge(normalizedPlugin, { [name]: options }),
- {},
- );
-}
-
-/**
- * normalize rollup plugin config in array form
- * @param plugins rollup plugin config in array form
- * @returns normalized plugin config
- */
-function arrayToPluginConfiguration(
- plugins: PluginList,
-): PluginConfiguration[] {
- return plugins.map((plugin) =>
- typeof plugin === 'string' ? [plugin] : plugin,
- );
-}
-
-/**
- * normalize rollup plugin config in object form
- * @param plugins rollup plugin config in object form
- * @returns normalized plugin config
- */
-function objectToPluginConfiguration(
- plugins: PluginObject,
-): PluginConfiguration[] {
- return [...Object.entries(plugins)];
-}
diff --git a/packages/preset-rollup/spec/index.spec.ts b/packages/preset-rollup/spec/index.spec.ts
index 30f2a768..75194295 100644
--- a/packages/preset-rollup/spec/index.spec.ts
+++ b/packages/preset-rollup/spec/index.spec.ts
@@ -1,38 +1,43 @@
import { existsSync, readdirSync } from 'node:fs';
import { resolve } from 'node:path';
-import { loadDynamicMap, resolveContext } from 'presetter';
+import { listAssetNames, resolveAssets, resolvePreset } from 'presetter';
import { describe, expect, it, vi } from 'vitest';
-import getPresetAsset from '#';
+import preset, { DEFAULT_VARIABLES as variables } from '#';
+
+import type { PresetContext } from 'presetter-types';
vi.mock('node:path', { spy: true });
-describe('fn:getPresetAsset', () => {
- it('use all templates', async () => {
- const asset = getPresetAsset();
- const context = await resolveContext({
- graph: [{ name: 'preset', asset, nodes: [] }],
- context: {
- target: { name: 'preset', root: '/', package: {} },
- custom: { preset: 'preset' },
- },
- });
-
- // load all potential dynamic content
- await loadDynamicMap(asset.supplementaryConfig, context);
- await loadDynamicMap(asset.template, context);
-
- const CONFIGS = resolve(import.meta.dirname, '..', 'configs');
- const configs = existsSync(CONFIGS) ? readdirSync(CONFIGS) : [];
- const TEMPLATES = resolve(import.meta.dirname, '..', 'templates');
+const OVERRIDES = resolve(import.meta.dirname, '..', 'overrides');
+const TEMPLATES = resolve(import.meta.dirname, '..', 'templates');
+
+const context = {
+ root: '/path/to/project',
+ package: {},
+} satisfies PresetContext;
+
+describe('fn:preset', () => {
+ it('should use all templates', async () => {
+ const node = await resolvePreset(preset, context);
+ listAssetNames(node, { ...context, variables });
+
+ const overrides = existsSync(OVERRIDES) ? readdirSync(OVERRIDES) : [];
const templates = existsSync(TEMPLATES) ? readdirSync(TEMPLATES) : [];
- for (const path of configs) {
- expect(vi.mocked(resolve)).toBeCalledWith(CONFIGS, path);
+ for (const path of overrides) {
+ expect(vi.mocked(resolve)).toHaveBeenCalledWith(OVERRIDES, path);
}
for (const path of templates) {
- expect(vi.mocked(resolve)).toBeCalledWith(TEMPLATES, path);
+ expect(vi.mocked(resolve)).toHaveBeenCalledWith(TEMPLATES, path);
}
});
+
+ it('should be able to resolve all assets', async () => {
+ const node = await resolvePreset(preset, context);
+ const result = resolveAssets(node, context);
+
+ await expect(result).resolves.not.toThrow();
+ });
});
diff --git a/packages/preset-rollup/spec/plugin.spec.ts b/packages/preset-rollup/spec/plugin.spec.ts
deleted file mode 100644
index dba27c84..00000000
--- a/packages/preset-rollup/spec/plugin.spec.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import { describe, expect, it } from 'vitest';
-
-import {
- assertPluginList,
- assertPluginListItem,
- assertPluginManifest,
- assertPluginObject,
-} from '#plugin';
-
-describe('fn:assertPluginListItem', () => {
- it('pass with just a string', () => {
- expect(() => assertPluginListItem('plugin')).not.toThrow();
- });
-
- it('pass with a string in an array', () => {
- expect(() => assertPluginListItem(['plugin'])).not.toThrow();
- });
-
- it('pass with a string and its options in an array', () => {
- expect(() =>
- assertPluginListItem(['plugin', { options: true }]),
- ).not.toThrow();
- });
-
- it('fails with a non-string header', () => {
- expect(() => assertPluginListItem([0])).toThrow(TypeError);
- });
-
- it('fails with an array more than 2 items', () => {
- expect(() =>
- assertPluginListItem(['plugin', { options: true }, 'extra']),
- ).toThrow(TypeError);
- });
-});
-
-describe('fn:assertPluginList', () => {
- it('pass with a valid plugin configuration list', () => {
- expect(() =>
- assertPluginList(['plugin', ['plugin'], ['plugin', { options: true }]]),
- ).not.toThrow();
- });
-
- it('fail with any invalid plugin configurations', () => {
- expect(() =>
- assertPluginList([
- 'plugin',
- ['plugin'],
- ['plugin', { options: true }],
- { invalid: true },
- ]),
- ).toThrow(TypeError);
- });
-});
-
-describe('fn:assertPluginObject', () => {
- it('pass with a valid plugin configuration object', () => {
- expect(() =>
- assertPluginObject({ plugin: { options: true } }),
- ).not.toThrow();
- });
-
- it('fail with any invalid plugin options', () => {
- expect(() => assertPluginObject({ plugin: true })).toThrow(TypeError);
- });
-});
-
-describe('fn:assertPluginManifest', () => {
- it('pass with a valid plugin configuration object', () => {
- expect(() =>
- assertPluginManifest({ plugin: { options: true } }),
- ).not.toThrow();
- });
-
- it('pass with a valid plugin configuration list', () => {
- expect(() =>
- assertPluginManifest([
- 'plugin',
- ['plugin'],
- ['plugin', { options: true }],
- ]),
- ).not.toThrow();
- });
-
- it('fail with any invalid manifest', () => {
- expect(() => assertPluginManifest(null)).toThrow(TypeError);
- expect(() => assertPluginManifest('invalid')).toThrow(TypeError);
- });
-});
diff --git a/packages/preset-rollup/spec/rollup.spec.ts b/packages/preset-rollup/spec/rollup.spec.ts
deleted file mode 100644
index 681ce7a4..00000000
--- a/packages/preset-rollup/spec/rollup.spec.ts
+++ /dev/null
@@ -1,207 +0,0 @@
-import { describe, expect, it } from 'vitest';
-
-import { getRollupParameter } from '#rollup';
-
-import type { Config, ResolvedPresetContext } from 'presetter-types';
-
-describe('fn:getRollupParameter', () => {
- const generateContext = (config?: Config): ResolvedPresetContext => ({
- target: { name: 'target', root: '/path/to/target', package: {} },
- custom: {
- preset: 'preset',
- config: config ? { rollup: config } : {},
- noSymlinks: [],
- variable: {},
- },
- });
-
- it('add plugins by importing from another config files', async () => {
- expect(
- getRollupParameter(
- generateContext({
- plugins: '@import config[plugins]',
- }),
- ),
- ).toEqual({
- rollupImport: `import * as import0 from 'config';`,
- rollupExport: 'export default {"plugins": import0.plugins}',
- });
- });
-
- it('add a plugin by adding the plugin in the object form, using the supplied options', async () => {
- expect(
- getRollupParameter(
- generateContext({
- plugins: { '@apply newPlugin': { name: 'newPlugin' } },
- }),
- ),
- ).toEqual({
- rollupImport: `import * as import0 from 'newPlugin';`,
- rollupExport:
- 'export default {"plugins": [import0(...([{"name": "newPlugin"}] as const))]}',
- });
- });
-
- it('add a plugin by just the plugin name, using everything default', async () => {
- expect(
- getRollupParameter(
- generateContext({
- plugins: ['@apply newPlugin'],
- }),
- ),
- ).toEqual({
- rollupImport: `import * as import0 from 'newPlugin';`,
- rollupExport: 'export default {"plugins": [import0(...([] as const))]}',
- });
- });
-
- it('add a plugin by adding the plugin in the array form, using everything default', async () => {
- expect(
- getRollupParameter(
- generateContext({
- plugins: [['@apply newPlugin']],
- }),
- ),
- ).toEqual({
- rollupImport: `import * as import0 from 'newPlugin';`,
- rollupExport: 'export default {"plugins": [import0(...([] as const))]}',
- });
- });
-
- it('add a plugin by adding the plugin in the array form, using the supplied options', async () => {
- expect(
- getRollupParameter(
- generateContext({
- plugins: [['@apply newPlugin', { name: 'newPlugin' }]],
- }),
- ),
- ).toEqual({
- rollupImport: `import * as import0 from 'newPlugin';`,
- rollupExport:
- 'export default {"plugins": [import0(...([{"name": "newPlugin"}] as const))]}',
- });
- });
-
- it('remove a plugin by setting the plugin config as null', async () => {
- expect(
- getRollupParameter(
- generateContext({
- plugins: {
- '@apply pluginWithOptions': null,
- },
- }),
- ),
- ).toEqual({
- rollupImport: ``,
- rollupExport: 'export default {"plugins": []}',
- });
- });
-
- it('add a plugin from a named import', async () => {
- expect(
- getRollupParameter(
- generateContext({
- plugins: {
- '@apply pluginWithOptions': null,
- '@apply pluginWithoutOptions': null,
- '@apply newPlugin[plugin]': { options: true },
- },
- }),
- ),
- ).toEqual({
- rollupImport: `import * as import0 from 'newPlugin';`,
- rollupExport:
- 'export default {"plugins": [import0.plugin(...([{"options": true}] as const))]}',
- });
- });
-
- it('generate default parameters if no further config is given', async () => {
- expect(getRollupParameter(generateContext())).toEqual({
- rollupImport: ``,
- rollupExport: 'export default {}',
- });
- });
-
- it('generate config with extra options other than plugins', async () => {
- expect(
- getRollupParameter(
- generateContext({
- cache: null,
- extra: { options: true },
- external: ['import1', 'import2'],
- }),
- ),
- ).toEqual({
- rollupImport: ``,
- rollupExport:
- 'export default {"cache": null, "extra": {"options": true}, "external": ["import1", "import2"]}',
- });
- });
-
- it('generate extra import statements for imports within plugin options', async () => {
- expect(
- getRollupParameter(
- generateContext({
- plugins: {
- '@apply pluginWithOptions': '@import another',
- '@apply pluginWithoutOptions': '@import another',
- },
- }),
- ),
- ).toEqual({
- rollupImport: `import * as import0 from 'another';\nimport * as import1 from 'pluginWithOptions';\nimport * as import2 from 'pluginWithoutOptions';`,
- rollupExport: `export default {"plugins": [import1(...([import0] as const)), import2(...([import0] as const))]}`,
- });
-
- expect(
- getRollupParameter(
- generateContext({
- plugins: [
- ['@apply pluginWithOptions', '@import another'],
- ['@apply pluginWithoutOptions', '@import another'],
- ],
- }),
- ),
- ).toEqual({
- rollupImport: `import * as import0 from 'another';\nimport * as import1 from 'pluginWithOptions';\nimport * as import2 from 'pluginWithoutOptions';`,
- rollupExport: `export default {"plugins": [import1(...([import0] as const)), import2(...([import0] as const))]}`,
- });
- });
-
- it('generate only one import statement per unique import', async () => {
- expect(
- getRollupParameter(
- generateContext({
- plugins: {
- '@apply pluginWithOptions': null,
- '@apply pluginWithoutOptions': null,
- '@apply plugin0': '@import another[export0]',
- '@apply plugin1': '@import another[export1]',
- },
- }),
- ),
- ).toEqual({
- rollupImport: `import * as import0 from 'another';\nimport * as import1 from 'plugin0';\nimport * as import2 from 'plugin1';`,
- rollupExport: `export default {"plugins": [import1(...([import0.export0] as const)), import2(...([import0.export1] as const))]}`,
- });
- });
-
- it('support nested plugin declaration', async () => {
- expect(
- getRollupParameter(
- generateContext({
- plugins: {
- '@apply pluginWithOptions': null,
- '@apply pluginWithoutOptions': null,
- '@apply plugin0': {
- plugins: { another: '@import options[export0]' },
- },
- },
- }),
- ),
- ).toEqual({
- rollupImport: `import * as import0 from 'options';\nimport * as import1 from 'plugin0';`,
- rollupExport: `export default {"plugins": [import1(...([{"plugins": [["another", import0.export0]]}] as const))]}`,
- });
- });
-});
diff --git a/packages/preset-rollup/templates/rollup.config.ts b/packages/preset-rollup/templates/rollup.config.ts
deleted file mode 100644
index dc2365f0..00000000
--- a/packages/preset-rollup/templates/rollup.config.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-{rollupImport}
-
-{rollupExport}
\ No newline at end of file
diff --git a/packages/preset-strict/.npmignore b/packages/preset-strict/.npmignore
index 309ad71f..a328dcaf 100644
--- a/packages/preset-strict/.npmignore
+++ b/packages/preset-strict/.npmignore
@@ -3,5 +3,5 @@
!/lib/**
!/generated/**
-!/configs/**
+!/overrides/**
!/templates/**
\ No newline at end of file
diff --git a/packages/preset-strict/README.md b/packages/preset-strict/README.md
index d9f49ed9..894eb308 100644
--- a/packages/preset-strict/README.md
+++ b/packages/preset-strict/README.md
@@ -16,35 +16,42 @@
-**presetter-preset-strict** is an opinionated extension of [**presetter-preset-esm**](https://github.com/alvis/presetter/tree/master/packages/preset-esm) with stricter lint rules. As the same as presetter-preset-esm, it's designed to help you get started with a typescript project in a fraction of time you usually take via [**presetter**](https://github.com/alvis/presetter).
+**presetter-preset-strict** is an opinionated preset with stricter rules on eslint and vitest. It's designed to help you get started with a typescript project in a fraction of time you usually take via [**presetter**](https://github.com/alvis/presetter).
-With presetter-preset-strict, it provides everything bundled from presetter-preset-esm, plus additional lint rules & 🐶 husky! You can check out the [additional rules here](https://github.com/alvis/presetter/tree/master/packages/preset-strict/templates).
+To work with presetter-preset-strict, you'll need another preset like [`presetter-preset-esm`](https://github.com/alvis/presetter/blob/master/packages/preset-esm) for ESM support and other basic toolings.
-In addition to a set of opinionated configuration files, it also provides a number of essential lifecycle and helper commands.
-
-## Features
+You can check out the [additional rules here](https://github.com/alvis/presetter/tree/master/packages/preset-strict/source).
-- 👥 Babel
-- 🚿 ESLint
-- 🐶 Husky
-- 🧪 Vitest
-- 💅 Prettier
-- 📤 Standard Version
-- 💯 Typescript
+In addition to a set of opinionated configuration files, it also provides a number of essential lifecycle and helper commands.
## Quick Start
[**FULL DOCUMENTATION IS AVAILABLE HERE**](https://github.com/alvis/presetter/blob/master/README.md)
-1. Bootstrap your project with presetter-preset-strict
+### 1. Bootstrap your project with presetter-preset-strict
+
+On your project root, create a `presetter.config.ts` file with the following content:
-```shell
-npx presetter use presetter-preset-strict
+```typescript
+// presetter.config.ts
+
+import { preset } from 'presetter';
+import esm from 'presetter-preset-esm';
+import strict from 'presetter-preset-strict';
+
+export default preset('project name', {
+ // NOTE
+ // you may need an additional preset like presetter-preset-esm for ESM support and other basic toolings
+ extends: [esm, strict],
+ override: {
+ // override the configuration here
+ },
+});
```
-That's. One command and you're set.
+Then, install your project as usual with `npm install` or any package manager you prefer.
-2. Develop and run life cycle scripts provided by the preset
+### 2. Develop and run life cycle scripts provided by the preset
At this point, all development packages specified in the preset are installed,
and now you can try to run some example life cycle scripts (e.g. run prepare).
@@ -53,11 +60,11 @@ and now you can try to run some example life cycle scripts (e.g. run prepare).
## Project Structure
-After installation, your project file structure should resemble the following or with more configuration files if you also installed other presets such as [`presetter-preset-rollup`](https://github.com/alvis/presetter/blob/master/packages/preset-rollup).
+After installation, your project file structure should resemble the following, or include more configuration files if you also installed other presets.
Implement your business logic under `source` and prepare tests under `spec`.
-**TIPS** You can always change the source directory to other (e.g. src) by setting the `source` variable in `.presetterrc.json`. See the [customization](https://github.com/alvis/presetter/blob/master/packages/preset-strict#customization) section below for more details.
+**TIPS** You can always change the source directory to other (e.g. src) by setting the `source` variable in `presetter.config.ts`. See the [customization](https://github.com/alvis/presetter/blob/master/packages/preset-essentials#customization) section below for more details.
```
(root)
@@ -66,7 +73,7 @@ Implement your business logic under `source` and prepare tests under `spec`.
├─ .lintstagedrc.json
├─ .npmignore
├─ .prettierrc.json
- ├─ .presetterrc.json
+ ├─ presetter.config.ts
├─ node_modules
├─ source
│ ├─
@@ -83,52 +90,8 @@ Implement your business logic under `source` and prepare tests under `spec`.
## Customization
-By default, this preset exports a handy configuration for rollup for a typescript project.
-But you can further customize (either extending or replacing) the configuration by specifying the change in the config file (`.presetterrc` or `.presetterrc.json`).
-
-These settings are available in the `config` field in the config file. For directories, the setting is specified in the `variable` field.
-
-The structure of `.presetterrc` should follow the interface below:
-
-```ts
-interface PresetterRC {
- /** name of the preset e.g. presetter-preset-strict */
- name: string | string[];
- /** additional configuration passed to the preset for generating the configuration files */
- config?: {
- // ┌─ configuration for other tools via other presets (e.g. presetter-preset-rollup)
- // ...
-
- /** configuration to be merged with .eslintrc */
- eslint?: Record;
- /** configuration to be merged with .lintstagedrc */
- lintstaged?: Record;
- /** patterns to be added to .gitignore */
- gitignore?: string[];
- /** patterns to be added to .npmignore */
- npmignore?: string[];
- /** configuration to be merged with .presetterrc */
- prettier?: Record;
- /** configuration to be merged with tsconfig.json */
- tsconfig?: Record;
- /** a list of config files not to be created */
- ignores?: string[];
- };
- /** relative path to root directories for different file types */
- variable?: {
- /** the directory containing the whole repository (default: .) */
- root?: string;
- /** the directory containing all source code (default: source) */
- source?: string;
- /** the directory containing all typing files (default: types) */
- types?: string;
- /** the directory containing all output tile (default: source) */
- output?: string;
- /** the directory containing all test files (default: spec) */
- test?: string;
- };
-}
-```
+By default, this preset exports a handy configuration for a typescript project.
+You can further customize (either extending or replacing) the configuration by specifying the changes in the config file `presetter.config.ts`.
## Script Template Summary
diff --git a/packages/preset-strict/templates/scripts.yaml b/packages/preset-strict/overrides/scripts.yaml
similarity index 100%
rename from packages/preset-strict/templates/scripts.yaml
rename to packages/preset-strict/overrides/scripts.yaml
diff --git a/packages/preset-strict/package.json b/packages/preset-strict/package.json
index d29af469..49c1b99e 100644
--- a/packages/preset-strict/package.json
+++ b/packages/preset-strict/package.json
@@ -19,14 +19,6 @@
".": {
"default": "./lib/index.js",
"types": "./lib/index.d.ts"
- },
- "./eslint.config": {
- "default": "./lib/eslint.config.js",
- "types": "./lib/eslint.config.d.ts"
- },
- "./vitest.config": {
- "default": "./lib/vitest.config.js",
- "types": "./lib/vitest.config.d.ts"
}
},
"repository": {
@@ -34,7 +26,7 @@
"url": "git+https://github.com/alvis/presetter.git"
},
"scripts": {
- "prepare": "tsc --declaration --moduleResolution bundler --module esnext --target esnext --skipLibCheck --outdir lib source/index.ts source/eslint.config.ts source/vitest.config.ts",
+ "prepare": "tsc --declaration --moduleResolution bundler --module esnext --target esnext --skipLibCheck --outdir lib source/index.ts && tsc-esm-fix --sourceMap --target lib",
"bootstrap": "presetter bootstrap",
"build": "run build",
"coverage": "run coverage --",
@@ -45,8 +37,7 @@
},
"peerDependencies": {
"eslint-plugin-no-secrets": "^1.0.0",
- "eslint-plugin-sonarjs": "^2.0.0",
- "presetter-preset-essentials": "workspace:*"
+ "eslint-plugin-sonarjs": "^2.0.0"
},
"dependencies": {
"presetter-types": "workspace:*"
diff --git a/packages/preset-strict/source/eslint.config.ts b/packages/preset-strict/source/eslint.config.ts
deleted file mode 100644
index f1f89417..00000000
--- a/packages/preset-strict/source/eslint.config.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import sonarjs from 'eslint-plugin-sonarjs';
-import essentialConfig from 'presetter-preset-essentials/eslint.config';
-
-import noSecrets from 'eslint-plugin-no-secrets';
-
-import type { Linter } from 'eslint';
-
-const COGNITIVE_COMPLEXITY = 15;
-
-export default [
- ...essentialConfig,
- sonarjs.configs.recommended,
- {
- name: 'presetter-preset-strict',
- plugins: { 'no-secrets': noSecrets },
- rules: {
- // ECMAScript //
- '@typescript-eslint/prefer-nullish-coalescing': 'warn', // simplify logic using nullish coalescing operator
-
- // Best Practices //
- '@typescript-eslint/no-base-to-string': 'error', // disallow Object.toString() to avoid meaningless [object Object]
- '@typescript-eslint/promise-function-async': 'warn', // ensure functions returning promises are marked async
- '@typescript-eslint/return-await': 'error', // return awaited promises in try/catch for better stack trace
- '@typescript-eslint/no-unnecessary-condition': 'error', // remove redundant logic for reduced complexity
- 'curly': 'error', // require curly braces around control statements
- 'default-case': 'error', // ensure a default case in switch statements
- 'sonarjs/function-return-type': 'off', // use typescript-eslint/explicit-function-return-type instead
- 'sonarjs/no-nested-conditional': 'off', // impractical
- 'sonarjs/redundant-type-aliases': 'off', // off as it reduces readability
-
- // Code Quality //
- '@typescript-eslint/no-unused-expressions': [
- 'warn',
- {
- allowShortCircuit: true, // allow short-circuit expressions
- allowTernary: true, // allow ternary expressions
- },
- ],
- '@typescript-eslint/prefer-function-type': 'warn', // prefer function types over interfaces for functions
- '@typescript-eslint/unified-signatures': 'warn', // simplify overloaded function signatures
- 'no-secrets/no-secrets': [
- // no sensitive information
- 'error',
- {
- additionalDelimiters: [
- '(?=[A-Z][a-z])', // split a camel case
- ':', // split a key from its value
- '_', // split a key from its value
- '\\?', // split an URL from its query string
- '&', // split a query string from its key - value pair
- '/', // split a URL from its path
- '#', // split a URL from its fragment
- ],
- ignoreContent: ['&', '%20'], // ignore encoded URL space character
- },
- ],
- 'sonarjs/cognitive-complexity': [
- 'warn',
- COGNITIVE_COMPLEXITY, // limit cognitive complexity to improve readability
- ],
- 'sonarjs/new-cap': 'off', // use typescript-eslint/naming-convention instead
- 'sonarjs/no-small-switch': 'off', // allow small switch statements if beneficial for readability
- 'sonarjs/sonar-no-unused-vars': 'off', // use typescript-eslint/no-unused-vars instead
- 'complexity': 'warn', // limit cyclomatic complexity in functions
- 'max-lines': [
- 'warn',
- {
- max: 250, // limit file length for readability
- skipBlankLines: true, // skip blank lines in count
- skipComments: true, // skip comments in count
- },
- ],
- 'max-lines-per-function': [
- 'warn',
- {
- max: 60, // limit function length for readability
- skipBlankLines: true, // skip blank lines in count
- skipComments: true, // skip comments in count
- IIFEs: true, // allow longer length for IIFE functions
- },
- ],
- 'no-console': 'warn', // warn on console usage, often for debugging purposes
- 'no-eval': 'error', // disallow eval usage as it can introduce security risks
-
- // Error Prevention //
- '@typescript-eslint/no-misused-promises': [
- 'warn',
- {
- checksVoidReturn: false, // ensure await is used correctly before promises
- },
- ],
- 'sonarjs/assertions-in-tests': 'off', // off as it has too many false positives
- },
- },
-] satisfies Linter.Config[] as Linter.Config[];
diff --git a/packages/preset-strict/source/eslint.override.ts b/packages/preset-strict/source/eslint.override.ts
new file mode 100644
index 00000000..84403c90
--- /dev/null
+++ b/packages/preset-strict/source/eslint.override.ts
@@ -0,0 +1,22 @@
+/* v8 ignore start */
+
+import type { Linter } from 'eslint';
+
+export default [
+ {
+ name: 'presetter-preset-strict:override:test-files',
+ files: [
+ '**/*.e2e.ts',
+ '**/*.integration.ts',
+ '**/*.spec.ts',
+ '**/*.spec-d.ts',
+ '**/*.test.ts',
+ '**/*.test-d.ts',
+ '**/*.unit.ts',
+ '**/*.unit-d.ts',
+ ],
+ rules: {
+ 'sonarjs/no-duplicate-string': 'off', // permits duplicate strings in tests, as repeated values may be intentional
+ },
+ },
+] satisfies Linter.Config[] as Linter.Config[];
diff --git a/packages/preset-strict/source/eslint.template.ts b/packages/preset-strict/source/eslint.template.ts
new file mode 100644
index 00000000..dd43265a
--- /dev/null
+++ b/packages/preset-strict/source/eslint.template.ts
@@ -0,0 +1,138 @@
+/* v8 ignore start */
+
+import sonarjs from 'eslint-plugin-sonarjs';
+import { asset } from 'presetter-types';
+
+import noSecrets from 'eslint-plugin-no-secrets';
+
+import type { Linter } from 'eslint';
+
+const COGNITIVE_COMPLEXITY = 15;
+
+export default asset<{ default: Linter.Config[] }>((current) => {
+ const configs = current?.default ?? [];
+
+ const hasTypescriptEslint = configs.some(
+ (config) => !!config.plugins?.['@typescript-eslint'],
+ );
+
+ return {
+ default: [
+ ...configs,
+ sonarjs.configs.recommended,
+ {
+ name: 'presetter-preset-strict:generic',
+ rules: {
+ 'curly': 'error', // enforce curly braces around control statements for clarity and maintainability
+ 'default-case': 'error', // require a default case in switch statements to prevent unhandled cases
+ 'complexity': ['warn', { variant: 'modified' }], // warn on high cyclomatic complexity to encourage simpler, more readable code
+ 'max-lines': [
+ 'warn',
+ {
+ max: 250, // set a file length limit to improve maintainability
+ skipBlankLines: true, // exclude blank lines to focus on meaningful content
+ skipComments: true, // exclude comments to avoid penalizing well-documented files
+ },
+ ],
+ 'max-lines-per-function': [
+ 'warn',
+ {
+ max: 60, // enforce shorter functions for better readability and modularity
+ skipBlankLines: true, // ignore blank lines for cleaner metrics
+ skipComments: true, // ignore comments to avoid penalizing documentation
+ IIFEs: true, // allow longer lengths for IIFEs due to their self-contained nature
+ },
+ ],
+ 'no-console': 'warn', // discourage console usage in production code, but allow it for debugging
+ 'no-eval': 'error', // disallow eval due to its security risks and performance concerns
+ },
+ },
+ {
+ name: 'presetter-preset-strict:@typescript-eslint',
+ rules: hasTypescriptEslint && {
+ // ECMAScript //
+ '@typescript-eslint/prefer-nullish-coalescing': 'warn', // prefer nullish coalescing operator for cleaner and safer defaulting logic
+
+ // Best Practices //
+ '@typescript-eslint/no-base-to-string': 'error', // prevent unintended string conversions of objects
+ '@typescript-eslint/promise-function-async': 'warn', // ensure promise-returning functions are properly marked as async
+ '@typescript-eslint/return-await': 'error', // improve stack traces in try/catch by enforcing `return await`
+ '@typescript-eslint/no-unnecessary-condition': 'error', // detect and remove redundant conditional checks
+
+ // Code Quality //
+ '@typescript-eslint/no-unused-expressions': [
+ 'warn',
+ {
+ allowShortCircuit: true, // permit short-circuit expressions for practical use
+ allowTernary: true, // permit ternary expressions for concise logic
+ },
+ ],
+ '@typescript-eslint/prefer-function-type': 'warn', // prefer function types over interfaces for function declarations
+ '@typescript-eslint/unified-signatures': 'warn', // enforce simplified function overloads for better readability
+
+ // Error Prevention //
+ '@typescript-eslint/no-misused-promises': [
+ 'warn',
+ {
+ checksVoidReturn: false, // allow void-returning promises where appropriate
+ },
+ ],
+ },
+ },
+ {
+ name: 'presetter-preset-strict:no-secrets',
+ plugins: { 'no-secrets': noSecrets },
+ rules: {
+ 'no-secrets/no-secrets': [
+ 'error',
+ {
+ additionalDelimiters: [
+ '(?=[A-Z][a-z])', // split camel case for improved analysis
+ ':', // split key-value pairs
+ '_', // split underscores in identifiers
+ '\\?', // split URLs at query strings
+ '&', // split query string pairs
+ '/', // split paths in URLs
+ '#', // split fragments in URLs
+ ],
+ ignoreContent: ['&', '%20'], // allow common URL-encoded entities
+ },
+ ],
+ },
+ },
+ {
+ name: 'presetter-preset-strict:sonar',
+ rules: {
+ // Best Practices //
+ 'sonarjs/sonar-no-control-regex': 'off', // disable in favor of a more targeted control-regex rule
+ 'sonarjs/different-types-comparison': 'off', // rely on TypeScript for type safety and comparison checks
+ 'sonarjs/function-return-type': 'off', // handled more effectively by @typescript-eslint/explicit-function-return-type
+ 'sonarjs/no-async-constructor': 'off', // allow async constructors when necessary
+ 'sonarjs/no-empty-function': 'off', // prefer @typescript-eslint/no-empty-function for consistency
+ 'sonarjs/no-misused-promises': 'off', // handled by @typescript-eslint/no-misused-promises
+ 'sonarjs/no-nested-assignment': 'off', // permit nested assignments for improved clarity in specific cases
+ 'sonarjs/no-nested-conditional': 'off', // allow nested conditionals when readability isn't compromised
+ 'sonarjs/no-nested-functions': 'off', // use cognitive complexity rule instead
+ 'sonarjs/no-redeclare': 'off', // leverage TypeScript to detect redeclaration issues
+ 'sonarjs/no-throw-literal': 'off', // managed by @typescript-eslint/no-throw-literal
+ 'sonarjs/no-unused-expressions': 'off', // handled by @typescript-eslint/no-unused-expressions
+ 'sonarjs/pseudo-random': 'off', // unnecessary unless dealing with cryptography
+ 'sonarjs/prefer-nullish-coalescing': 'off', // defer to @typescript-eslint/prefer-nullish-coalescing
+ 'sonarjs/redundant-type-aliases': 'off', // allow for improved readability
+
+ // Code Quality //
+ 'sonarjs/cognitive-complexity': [
+ 'warn',
+ COGNITIVE_COMPLEXITY, // enforce cognitive complexity limits for better readability
+ ],
+ 'sonarjs/new-cap': 'off', // handled by @typescript-eslint/naming-convention
+ 'sonarjs/no-small-switch': 'off', // permit small switch cases for concise logic
+ 'sonarjs/sonar-no-unused-vars': 'off', // defer to @typescript-eslint/no-unused-vars
+
+ // Error Prevention //
+ 'sonarjs/assertions-in-tests': 'off', // disabled due to frequent false positives in test code
+ },
+ },
+ ] satisfies Linter.Config[] as Linter.Config[],
+ };
+});
diff --git a/packages/preset-strict/source/index.ts b/packages/preset-strict/source/index.ts
index 58ca6bf1..0c6f393f 100644
--- a/packages/preset-strict/source/index.ts
+++ b/packages/preset-strict/source/index.ts
@@ -1,41 +1,26 @@
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
-import type { PresetAsset } from 'presetter-types';
+import { preset } from 'presetter-types';
+
+import * as eslintOverride from './eslint.override';
+import eslintTemplate from './eslint.template';
+import * as vitestOverride from './vitest.override';
const DIR = dirname(fileURLToPath(import.meta.url));
// paths to the template directory
-const TEMPLATES = resolve(DIR, '..', 'templates');
-
-/** config for this preset */
-export interface PresetConfig {
- /** configuration to be merged with .eslintrc */
- eslint?: Record;
-}
-
-/** list of configurable variables */
-export interface Variable {
- /** the directory containing all source code (default: source) */
- source: string;
-}
-
-export const DEFAULT_VARIABLE = {
- source: 'source',
-} satisfies Variable;
+const OVERRIDES = resolve(DIR, '..', 'overrides');
-/**
- * get the list of templates provided by this preset
- * @returns list of preset templates
- */
-export default async function (): Promise {
- return {
- extends: ['presetter-preset-esm'],
- template: {
- 'eslint.config.ts': resolve(TEMPLATES, 'eslint.config.ts'),
- 'vitest.config.ts': resolve(TEMPLATES, 'vitest.config.ts'),
+export default preset('presetter-preset-strict', {
+ assets: {
+ 'eslint.config.ts': eslintTemplate,
+ },
+ override: {
+ scripts: resolve(OVERRIDES, 'scripts.yaml'),
+ assets: {
+ 'eslint.config.ts': eslintOverride,
+ 'vitest.config.ts': vitestOverride,
},
- scripts: resolve(TEMPLATES, 'scripts.yaml'),
- variable: DEFAULT_VARIABLE,
- };
-}
+ },
+});
diff --git a/packages/preset-strict/source/vitest.config.ts b/packages/preset-strict/source/vitest.config.ts
deleted file mode 100644
index e61cf36f..00000000
--- a/packages/preset-strict/source/vitest.config.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import essentialConfig from 'presetter-preset-essentials/vitest.config';
-
-import { defineConfig, mergeConfig } from 'vitest/config';
-
-export default mergeConfig(
- essentialConfig,
- defineConfig({
- test: {
- coverage: {
- provider: 'v8',
- thresholds: {
- branches: 100,
- functions: 100,
- lines: 100,
- statements: 100,
- },
- },
- },
- }),
-);
diff --git a/packages/preset-strict/source/vitest.override.ts b/packages/preset-strict/source/vitest.override.ts
new file mode 100644
index 00000000..c1dece74
--- /dev/null
+++ b/packages/preset-strict/source/vitest.override.ts
@@ -0,0 +1,17 @@
+/* v8 ignore start */
+
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ coverage: {
+ provider: 'v8',
+ thresholds: {
+ branches: 100,
+ functions: 100,
+ lines: 100,
+ statements: 100,
+ },
+ },
+ },
+});
diff --git a/packages/preset-strict/spec/index.spec.ts b/packages/preset-strict/spec/index.spec.ts
index 774f5a03..75194295 100644
--- a/packages/preset-strict/spec/index.spec.ts
+++ b/packages/preset-strict/spec/index.spec.ts
@@ -1,38 +1,43 @@
import { existsSync, readdirSync } from 'node:fs';
import { resolve } from 'node:path';
-import { loadDynamicMap, resolveContext } from 'presetter';
+import { listAssetNames, resolveAssets, resolvePreset } from 'presetter';
import { describe, expect, it, vi } from 'vitest';
-import getPresetAsset from '#';
+import preset, { DEFAULT_VARIABLES as variables } from '#';
+
+import type { PresetContext } from 'presetter-types';
vi.mock('node:path', { spy: true });
-describe('fn:getPresetAsset', () => {
- it('use all templates', async () => {
- const asset = await getPresetAsset();
- const context = await resolveContext({
- graph: [{ name: 'preset', asset, nodes: [] }],
- context: {
- target: { name: 'preset', root: '/', package: {} },
- custom: { preset: 'preset' },
- },
- });
-
- // load all potential dynamic content
- await loadDynamicMap(asset.supplementaryConfig, context);
- await loadDynamicMap(asset.template, context);
-
- const CONFIGS = resolve(import.meta.dirname, '..', 'configs');
- const configs = existsSync(CONFIGS) ? readdirSync(CONFIGS) : [];
- const TEMPLATES = resolve(import.meta.dirname, '..', 'templates');
+const OVERRIDES = resolve(import.meta.dirname, '..', 'overrides');
+const TEMPLATES = resolve(import.meta.dirname, '..', 'templates');
+
+const context = {
+ root: '/path/to/project',
+ package: {},
+} satisfies PresetContext;
+
+describe('fn:preset', () => {
+ it('should use all templates', async () => {
+ const node = await resolvePreset(preset, context);
+ listAssetNames(node, { ...context, variables });
+
+ const overrides = existsSync(OVERRIDES) ? readdirSync(OVERRIDES) : [];
const templates = existsSync(TEMPLATES) ? readdirSync(TEMPLATES) : [];
- for (const path of configs) {
- expect(vi.mocked(resolve)).toBeCalledWith(CONFIGS, path);
+ for (const path of overrides) {
+ expect(vi.mocked(resolve)).toHaveBeenCalledWith(OVERRIDES, path);
}
for (const path of templates) {
- expect(vi.mocked(resolve)).toBeCalledWith(TEMPLATES, path);
+ expect(vi.mocked(resolve)).toHaveBeenCalledWith(TEMPLATES, path);
}
});
+
+ it('should be able to resolve all assets', async () => {
+ const node = await resolvePreset(preset, context);
+ const result = resolveAssets(node, context);
+
+ await expect(result).resolves.not.toThrow();
+ });
});
diff --git a/packages/preset-strict/templates/eslint.config.ts b/packages/preset-strict/templates/eslint.config.ts
deleted file mode 100644
index f8917d66..00000000
--- a/packages/preset-strict/templates/eslint.config.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import presetConfig from 'presetter-preset-strict/eslint.config';
-
-export default [
- ...presetConfig,
- {
- ignores: ['{test}/**', 'types/**', 'generated/**', '{output}/**'],
- },
-];
diff --git a/packages/preset-strict/templates/vitest.config.ts b/packages/preset-strict/templates/vitest.config.ts
deleted file mode 100644
index e9834d1f..00000000
--- a/packages/preset-strict/templates/vitest.config.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from 'presetter-preset-strict/vitest.config';
diff --git a/packages/preset-web/README.md b/packages/preset-web/README.md
index ddf8dd7c..0d79925f 100644
--- a/packages/preset-web/README.md
+++ b/packages/preset-web/README.md
@@ -20,23 +20,37 @@
**presetter-preset-web** is an opinionated preset for you to setup some common tools for a web project in a fraction of time you usually take via [**presetter**](https://github.com/alvis/presetter)
-- 🕸️ GraphQL 15
- 💄 PostCSS 8
-- 💨 TailwindCSS 2
+- 💨 TailwindCSS 3
## Quick Start
[**FULL DOCUMENTATION IS AVAILABLE HERE**](https://github.com/alvis/presetter/blob/master/README.md)
-1. Bootstrap your project with `presetter-preset-esm` & `presetter-preset-web`
+### 1. Bootstrap your project with presetter-preset-web
-```shell
-npx presetter use presetter-preset presetter-preset-web
+On your project root, create a `presetter.config.ts` file with the following content:
+
+```typescript
+// presetter.config.ts
+
+import { preset } from 'presetter';
+import essentials from 'presetter-preset-essentials';
+import web from 'presetter-preset-web';
+
+export default preset('project name', {
+ // NOTE
+ // you may need an additional preset like presetter-preset-rollup for typescript support and other basic toolings
+ extends: [essentials, web],
+ override: {
+ // override the configuration here
+ },
+});
```
-That's. One command and you're set.
+Then, install your project as usual with `npm install` or any package manager you prefer.
-2. Develop and run life cycle scripts provided by the preset
+### 2. Develop and run life cycle scripts provided by the preset
At this point, all development packages specified in the preset are installed,
and now you can try to run some example life cycle scripts (e.g. run prepare).
@@ -45,15 +59,15 @@ and now you can try to run some example life cycle scripts (e.g. run prepare).
## Project Structure
-After installation, your project file structure should resemble the following or with more configuration files if you also installed other presets such as [`presetter-preset-esm`](https://github.com/alvis/presetter/blob/master/packages/preset-esm).
+After installation, your project file structure should resemble the following, or include more configuration files if you also installed other presets.
-**NOTE** You will notice there's no additional configuration file on your root folder like other presets such as [`presetter-preset-esm`](https://github.com/alvis/presetter/blob/master/packages/preset-esm).
+**NOTE** You will notice there's no additional configuration file on your root folder like other presets such as [`presetter-preset-essentials`](https://github.com/alvis/presetter/blob/master/packages/preset-essentials).
It's because `presetter-preset-web` is a bundle only preset, meaning it only helps you to install the development packages specified in this preset only.
```
(root)
├─ .git
- ├─ .presetterrc.json
+ ├─ presetter.config.ts
├─ node_modules
└─ package.json
```
@@ -62,13 +76,6 @@ It's because `presetter-preset-web` is a bundle only preset, meaning it only hel
As a bundle only preset, it offers no further customization.
-However, you are still required to specify the preset name in `.presetterrc` or `.presetterrc.json` as the interface below.
+However, you can further customize (either extending or replacing) the configuration by specifying the changes in the config file `presetter.config.ts`.
-**NOTE**: You may want to use other presets together with `presetter-preset-web` to setup your project, specify the presets in `.presetterrc` or `.presetterrc.json` as well and checkout their available customization.
-
-```ts
-interface PresetterRC {
- /** name(s) of the preset e.g. "presetter-preset-web" or ["presetter-preset-esm", "presetter-preset-web", "presetter-preset-react"] */
- name: string | string[];
-}
-```
+**NOTE**: You may want to use other presets together with `presetter-preset-web` to setup your project.
diff --git a/packages/preset-web/configs/tsconfig.yaml b/packages/preset-web/overrides/tsconfig.yaml
similarity index 100%
rename from packages/preset-web/configs/tsconfig.yaml
rename to packages/preset-web/overrides/tsconfig.yaml
diff --git a/packages/preset-web/package.json b/packages/preset-web/package.json
index 63cc7d22..7fd552ac 100644
--- a/packages/preset-web/package.json
+++ b/packages/preset-web/package.json
@@ -19,10 +19,6 @@
".": {
"default": "./lib/index.js",
"types": "./lib/index.d.ts"
- },
- "./eslint.config": {
- "default": "./lib/eslint.config.js",
- "types": "./lib/eslint.config.d.ts"
}
},
"repository": {
diff --git a/packages/preset-web/source/eslint.config.ts b/packages/preset-web/source/eslint.template.ts
similarity index 81%
rename from packages/preset-web/source/eslint.config.ts
rename to packages/preset-web/source/eslint.template.ts
index 0b3bb862..69003c73 100644
--- a/packages/preset-web/source/eslint.config.ts
+++ b/packages/preset-web/source/eslint.template.ts
@@ -1,6 +1,6 @@
-import globals from 'globals';
+/* v8 ignore start */
-import essentialConfig from 'presetter-preset-essentials/eslint.config';
+import globals from 'globals';
import tailwind from 'eslint-plugin-tailwindcss';
import testing from 'eslint-plugin-testing-library';
@@ -8,7 +8,6 @@ import testing from 'eslint-plugin-testing-library';
import type { Linter } from 'eslint';
export default [
- ...essentialConfig,
...tailwind.configs['flat/recommended'],
testing.configs['flat/dom'],
{
diff --git a/packages/preset-web/source/index.ts b/packages/preset-web/source/index.ts
index d6f0e816..aa9ff633 100644
--- a/packages/preset-web/source/index.ts
+++ b/packages/preset-web/source/index.ts
@@ -1,26 +1,27 @@
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
-import type { PresetAsset } from 'presetter-types';
+import { preset } from 'presetter-types';
+
+import * as eslint from './eslint.template';
const DIR = fileURLToPath(dirname(import.meta.url));
// paths to the template directories
-const CONFIGS = resolve(DIR, '..', 'configs');
-const TEMPLATES = resolve(DIR, '..', 'templates');
+const OVERRIDES = resolve(DIR, '..', 'overrides');
-/**
- * get the list of templates provided by this preset
- * @returns list of preset templates
- */
-export default async function (): Promise {
- return {
- extends: ['presetter-preset-esm'],
- supplementaryConfig: {
- tsconfig: resolve(CONFIGS, 'tsconfig.yaml'),
- },
- template: {
- 'eslint.config.ts': resolve(TEMPLATES, 'eslint.config.ts'),
+/** list of configurable variables */
+export interface Variables {}
+
+export const DEFAULT_VARIABLES = {} satisfies Variables;
+
+export default preset('presetter-preset-web', {
+ assets: {
+ 'eslint.config.ts': eslint,
+ },
+ override: {
+ assets: {
+ '.tsconfig.json': resolve(OVERRIDES, 'tsconfig.yaml'),
},
- };
-}
+ },
+});
diff --git a/packages/preset-web/spec/index.spec.ts b/packages/preset-web/spec/index.spec.ts
index 774f5a03..75194295 100644
--- a/packages/preset-web/spec/index.spec.ts
+++ b/packages/preset-web/spec/index.spec.ts
@@ -1,38 +1,43 @@
import { existsSync, readdirSync } from 'node:fs';
import { resolve } from 'node:path';
-import { loadDynamicMap, resolveContext } from 'presetter';
+import { listAssetNames, resolveAssets, resolvePreset } from 'presetter';
import { describe, expect, it, vi } from 'vitest';
-import getPresetAsset from '#';
+import preset, { DEFAULT_VARIABLES as variables } from '#';
+
+import type { PresetContext } from 'presetter-types';
vi.mock('node:path', { spy: true });
-describe('fn:getPresetAsset', () => {
- it('use all templates', async () => {
- const asset = await getPresetAsset();
- const context = await resolveContext({
- graph: [{ name: 'preset', asset, nodes: [] }],
- context: {
- target: { name: 'preset', root: '/', package: {} },
- custom: { preset: 'preset' },
- },
- });
-
- // load all potential dynamic content
- await loadDynamicMap(asset.supplementaryConfig, context);
- await loadDynamicMap(asset.template, context);
-
- const CONFIGS = resolve(import.meta.dirname, '..', 'configs');
- const configs = existsSync(CONFIGS) ? readdirSync(CONFIGS) : [];
- const TEMPLATES = resolve(import.meta.dirname, '..', 'templates');
+const OVERRIDES = resolve(import.meta.dirname, '..', 'overrides');
+const TEMPLATES = resolve(import.meta.dirname, '..', 'templates');
+
+const context = {
+ root: '/path/to/project',
+ package: {},
+} satisfies PresetContext;
+
+describe('fn:preset', () => {
+ it('should use all templates', async () => {
+ const node = await resolvePreset(preset, context);
+ listAssetNames(node, { ...context, variables });
+
+ const overrides = existsSync(OVERRIDES) ? readdirSync(OVERRIDES) : [];
const templates = existsSync(TEMPLATES) ? readdirSync(TEMPLATES) : [];
- for (const path of configs) {
- expect(vi.mocked(resolve)).toBeCalledWith(CONFIGS, path);
+ for (const path of overrides) {
+ expect(vi.mocked(resolve)).toHaveBeenCalledWith(OVERRIDES, path);
}
for (const path of templates) {
- expect(vi.mocked(resolve)).toBeCalledWith(TEMPLATES, path);
+ expect(vi.mocked(resolve)).toHaveBeenCalledWith(TEMPLATES, path);
}
});
+
+ it('should be able to resolve all assets', async () => {
+ const node = await resolvePreset(preset, context);
+ const result = resolveAssets(node, context);
+
+ await expect(result).resolves.not.toThrow();
+ });
});
diff --git a/packages/preset-web/templates/eslint.config.ts b/packages/preset-web/templates/eslint.config.ts
deleted file mode 100644
index 61d4935a..00000000
--- a/packages/preset-web/templates/eslint.config.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import presetConfig from 'presetter-preset-web/eslint.config';
-
-export default [
- ...presetConfig,
- {
- ignores: ['{test}/**', 'types/**', 'generated/**', '{output}/**'],
- },
-];
diff --git a/packages/presetter/dependency_graph.svg b/packages/presetter/dependency_graph.svg
new file mode 100644
index 00000000..02f2b491
--- /dev/null
+++ b/packages/presetter/dependency_graph.svg
@@ -0,0 +1,619 @@
+
+
+
+
+
diff --git a/packages/presetter/package.json b/packages/presetter/package.json
index b86ff9bb..99b258ec 100644
--- a/packages/presetter/package.json
+++ b/packages/presetter/package.json
@@ -34,8 +34,7 @@
"url": "git+https://github.com/alvis/presetter.git"
},
"scripts": {
- "echo": "echo $npm_package_name",
- "prepare": "tsx --eval \"import('./source/preset/setup.ts').then(({bootstrapPreset})=>bootstrapPreset())\" && npm run build",
+ "prepare": "tsx --eval \"import('./source/preset/bootstrap.ts').then(({ bootstrap }) => bootstrap())\" && npm run build",
"build": "npm run script -- build",
"clean": "npm run script -- clean",
"coverage": "npm run script -- coverage --",
@@ -64,6 +63,7 @@
"chalk": "^5.3.0",
"debug": "^4.3.5",
"execa": "^9.3.0",
+ "jiti": "^2.4.0",
"js-yaml": "^4.1.0",
"listr2": "^8.2.0",
"mvdan-sh": "^0.10.0",
@@ -75,6 +75,7 @@
"source-map-support": "^0.5.0",
"write-pkg": "^7.0.0",
"yargs": "^17.0.0",
- "yargs-parser": "^21.0.0"
+ "yargs-parser": "^21.0.0",
+ "xception": "^6.0.0"
}
}
diff --git a/packages/presetter/presetter.config.ts b/packages/presetter/presetter.config.ts
new file mode 100644
index 00000000..6a60d557
--- /dev/null
+++ b/packages/presetter/presetter.config.ts
@@ -0,0 +1,10 @@
+import { preset } from 'presetter-types';
+
+import monorepo from '../../presetter.config';
+
+export default preset('presetter', {
+ extends: [monorepo],
+ assets: {
+ '.gitignore': ['/generated'],
+ },
+});
diff --git a/packages/presetter/source/content.ts b/packages/presetter/source/content.ts
deleted file mode 100644
index 15b7a5ca..00000000
--- a/packages/presetter/source/content.ts
+++ /dev/null
@@ -1,389 +0,0 @@
-import debug from './debugger';
-import { getConfigKey, loadDynamic, loadDynamicMap } from './resolution';
-import { merge, mergeTemplate, substitute } from './template';
-import { filter } from './utilities';
-
-import type {
- Config,
- PresetContext,
- PresetGraph,
- PresetNode,
- PresetterConfig,
- ResolvedPresetContext,
- Template,
-} from 'presetter-types';
-
-/**
- * enrich the context with the resolved supplementary assets
- * @param _ collection of arguments
- * @param _.graph preset graph
- * @param _.context preset context
- * @returns enriched preset context
- */
-export async function resolveContext(_: {
- graph: PresetGraph;
- context: PresetContext;
-}): Promise {
- const { graph } = _;
-
- // compute a new context with variable resolved
- const context: ResolvedPresetContext<'variable'> = {
- ..._.context,
- custom: {
- ..._.context.custom,
- variable: resolveVariable({ graph, config: _.context.custom }),
- },
- };
-
- const config = await resolveSupplementaryConfig({ graph, context });
- const noSymlinks = await resolveNoSymlinks({ graph, context });
- const scripts = await resolveSupplementaryScripts({ graph, context });
-
- const custom = {
- ...context.custom,
- preset: context.custom.preset,
- config,
- noSymlinks,
- scripts,
- };
-
- debug('RESOLVED CONFIGURATION WITH PRESET\n%O', custom);
-
- // return a new context with everything resolved
- return {
- target: context.target,
- custom,
- };
-}
-
-/**
- * resolve no noSymlinks
- * @param _ collection of arguments
- * @param _.graph preset graph
- * @param _.context preset context
- * @returns list of noSymlinks
- */
-export async function resolveNoSymlinks(_: {
- graph: PresetGraph;
- context: ResolvedPresetContext<'variable'>;
-}): Promise {
- const { graph, context } = _;
-
- const fromPreset = (
- await Promise.all(
- graph.map(async (node) => resolveNoSymlinksFromNode({ node, context })),
- )
- ).flat();
- const fromUser = context.custom.noSymlinks ?? [];
-
- return [...new Set([...fromPreset, ...fromUser])];
-}
-
-/**
- * resolve noSymlinks from a preset node
- * @param _ collection of arguments
- * @param _.node preset node
- * @param _.context resolved preset context
- * @returns list of noSymlinks
- */
-async function resolveNoSymlinksFromNode(_: {
- node: PresetNode;
- context: ResolvedPresetContext<'variable'>;
-}): Promise {
- const { node, context } = _;
- const { asset, nodes } = node;
-
- // resolve noSymlink lists from preset's extensions
- const fromChildren = (
- await Promise.all(
- nodes.map(async (extension) =>
- resolveNoSymlinksFromNode({ node: extension, context }),
- ),
- )
- ).flat();
-
- // resolve preset's noSymlink list
- const fromPreset = await loadDynamic(asset.noSymlinks ?? [], context);
-
- return [...new Set([...fromChildren, ...fromPreset])];
-}
-
-/**
- * compute the final config map
- * @param _ collection of arguments
- * @param _.graph preset graph
- * @param _.context preset context
- * @returns map of config content
- */
-export async function resolveSupplementaryConfig(_: {
- graph: PresetGraph;
- context: ResolvedPresetContext<'variable'>;
-}): Promise> {
- const { graph, context } = _;
-
- const fromPresets = (
- await Promise.all(
- graph.map(async (node) =>
- resolveSupplementaryConfigFromNode({ node, context }),
- ),
- )
- ).reduce((merged, next) => merge(merged, next), {});
-
- return merge(fromPresets, context.custom.config);
-}
-
-/**
- * compute the supplementary config map from a preset node
- * @param _ collection of arguments
- * @param _.node preset node
- * @param _.context preset context
- * @returns map of config content
- */
-export async function resolveSupplementaryConfigFromNode(_: {
- node: PresetNode;
- context: ResolvedPresetContext<'variable'>;
-}): Promise> {
- const { node, context } = _;
- const { asset, nodes } = node;
-
- // resolve configs from the preset's extensions
- const fromChildren = (
- await Promise.all(
- nodes.map(async (node) =>
- resolveSupplementaryConfigFromNode({ node, context }),
- ),
- )
- ).reduce((merged, next) => merge(merged, next), {});
-
- // resolve preset's config
- const fromPreset = await loadDynamicMap<'supplementaryConfig'>(
- asset.supplementaryConfig,
- context,
- );
-
- // merge preset's config on top of the extensions
- return merge(fromChildren, fromPreset);
-}
-
-/**
- * compute script that will be merged with those provided by presets
- * @param _ collection of arguments
- * @param _.graph preset graph
- * @param _.context preset context
- * @returns map of config content
- */
-export async function resolveSupplementaryScripts(_: {
- graph: PresetGraph;
- context: ResolvedPresetContext<'variable'>;
-}): Promise> {
- const { graph, context } = _;
-
- const fromPresets = (
- await Promise.all(
- graph.map(async (node) =>
- resolveSupplementaryScriptsFromNode({ node, context }),
- ),
- )
- ).reduce((merged, next) => merge(merged, next), {});
-
- return merge(fromPresets, context.custom.scripts);
-}
-
-/**
- * compute the supplementary config map from a preset node
- * @param _ collection of arguments
- * @param _.node preset node
- * @param _.context preset context
- * @returns map of config content
- */
-export async function resolveSupplementaryScriptsFromNode(_: {
- node: PresetNode;
- context: ResolvedPresetContext<'variable'>;
-}): Promise> {
- const { node, context } = _;
- const { asset, nodes } = node;
- const { supplementaryScripts } = asset;
-
- // resolve configs from the preset's extensions
- const fromChildren = (
- await Promise.all(
- nodes.map(async (node) =>
- resolveSupplementaryScriptsFromNode({ node, context }),
- ),
- )
- ).reduce((merged, next) => merge(merged, next), {});
-
- // resolve preset's config
- const fromPreset = await loadDynamic(supplementaryScripts ?? {}, context);
-
- // merge preset's config on top of the extensions
- return merge(fromChildren, fromPreset);
-}
-
-/**
- * combine default variables from presets with custom variables
- * @param _ collection of arguments
- * @param _.graph preset graph
- * @param _.config user config
- * @returns combined variables
- */
-export function resolveVariable(_: {
- graph: PresetGraph;
- config: PresetterConfig;
-}): Record {
- const { graph, config } = _;
-
- // get the default from presets
- const fromPresets = graph
- .map((node) => resolveVariableFromNode({ node }))
- .reduce((merged, next) => merge(merged, next), {});
-
- // merge with those from the config file
- return merge(fromPresets, config.variable);
-}
-
-/**
- * resolve variables from a preset node
- * @param _ collection of arguments
- * @param _.node preset node
- * @returns combined variables
- */
-function resolveVariableFromNode(_: {
- node: PresetNode;
-}): Record {
- const { node } = _;
- const { asset, nodes } = node;
-
- // resolve variables from the preset's extensions
- const fromChildren = nodes
- .map((node) => resolveVariableFromNode({ node }))
- .reduce((merged, next) => merge(merged, next), {});
-
- // merge with the preset's default variables
- return merge(fromChildren, asset.variable);
-}
-
-/**
- * compute the final script map
- * @param _ collection of arguments
- * @param _.graph preset graph
- * @param _.context preset context
- * @returns map of script content
- */
-export async function resolveScripts(_: {
- graph: PresetGraph;
- context: ResolvedPresetContext<'variable'>;
-}): Promise> {
- const { graph, context } = _;
-
- // resolve scripts from all presets
- const fromPresets = (
- await Promise.all(
- graph.map(async (node) => resolveScriptsFromNode({ node, context })),
- )
- ).reduce((merged, next) => merge(merged, next), {});
-
- const fromConfig = context.custom.scripts;
-
- return substitute(merge(fromPresets, fromConfig), context.custom.variable);
-}
-
-/**
- * compute the final script map from a preset node
- * @param _ collection of arguments
- * @param _.node preset node
- * @param _.context preset context
- * @returns map of script content
- */
-export async function resolveScriptsFromNode(_: {
- node: PresetNode;
- context: ResolvedPresetContext<'variable'>;
-}): Promise> {
- const { node, context } = _;
- const { asset, nodes } = node;
-
- // resolve scripts from the preset's extensions
- const fromChildren = (
- await Promise.all(
- nodes.map(async (node) => resolveScriptsFromNode({ node, context })),
- )
- ).reduce((merged, next) => merge(merged, next), {});
-
- // resolve preset's scripts
- const fromPreset = await loadDynamic(asset.scripts ?? {}, context);
-
- // merge preset's scripts on top of the extensions
- return merge(fromChildren, fromPreset);
-}
-
-/**
- * compute the final template content
- * @param _ collection of arguments
- * @param _.graph preset graph
- * @param _.context preset context
- * @returns map of template content
- */
-export async function resolveTemplate(_: {
- graph: PresetGraph;
- context: ResolvedPresetContext;
-}): Promise> {
- const { graph, context } = _;
-
- // deduce all the template contents and their paths from presets
- const fromPreset = (
- await Promise.all(
- graph.map(async (node) => resolveTemplateFromNode({ node, context })),
- )
- ).reduce((merged, next) => mergeTemplate(merged, next), {});
-
- // merge the template with the config supplied by user
- const customTemplate = Object.fromEntries(
- Object.entries(fromPreset).map(([path, current]) => {
- const config = context.custom.config[getConfigKey(path)] as Partial<
- typeof context.custom.config
- >[string];
- const candidate = Array.isArray(config) ? config.join('\n') : config;
-
- return [path, candidate ?? current];
- }),
- );
- const merged = mergeTemplate(fromPreset, customTemplate);
-
- const resolvedTemplate = filter(merged, ...(context.custom.ignores ?? []));
-
- return substitute(resolvedTemplate, context.custom.variable);
-}
-
-/**
- * compute the final template content from a preset node
- * @param _ collection of arguments
- * @param _.node preset node
- * @param _.context preset context
- * @returns map of template content
- */
-export async function resolveTemplateFromNode(_: {
- node: PresetNode;
- context: ResolvedPresetContext;
-}): Promise> {
- const { node, context } = _;
- const { asset, nodes } = node;
- const { supplementaryIgnores } = asset;
-
- // resolve template from the preset's extensions
- const fromChildren = (
- await Promise.all(
- nodes.map(async (node) => resolveTemplateFromNode({ node, context })),
- )
- ).reduce((current, next) => mergeTemplate(current, next), {});
-
- const fromPreset = await loadDynamicMap<'template'>(asset.template, context);
-
- const merged = mergeTemplate(fromChildren, fromPreset);
-
- const ignoreRules =
- typeof supplementaryIgnores === 'function'
- ? await supplementaryIgnores(context)
- : supplementaryIgnores;
-
- return filter(merged, ...(ignoreRules ?? []));
-}
diff --git a/packages/presetter/source/directive.ts b/packages/presetter/source/directive.ts
deleted file mode 100644
index 7187aa89..00000000
--- a/packages/presetter/source/directive.ts
+++ /dev/null
@@ -1,240 +0,0 @@
-import type { PresetContext } from 'presetter-types';
-
-/** syntax for an apply directive */
-export type ApplyDirective = `@apply ${string}` | `@apply ${string}[${string}]`;
-
-/** syntax for an import directive */
-export type ImportDirective =
- | `@import ${string}`
- | `@import ${string}[${string}]`;
-
-/** context that pass down to all parsers */
-interface ConfigResolutionContext extends PresetContext {
- /** a set of packages to be used in the output */
- packages: Set;
-}
-
-/** syntax for an apply directive */
-const applyRegex =
- // eslint-disable-next-line sonarjs/slow-regex
- /^@apply ((\.+\/|@)?[a-zA-Z]+[a-zA-Z0-9_/.-]*\w+)(\[([a-zA-Z]+[a-zA-Z0-9_.]*)\])?$/;
-
-/** syntax for an import directive */
-const importRegex =
- // eslint-disable-next-line sonarjs/slow-regex
- /^@import ((\.+\/|@)?[a-zA-Z]+[a-zA-Z0-9_/.-]*\w+)(\[([a-zA-Z]+[a-zA-Z0-9_.]*)\])?$/;
-
-/**
- * indicate whether the given value is an apply directive
- * @param value value to be tested
- * @returns true if the value is an apply directive
- */
-export function isApplyDirective(value: unknown): value is ApplyDirective {
- return typeof value === 'string' && !!applyRegex.exec(value);
-}
-
-/**
- * indicate whether the given value is an import directive
- * @param value value to be tested
- * @returns true if the value is an import directive
- */
-export function isImportDirective(value: unknown): value is ImportDirective {
- return typeof value === 'string' && !!importRegex.exec(value);
-}
-
-/**
- * indicate whether the given value is a directive
- * @param value value to be tested
- * @returns true if the value is a directive
- */
-export function isDirective(
- value: unknown,
-): value is ApplyDirective | ImportDirective {
- return isApplyDirective(value) || isImportDirective(value);
-}
-
-/**
- * resolve directives from configuration options
- * @param config configuration options
- * @param context background context about the configuration
- * @returns resolved configuration options with directive replaced
- */
-export function resolveDirective(
- config: Record,
- context: PresetContext,
-): {
- importMap: Record;
- stringifiedConfig: string;
-} {
- const packages = new Set();
- const stringifiedConfig = stringifyConfigObject(config, {
- ...context,
- packages,
- });
-
- const importMap = Object.fromEntries(
- [...packages.values()].map((packageName, key) => [
- packageName,
- `import${key}`,
- ]),
- );
-
- return { importMap, stringifiedConfig };
-}
-
-/**
- * resolve an apply directive
- * @param directiveMeta a potential directive in the form of [directive, options]
- * @param context shared context passed from the upstream
- * @returns resolved string or null if no directive is found
- */
-function resolveApplyDirective(
- directiveMeta: unknown[],
- context: ConfigResolutionContext,
-): string | null {
- const [directive, ...args] = directiveMeta;
- const { packages } = context;
-
- if (typeof directive === 'string') {
- const match = applyRegex.exec(directive);
-
- if (match) {
- const [_, packageName, _prefix, _importBracket, importName] = match;
- const resolvedArgs = args.map((arg) => stringifyValue(arg, context));
- const argsExpression = `[${resolvedArgs.join(', ')}]`;
-
- const resolvedName = registerUsage(packages, packageName, importName);
-
- return `${resolvedName}(...(${argsExpression} as const))`;
- }
- }
-
- // return null for a non-match
- return null;
-}
-
-/**
- * resolve an import directive
- * @param directive a potential directive string
- * @param context shared context passed from the upstream
- * @returns resolved string or null if no directive is found
- */
-function resolveImportDirective(
- directive: string,
- context: ConfigResolutionContext,
-): string | null {
- const { packages } = context;
- const match = importRegex.exec(directive);
-
- if (match) {
- const [_, packageName, _prefix, _importBracket, importName] = match;
-
- return registerUsage(packages, packageName, importName);
- }
-
- // return null for a non-match
- return null;
-}
-
-/**
- * stringify a value which has a type object
- * @param value the value to be stringified
- * @param context shared context passed from the upstream
- * @returns stringified value
- */
-function stringifyObjectValue(
- value: Record | any[] | null,
- context: ConfigResolutionContext,
-): string {
- if (Array.isArray(value)) {
- return (
- resolveApplyDirective(value, context) ??
- stringifyConfigArray(value, context)
- );
- } else if (value !== null) {
- return stringifyConfigObject(value, context);
- } else {
- return 'null';
- }
-}
-
-/**
- * stringify an array
- * @param config an object to be parsed
- * @param context shared context passed from the upstream
- * @returns stringified object
- */
-function stringifyConfigArray(
- config: unknown[],
- context: ConfigResolutionContext,
-): string {
- const values: string[] = config.map((value) =>
- stringifyValue(value, context),
- );
-
- return `[${values.join(', ')}]`;
-}
-
-/**
- * stringify an object
- * @param config an object to be parsed
- * @param context shared context passed from the upstream
- * @returns stringified object
- */
-function stringifyConfigObject(
- config: Record,
- context: ConfigResolutionContext,
-): string {
- const values: string[] = Object.entries(config).map(([key, value]) => {
- return `${JSON.stringify(key)}: ${stringifyValue(value, context)}`;
- });
-
- return `{${values.join(', ')}}`;
-}
-
-/**
- * stringify a value
- * @param value the value to be stringified
- * @param context shared context passed from the upstream
- * @returns stringified value
- */
-function stringifyValue(
- value: unknown,
- context: ConfigResolutionContext,
-): string {
- switch (typeof value) {
- case 'string':
- return resolveImportDirective(value, context) ?? JSON.stringify(value);
- case 'object':
- return stringifyObjectValue(value, context);
- case 'boolean':
- case 'number':
- default:
- return JSON.stringify(value);
- }
-}
-
-/**
- * add the needed package name to the registry and return its resolved name
- * @param packages package registry
- * @param packageName name of the package to be imported
- * @param importName named import to be used
- * @returns resolved symbol
- */
-function registerUsage(
- packages: Set,
- packageName: string,
- importName?: string,
-): string {
- // add package and import to the register
- packages.add(packageName);
-
- // replace the directive with the value path
- const packageKey = [...packages.values()].findIndex(
- (name) => name === packageName,
- );
-
- const named = importName ? `.${importName}` : '';
-
- return `import${packageKey}${named}`;
-}
diff --git a/packages/presetter/source/error.ts b/packages/presetter/source/error.ts
deleted file mode 100644
index c3d4697e..00000000
--- a/packages/presetter/source/error.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * prepend a rejected error message with a better explanation
- * @param promise a promise to be resolved
- * @param message a custom message to be prepended to any rejected message
- * @returns wrapped error
- */
-export async function wrap(
- promise: Promise,
- message: string,
-): Promise {
- try {
- return await promise;
- } catch (error) {
- if (error instanceof Error) {
- error.message = `${message}: ${error.message}`;
- }
-
- throw error;
- }
-}
diff --git a/packages/presetter/source/executable/entry.ts b/packages/presetter/source/executable/entry.ts
index 696dabea..9a9e3291 100644
--- a/packages/presetter/source/executable/entry.ts
+++ b/packages/presetter/source/executable/entry.ts
@@ -2,26 +2,12 @@ import { existsSync } from 'node:fs';
import yargs from 'yargs';
-import { bootstrapPreset, setupPreset, unsetPreset } from '../preset';
+import { bootstrap } from '../preset';
import { run } from '../run';
import { parseGlobalArgs, parseTaskSpec } from '../task';
import type { CommandModule } from 'yargs';
-const useCommand: CommandModule, { preset: string }> = {
- command: 'use ',
- describe: 'adopt the specified preset to the project',
- builder: (yargs) =>
- yargs
- .positional('preset', {
- required: true as const,
- type: 'string',
- description: 'the name of the preset to be used',
- })
- .help(),
- handler: async (argv) => setupPreset(...argv.preset),
-};
-
const bootstrapCommand: CommandModule<
Record,
{ force?: boolean; only?: string }
@@ -30,22 +16,17 @@ const bootstrapCommand: CommandModule<
describe: 'setup the project according to the specified preset',
builder: (yargs) =>
yargs
- .option('force', {
- type: 'boolean',
- default: false,
- description: 'overwrite existing files',
- })
.option('only', {
type: 'string',
description: 'proceed only if the specified file exists',
})
.help(),
handler: async (argv) => {
- const { force, only } = argv;
+ const { only } = argv;
// only proceed if the specified file exists
if (!only || existsSync(only)) {
- await bootstrapPreset({ force });
+ await bootstrap();
}
},
};
@@ -104,12 +85,6 @@ const runPCommand: CommandModule = {
},
};
-const unsetCommand: CommandModule = {
- command: 'unset',
- describe: 'remove all artifacts created by the preset',
- handler: async () => unsetPreset(),
-};
-
/**
* provide a command line interface
* @param args command line arguments
@@ -124,12 +99,10 @@ export async function entry(args: string[]): Promise {
})
.usage('⚙ presetter: your preset configurator')
.showHelpOnFail(true)
- .command(useCommand)
.command(bootstrapCommand)
.command(runCommand)
.command(runSCommand)
.command(runPCommand)
- .command(unsetCommand)
.demandCommand()
.parse(args);
}
diff --git a/packages/presetter/source/index.ts b/packages/presetter/source/index.ts
index 0aba8ac4..b1e2a279 100644
--- a/packages/presetter/source/index.ts
+++ b/packages/presetter/source/index.ts
@@ -2,9 +2,7 @@
export * from 'presetter-types';
-export * from './content';
-export * from './directive';
-export * from './io';
-export * from './resolution';
+export * from './preset';
+export * from './resolve';
+export * from './run';
export * from './template';
-export * from './utilities';
diff --git a/packages/presetter/source/io.ts b/packages/presetter/source/io.ts
index f95dc984..efab1b40 100644
--- a/packages/presetter/source/io.ts
+++ b/packages/presetter/source/io.ts
@@ -1,225 +1,45 @@
-import { info } from 'node:console';
-import {
- linkSync,
- lstatSync,
- mkdirSync,
- readFileSync,
- readlinkSync,
- statSync,
- symlinkSync,
- unlinkSync,
- writeFileSync,
-} from 'node:fs';
-import { basename, dirname, extname, relative, resolve } from 'node:path';
+import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
+import { dirname, extname } from 'node:path';
-import { dump, load } from 'js-yaml';
+import { load } from 'js-yaml';
-import type { Template } from 'presetter-types';
+import { substitute } from './template';
-/** collection of options for file ios */
-export interface IOOptions {
- /** whether to overwrite existing files */
- force?: boolean;
-}
-
-// JSON format
-const INDENT = 2;
-
-/**
- * check if a path is either a link or a file with multiple hard links
- * @param path path to be checked
- * @returns true if the path is a link or a file with multiple hard links
- */
-export function islink(path: string): boolean {
- const pathLinkStat = lstatSync(path, { throwIfNoEntry: false });
- const pathStat = statSync(path, { throwIfNoEntry: false });
-
- return (
- !!pathLinkStat?.isSymbolicLink() ||
- (!!pathStat?.nlink && pathStat.nlink > 1)
- );
-}
+import type { Variables } from 'presetter-types';
+import type { JsonObject } from 'type-fest';
/**
* load the content of a file
* @param path file path to be loaded
- * @param defaultFormat default format of the file
+ * @param variables variables to be used during the resolution
* @returns content of the file
*/
export function loadFile(
path: string,
- defaultFormat?: 'json' | 'yaml',
-): Record;
-export function loadFile(path: string, defaultFormat?: 'text'): string;
-export function loadFile(
- path: string,
- defaultFormat: 'json' | 'yaml' | 'text' = 'text',
-): string | Record {
- const content = readFileSync(path).toString();
-
- // parse the content depending on the extension
- switch (extname(path) || `.${defaultFormat}`) {
- case '.json':
- return JSON.parse(content) as Record;
- case '.yaml':
- case '.yml':
- return load(content) as Record;
- default:
- return content;
+ variables: Variables = {},
+): JsonObject | string[] | Buffer {
+ if (!existsSync(path)) {
+ throw new Error(`file not found: ${path}`);
}
-}
-/**
- * serialize a configuration content to the right format according to its destination
- * @param destination the path to which the content will be written
- * @param content configuration content
- * @returns serialized content
- */
-export function serializeContent(
- destination: string,
- content: Template,
-): string {
- if (typeof content === 'string') {
- return content;
- }
+ const content = readFileSync(path);
- switch (extname(destination)) {
+ // parse the content depending on the extension
+ switch (extname(path)) {
+ case '.json':
+ return JSON.parse(
+ substitute(content.toString(), variables),
+ ) as JsonObject;
case '.yaml':
case '.yml':
- return dump(content);
- case '.json':
+ return load(substitute(content.toString(), variables)) as JsonObject;
default:
- return JSON.stringify(content, null, INDENT);
- }
-}
-
-/**
- * write all generated configuration to their destination
- * @param root path to the target project root
- * @param config a map of configuration content and its path to be written
- * @param pathMap a map of keys in the config map and their destination path
- * @param options collection of options
- */
-export function writeFiles(
- root: string,
- config: Record,
- pathMap: Record,
- options?: IOOptions,
-): void {
- const { force = false } = { ...options };
-
- for (const [key, content] of Object.entries(config)) {
- const destination = pathMap[key];
-
- // write content to the destination path
- if (
- // force overwrite
- force ||
- // file don't exist
- !lstatSync(destination, { throwIfNoEntry: false }) ||
- // content to be written under the configurations folder
- destination !== resolve(root, key)
- ) {
- info(`Generating ${key}`);
-
- ensureFile(destination, serializeContent(destination, content));
- } else {
- info(`Skipping ${key}`);
- }
- }
-}
-
-/**
- * link generated files to the project root
- * @param root path to the target project root
- * @param configurationLink map of symlinks to its real path
- * @param options collection of options
- */
-export function linkFiles(
- root: string,
- configurationLink: Record,
- options?: IOOptions,
-): void {
- const { force = false } = { ...options };
-
- for (const [file, destination] of Object.entries(configurationLink)) {
- const path = resolve(root, file);
- const to = relative(dirname(path), destination);
-
- const pathStat = lstatSync(path, { throwIfNoEntry: false });
- const linkDoesNotExist = !pathStat;
-
- if (
- // for files that mean to be created directly on the target project root, not via symlink
- to !== basename(to) &&
- // do not replace any user created files unless overwrite is set
- (linkDoesNotExist || islink(path) || force)
- ) {
- info(`Linking ${relative(root, path)} => ${to}`);
- ensureLink(path, to);
- }
- }
-}
-
-/**
- * unlink generated files from the project root
- * @param root path to the target project root
- * @param configurationLink map of symlinks to its real path
- * @param options collection of options
- */
-export function unlinkFiles(
- root: string,
- configurationLink: Record,
- options?: IOOptions,
-): void {
- const { force = false } = { ...options };
-
- for (const [name, destination] of Object.entries(configurationLink)) {
- const link = readLink(resolve(root, name));
- const to = relative(root, destination);
-
- if (link === to || force) {
- info(`Removing ${name}`);
- unlinkSync(resolve(root, name));
- continue;
- }
-
- info(`Skipping ${name}`);
- }
-}
-
-/**
- * resolve a path and follow any symlink
- * @param path path to be resolved
- * @returns the target of the symlink or a resolved path if the path is not a symlink
- */
-function readLink(path: string): string | null {
- const pathStat = lstatSync(path, { throwIfNoEntry: false });
-
- return pathStat?.isSymbolicLink() ? readlinkSync(path) : null;
-}
-
-/**
- * ensure that there is a symlink at the given path pointing to the target
- * @param path path where the symlink should be created
- * @param to target of the symlink
- */
-function ensureLink(path: string, to: string): void {
- // create the parent directory if it doesn't exist
- mkdirSync(dirname(path), { recursive: true });
-
- // remove any existing files or symlinks
- if (lstatSync(path, { throwIfNoEntry: false })) {
- unlinkSync(path);
- }
-
- // create a new link pointing to the target
- try {
- // try to create a hardlink
- linkSync(to, path);
- } catch {
- // if hard link creation failed (e.g. linking across disc boundary) create a symlink instead
- symlinkSync(to, path);
+ return path.endsWith('ignore')
+ ? substitute(content.toString(), variables)
+ .split('\n')
+ .map((line) => line.trim())
+ .filter((line) => !line.startsWith('#'))
+ : content;
}
}
@@ -228,7 +48,7 @@ function ensureLink(path: string, to: string): void {
* @param path path where the file should be created
* @param content content of the file
*/
-function ensureFile(path: string, content: string): void {
+export function ensureFile(path: string, content: string | Buffer): void {
// ensure that all parent folders exist to avoid errors from writeFile
mkdirSync(dirname(path), { recursive: true });
diff --git a/packages/presetter/source/preset/bootstrap.ts b/packages/presetter/source/preset/bootstrap.ts
new file mode 100644
index 00000000..c1675fc6
--- /dev/null
+++ b/packages/presetter/source/preset/bootstrap.ts
@@ -0,0 +1,38 @@
+import { info } from 'node:console';
+import { resolve } from 'node:path';
+
+import { ensureFile } from '../io';
+import { arePeerPackagesAutoInstalled, reifyDependencies } from '../package';
+import { serialize } from '../serialization';
+
+import { getContext } from './context';
+import { resolveProjectPreset } from './project';
+import { resolveAssets } from './resolution';
+
+/**
+ * generate files from templates and place them to the target project root
+ */
+export async function bootstrap(): Promise {
+ const context = await getContext();
+
+ // install all related packages first
+ if (!arePeerPackagesAutoInstalled()) {
+ await reifyDependencies({ root: context.root });
+ }
+
+ // generate configurations
+ const node = await resolveProjectPreset(context);
+ const assets = await resolveAssets(node, context);
+
+ for (const [name, asset] of Object.entries(assets)) {
+ if (asset === null) {
+ info(`Skipping ${name}`);
+ } else {
+ const destination = resolve(context.root, name);
+
+ info(`Generating ${name}`);
+
+ ensureFile(destination, serialize(destination, asset));
+ }
+ }
+}
diff --git a/packages/presetter/source/preset/config/index.ts b/packages/presetter/source/preset/config/index.ts
new file mode 100644
index 00000000..7352cf7f
--- /dev/null
+++ b/packages/presetter/source/preset/config/index.ts
@@ -0,0 +1,4 @@
+/* v8 ignore start */
+
+export * from './resolve';
+export * from './search';
diff --git a/packages/presetter/source/preset/config/resolve.ts b/packages/presetter/source/preset/config/resolve.ts
new file mode 100644
index 00000000..674de1cd
--- /dev/null
+++ b/packages/presetter/source/preset/config/resolve.ts
@@ -0,0 +1,33 @@
+import { createJiti } from 'jiti';
+
+import debug from '../../debugger';
+
+import { searchPresetterConfigs } from './search';
+
+import type { Preset } from 'presetter-types';
+
+/**
+ * resolve the presetter configuration from the project root
+ * @param root the root directory containing the project's package.json
+ * @returns the resolved presetter configuration
+ */
+export async function resolvePresetterConfig(root: string): Promise {
+ const configPaths = await searchPresetterConfigs(root);
+
+ if (configPaths.length === 0) {
+ throw new Error('no presetter configuration file found');
+ }
+
+ // NOTE: the priority is given to the configuration file closest to the project root, from mts, ts, mjs then js
+ const closestConfigPath = configPaths[0];
+
+ debug(`loading presetter configuration from ${closestConfigPath}`);
+ const jiti = createJiti(root, {
+ debug: !!process.env.DEBUG?.includes('presetter'),
+ });
+
+ // get the preset
+ return jiti.import(closestConfigPath, {
+ default: true,
+ });
+}
diff --git a/packages/presetter/source/preset/config/search.ts b/packages/presetter/source/preset/config/search.ts
new file mode 100644
index 00000000..5f76207f
--- /dev/null
+++ b/packages/presetter/source/preset/config/search.ts
@@ -0,0 +1,33 @@
+import { existsSync } from 'node:fs';
+import { dirname, resolve } from 'node:path';
+
+import { readPackageUp } from 'read-pkg-up';
+
+import debug from '../../debugger';
+
+/** presetter configuration filename */
+const PRESETTER_CONFIG = 'presetter.config';
+
+/**
+ * get all potential presetter configuration files by searching from the current base up to the monorepo root, if there is one
+ * @param base the base directory to start searching for the configuration file
+ * @returns list of presetter configuration files
+ */
+export async function searchPresetterConfigs(base: string): Promise {
+ debug(`searching for presetter configuration files from ${base}`);
+
+ const filesFromBase = ['.mts', '.ts', '.mjs', '.js']
+ .map((ext) => resolve(base, `${PRESETTER_CONFIG}${ext}`))
+ .filter(existsSync);
+
+ debug(`found ${filesFromBase.length} configuration files from ${base}`);
+
+ const parent = await readPackageUp({ cwd: dirname(base) });
+
+ // if the base is the root of a monorepo, stop searching
+ const filesFromParent = parent?.path
+ ? await searchPresetterConfigs(dirname(base))
+ : [];
+
+ return [...filesFromBase, ...filesFromParent];
+}
diff --git a/packages/presetter/source/preset/content.ts b/packages/presetter/source/preset/content.ts
deleted file mode 100644
index facfa6bc..00000000
--- a/packages/presetter/source/preset/content.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { resolveContext, resolveTemplate } from '../content';
-import { linkFiles, writeFiles } from '../io';
-
-import { getPresetGraph } from './graph';
-import { getDestinationMap } from './mapping';
-
-import type { PresetContext } from 'presetter-types';
-
-/** collection of options for bootstrapping */
-interface BootstrapOptions {
- /** whether to skip all checks */
- force?: boolean;
-}
-
-/**
- * generate files from templates and link them to the target project root
- * @param context context about the target project and any customization in .presetterrc
- * @param options collection of options
- */
-export async function bootstrapContent(
- context: PresetContext,
- options?: BootstrapOptions,
-): Promise {
- const graph = await getPresetGraph(context);
-
- const resolvedContext = await resolveContext({ graph, context });
- const content = await resolveTemplate({ graph, context: resolvedContext });
-
- const destinationMap = await getDestinationMap(content, resolvedContext);
-
- writeFiles(context.target.root, content, destinationMap, options);
- linkFiles(context.target.root, destinationMap, options);
-}
diff --git a/packages/presetter/source/preset/context.ts b/packages/presetter/source/preset/context.ts
index 66c261b3..0db64ce8 100644
--- a/packages/presetter/source/preset/context.ts
+++ b/packages/presetter/source/preset/context.ts
@@ -2,22 +2,16 @@ import { dirname } from 'node:path';
import { getPackage } from '../package';
-import { getPresetterRC } from './presetterRC';
-
import type { PresetContext } from 'presetter-types';
/**
* get context about the target project and any customization in .presetterrc
+ * @param cwd the current working directory
* @returns context about the target project and any customization in .presetterrc
*/
-export async function getContext(): Promise {
- const { json, path } = await getPackage();
+export async function getContext(cwd?: string): Promise {
+ const { json, path } = await getPackage(cwd);
const root = dirname(path);
- const target = { name: json.name!, root, package: json };
- const custom = await getPresetterRC(root);
- return {
- target,
- custom,
- };
+ return { root, package: json };
}
diff --git a/packages/presetter/source/preset/graph.ts b/packages/presetter/source/preset/graph.ts
deleted file mode 100644
index 7ef249b9..00000000
--- a/packages/presetter/source/preset/graph.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import type {
- PresetAsset,
- PresetContext,
- PresetGraph,
- PresetNode,
-} from 'presetter-types';
-
-/**
- * get assets from a preset
- * @param name name of the preset
- * @param context context about the target project and customization in .presetterrc
- * @returns assets from the preset
- */
-export async function getPresetAsset(
- name: string,
- context: PresetContext,
-): Promise {
- try {
- // get the preset
- const { default: getPresetAsset } = (await import(name)) as {
- default: (args: PresetContext) => Promise;
- };
-
- return await getPresetAsset(context);
- } catch {
- throw new Error(`cannot resolve preset ${name}`);
- }
-}
-
-/**
- * compute a graph of presets
- * @param context context about the target project and customization in .presetterrc
- * @returns resolved preset graph
- */
-export async function getPresetGraph(
- context: PresetContext,
-): Promise {
- // get the preset name
- const { preset } = context.custom;
-
- const presets = Array.isArray(preset) ? preset : [preset];
-
- return Promise.all(presets.map(async (name) => getPresetNode(name, context)));
-}
-
-/**
- * resolve a preset as a node
- * @param name name of the preset
- * @param context context about the target project and customization in .presetterrc
- * @returns resolved preset node
- */
-export async function getPresetNode(
- name: string,
- context: PresetContext,
-): Promise {
- const asset = await getPresetAsset(name, context);
- const nodes = await Promise.all(
- (asset.extends ?? []).map(async (extension) =>
- getPresetNode(extension, context),
- ),
- );
-
- return { name, asset, nodes };
-}
diff --git a/packages/presetter/source/preset/index.ts b/packages/presetter/source/preset/index.ts
index dacf9154..3ff2b882 100644
--- a/packages/presetter/source/preset/index.ts
+++ b/packages/presetter/source/preset/index.ts
@@ -6,11 +6,13 @@
//
// for template resolution
// STEP 3 resolve template content from resolved variables
-// STEP 4 resolve noSimlinks from resolved variables
//
// for script resolution
// STEP 3 resolve script content from resolved variables
+export * from './bootstrap';
+export * from './config';
+export * from './context';
+export * from './project';
+export * from './resolution';
export * from './scripts';
-export * from './setup';
-export * from './unset';
diff --git a/packages/presetter/source/preset/mapping.ts b/packages/presetter/source/preset/mapping.ts
deleted file mode 100644
index ed19fd39..00000000
--- a/packages/presetter/source/preset/mapping.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { resolve } from 'node:path';
-
-import resolvePackage from 'resolve-pkg';
-
-import debug from '../debugger';
-
-import type { ResolvedPresetContext, Template } from 'presetter-types';
-
-/**
- * compute the output paths of all configuration files to be generated
- * @param template resolved template map
- * @param context resolved context about the target project and customization
- * @returns mapping of configuration symlinks to its real path
- */
-export async function getDestinationMap(
- template: Record,
- context: ResolvedPresetContext<'noSymlinks'>,
-): Promise> {
- const {
- custom: { noSymlinks },
- target: { root, name },
- } = context;
- // make sure we use the path of presetter under the target project, not the one via npx
- const presetterDir = resolvePackage('presetter', { cwd: root });
- const outDir = resolve(presetterDir!, '..', '.presetter', name);
-
- const relativePaths = [...Object.keys(template)];
-
- const map = Object.fromEntries([
- ...relativePaths.map((relativePath): [string, string] => [
- relativePath,
- resolve(
- // output on the project root if it's specified as not a symlink
- noSymlinks.includes(relativePath) ? context.target.root : outDir,
- relativePath,
- ),
- ]),
- ]);
-
- debug('DESTINATION MAP\n%O', map);
-
- return map;
-}
diff --git a/packages/presetter/source/preset/presetterRC.ts b/packages/presetter/source/preset/presetterRC.ts
deleted file mode 100644
index 1c12d214..00000000
--- a/packages/presetter/source/preset/presetterRC.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import { existsSync, writeFileSync } from 'node:fs';
-import { dirname, resolve } from 'node:path';
-
-import { readPackageUp } from 'read-pkg-up';
-
-import debug from '../debugger';
-import { loadFile } from '../io';
-import { merge } from '../template';
-import { isJsonObject } from '../utilities';
-
-import type { PresetterConfig } from 'presetter-types';
-import type { JsonValue } from 'type-fest';
-
-/** presetter configuration filename */
-const PRESETTERRC = '.presetterrc';
-
-const JSON_INDENT = 2;
-
-/**
- * get presetter configuration files recursively from the current base up to the monorepo root, if there is one
- * @param base the base directory to start searching for the configuration file
- * @returns list of presetter configuration files
- */
-export async function getPresetterRCPaths(base: string): Promise {
- const filesFromBase = ['', '.json']
- .map((ext) => resolve(base, `${PRESETTERRC}${ext}`))
- .filter(existsSync);
-
- const parent = await readPackageUp({ cwd: dirname(base) });
-
- // if the base is the root of a monorepo, stop searching
- const filesFromParent = parent?.path
- ? await getPresetterRCPaths(dirname(base))
- : [];
-
- return [...filesFromParent, ...filesFromBase];
-}
-
-/**
- * get the .presetterrc configuration file content
- * @param root the base directory in which the configuration file should be located
- * @returns content of the configuration file
- */
-export async function getPresetterRC(root: string): Promise {
- // locate all possible configuration files
- const paths = await getPresetterRCPaths(root);
-
- if (!paths.length) {
- throw new Error('missing presetter configuration file');
- }
-
- const configs = paths.map((path) => loadFile(path, 'json') as JsonValue);
-
- const mergedConfig = configs.reduce(
- (mergedConfig, config) => merge(mergedConfig, config),
- {},
- );
-
- assertPresetterRC(mergedConfig);
-
- debug('MERGED CONFIGURATION PROVIDED BY PROJECT\n%O', mergedConfig);
-
- return mergedConfig;
-}
-
-/**
- * update .presetterrc configuration file content
- * @param root the base directory in which the configuration file should be located
- * @param config content to be merged with the existing configuration file
- */
-export async function updatePresetterRC(
- root: string,
- config: PresetterConfig,
-): Promise {
- const existingPresetterRC = await getPresetterRC(root).catch(() => ({}));
-
- writeFileSync(
- resolve(root, `${PRESETTERRC}.json`),
- JSON.stringify(
- merge(existingPresetterRC as JsonValue, config as unknown as JsonValue),
- null,
- JSON_INDENT,
- ),
- );
-}
-
-/**
- * check that the configuration is valid
- * @param value content from a configuration file
- */
-export function assertPresetterRC(
- value: unknown,
-): asserts value is PresetterConfig {
- if (
- !isJsonObject(value) ||
- (typeof value.preset !== 'string' && !Array.isArray(value.preset))
- ) {
- throw new Error(`invalid presetter configuration file`);
- }
-}
diff --git a/packages/presetter/source/preset/project.ts b/packages/presetter/source/preset/project.ts
new file mode 100644
index 00000000..3d8b5a0f
--- /dev/null
+++ b/packages/presetter/source/preset/project.ts
@@ -0,0 +1,35 @@
+import { xception } from 'xception';
+
+import debug from '../debugger';
+
+import { resolvePresetterConfig } from './config';
+import { resolvePreset } from './resolution';
+
+import type { PresetContext, PresetNode } from 'presetter-types';
+
+/**
+ * resolve the project preset
+ * @param context context about the target project and customization in .presetterrc
+ * @returns assets from the preset
+ */
+export async function resolveProjectPreset(
+ context: PresetContext,
+): Promise {
+ try {
+ debug(`resolving preset at ${context.root}`);
+
+ // get the preset
+ const preset = await resolvePresetterConfig(context.root);
+
+ debug(`preset loaded, resolving nodes...`);
+
+ return await resolvePreset(preset, context);
+ } catch (cause) {
+ throw xception(cause, {
+ meta: { project: context.package.name, root: context.root },
+ });
+ /* v8 ignore start */
+ } finally {
+ debug(`all nodes resolved`);
+ }
+}
diff --git a/packages/presetter/source/preset/resolution/asset.ts b/packages/presetter/source/preset/resolution/asset.ts
new file mode 100644
index 00000000..76da8cff
--- /dev/null
+++ b/packages/presetter/source/preset/resolution/asset.ts
@@ -0,0 +1,121 @@
+import { prefixDisplay } from '../../utilities';
+
+import { resolveNodeContent } from './content';
+import Debug from './debugger';
+import { resolveObject } from './object';
+import { resolveVariables } from './variable';
+
+import type {
+ PresetContentContext,
+ PresetContext,
+ PresetNode,
+ ResolvedPresetAsset,
+ ResolvedPresetAssets,
+ Variables,
+} from 'presetter-types';
+
+/**
+ * lists all asset names in a preset node
+ * @param node the preset node
+ * @param context the context in which the node is being resolved
+ * @returns an array of asset names
+ */
+export function listAssetNames(
+ node: PresetNode,
+ context: PresetContentContext,
+): string[] {
+ const assetsResolver = node.definition.assets ?? {};
+ const assets =
+ assetsResolver instanceof Function
+ ? assetsResolver(context)
+ : assetsResolver;
+
+ return [
+ ...new Set([
+ ...Object.keys(assets),
+ ...node.nodes.flatMap((node) => listAssetNames(node, context)),
+ ]),
+ ];
+}
+
+/**
+ * resolves all assets for a given preset node
+ * @param node the preset node
+ * @param context the context in which the node is being resolved
+ * @returns a promise that resolves to the RESOLVED assets
+ */
+export async function resolveAssets(
+ node: PresetNode,
+ context: PresetContext,
+): Promise {
+ const debug = Debug.extend('ASSETS').extend(node.definition.id);
+
+ // resolve variables for the node
+ const variables = await resolveVariables(node, context);
+
+ // list all asset names in the node
+ const names = listAssetNames(node, { ...context, variables });
+ debug('ASSET FILES\n%O', names);
+
+ // resolve each asset and return as an object
+ return Object.fromEntries(
+ await Promise.all(
+ names.map(async (name) => [
+ name,
+ await resolveAsset({ name, node, context, variables }),
+ ]),
+ ),
+ ) as ResolvedPresetAssets;
+}
+
+/**
+ * resolves a specific asset for a given preset node
+ * @param _ collection of arguments
+ * @param _.name the name of the asset
+ * @param _.node the preset node
+ * @param _.context the context in which the node is being resolved
+ * @param _.variables the variables to use during resolution
+ * @returns a promise that resolves to the RESOLVED asset
+ */
+export async function resolveAsset(_: {
+ name: string;
+ node: PresetNode;
+ context: PresetContext;
+ variables: Variables;
+}): Promise {
+ const { name, node, context, variables } = _;
+ const debug = Debug.extend(name).extend(node.definition.id);
+
+ debug(`RESOLVING ASSET ${name} (INITIAL PASS)`);
+
+ // resolve the initial value of the asset
+ const initial = await resolveNodeContent({
+ name,
+ node,
+ context,
+ variables,
+ select: ({ assets }) =>
+ resolveObject(assets, { ...context, variables })?.[name],
+ });
+
+ debug(
+ `RESOLVED ASSET ${name} (INITIAL PASS)\n${prefixDisplay('└─ ', initial)}`,
+ );
+
+ debug(`RESOLVING ASSET ${name} (FINAL PASS)`);
+
+ // resolve the final value of the asset, considering overrides
+ const final = await resolveNodeContent({
+ name,
+ node,
+ context,
+ initial,
+ variables,
+ select: ({ override }) =>
+ resolveObject(override?.assets, { ...context, variables })?.[name],
+ });
+
+ debug(`RESOLVED ASSET ${name} (FINAL PASS)\n${prefixDisplay('└─ ', final)}`);
+
+ return final;
+}
diff --git a/packages/presetter/source/preset/resolution/content.ts b/packages/presetter/source/preset/resolution/content.ts
new file mode 100644
index 00000000..4224f9d6
--- /dev/null
+++ b/packages/presetter/source/preset/resolution/content.ts
@@ -0,0 +1,94 @@
+import { loadFile } from '../../io';
+import { merge } from '../../template';
+import { prefixDisplay } from '../../utilities';
+
+import Debug from './debugger';
+
+import type {
+ PresetContent,
+ PresetContext,
+ PresetDefinition,
+ PresetNode,
+ Variables,
+} from 'presetter-types';
+
+/**
+ * loads a potentially dynamic content
+ * @param _ collection of arguments
+ * @param _.content content to be loaded
+ * @param _.current the current content
+ * @param _.variables the variables to use during resolution
+ * @param _.context the context in which the content is being resolved
+ * @returns a promise that resolves to the RESOLVED content
+ */
+export async function resolveContent(_: {
+ content?: PresetContent;
+ current?: T | null;
+ variables?: Variables;
+ context: PresetContext;
+}): Promise {
+ const { content, current, variables = {}, context } = _;
+
+ // if content is a function, call it with the current content and context
+ if (content instanceof Function) {
+ return content(current, { ...context, variables });
+ }
+
+ // if content is a string, load the file content
+ const resolvedContent =
+ typeof content === 'string' ? (loadFile(content, variables) as T) : content;
+
+ // merge the current content with the RESOLVED content
+ return merge(current, resolvedContent) as T | null | undefined;
+}
+
+/**
+ * resolves the content of a node within a preset
+ * @template T the type of the content to be resolved
+ * @param _ collection of arguments
+ * @param _.name the name of the preset being resolved
+ * @param _.node the node to resolve
+ * @param _.context the context in which the node is being resolved
+ * @param _.initial an optional initial value to start the resolution with
+ * @param _.variables an optional record of variables to use during resolution
+ * @param _.select a function to select the content definition from the preset definition
+ * @returns a promise that resolves to the RESOLVED content of the node, which can be of type `T`, `null`, or `undefined`
+ */
+export async function resolveNodeContent(_: {
+ name: string;
+ node: PresetNode;
+ context: PresetContext;
+ initial?: T | null;
+ variables?: Variables;
+ select: (definition: PresetDefinition) => PresetContent | undefined;
+}): Promise {
+ const { name, node, context, initial, variables, select } = _;
+ const { definition, nodes } = node;
+ const debug = Debug.extend(name).extend(node.definition.id);
+
+ debug(`INCOMING (${name})\n${prefixDisplay('└─ ', initial)}`);
+
+ const current = await nodes.reduce(
+ async (merged, node) => {
+ const initial = await merged;
+ const next = await resolveNodeContent({ ..._, node, initial });
+
+ return merge(initial, next) as T | null | undefined;
+ },
+ initial as Promise,
+ );
+
+ // select the content from the definition
+ const content = select(definition);
+
+ debug(
+ `COMBINING (${name})\n${prefixDisplay('└─ ', current)}\n${prefixDisplay('└─ ', content)}`,
+ );
+
+ // resolve dynamic content
+ const out = await resolveContent({ content, current, variables, context });
+
+ debug(`RETURNING (${name})\n${prefixDisplay('└─ ', out)}`);
+
+ return out;
+}
diff --git a/packages/presetter/source/preset/resolution/debugger.ts b/packages/presetter/source/preset/resolution/debugger.ts
new file mode 100644
index 00000000..e6d0a610
--- /dev/null
+++ b/packages/presetter/source/preset/resolution/debugger.ts
@@ -0,0 +1,3 @@
+import Debug from '../../debugger';
+
+export default Debug.extend('resolution');
diff --git a/packages/presetter/source/preset/resolution/index.ts b/packages/presetter/source/preset/resolution/index.ts
new file mode 100644
index 00000000..e685c8f4
--- /dev/null
+++ b/packages/presetter/source/preset/resolution/index.ts
@@ -0,0 +1,7 @@
+/* v8 ignore start */
+
+export * from './asset';
+export * from './object';
+export * from './preset';
+export * from './script';
+export * from './variable';
diff --git a/packages/presetter/source/preset/resolution/object.ts b/packages/presetter/source/preset/resolution/object.ts
new file mode 100644
index 00000000..9cadd64f
--- /dev/null
+++ b/packages/presetter/source/preset/resolution/object.ts
@@ -0,0 +1,14 @@
+import type { PresetContentContext, PresetObject } from 'presetter-types';
+
+/**
+ * resolve an object
+ * @param object the object to resolve
+ * @param context the context in which the object is being resolved
+ * @returns the resolved object
+ */
+export function resolveObject(
+ object: PresetObject,
+ context: PresetContentContext,
+): T {
+ return object instanceof Function ? object(context) : object;
+}
diff --git a/packages/presetter/source/preset/resolution/preset.ts b/packages/presetter/source/preset/resolution/preset.ts
new file mode 100644
index 00000000..b5805cf9
--- /dev/null
+++ b/packages/presetter/source/preset/resolution/preset.ts
@@ -0,0 +1,31 @@
+import debug from './debugger';
+
+import type { Preset, PresetContext, PresetNode } from 'presetter-types';
+
+/**
+ * resolve a preset
+ * @param preset the preset to resolve
+ * @param context the context in which the preset is being resolved
+ * @returns the resolved preset node
+ */
+export async function resolvePreset(
+ preset: Preset,
+ context: PresetContext,
+): Promise {
+ debug(`resolving node ${preset.id}`);
+
+ const definition =
+ preset instanceof Function
+ ? { id: preset.id, ...(await preset(context)) }
+ : preset;
+
+ const nodes = await Promise.all(
+ (definition.extends ?? []).map(async (preset) =>
+ resolvePreset(preset, context),
+ ),
+ );
+
+ debug(`preset ${preset.id} resolved`);
+
+ return { definition, nodes };
+}
diff --git a/packages/presetter/source/preset/resolution/script.ts b/packages/presetter/source/preset/resolution/script.ts
new file mode 100644
index 00000000..25116092
--- /dev/null
+++ b/packages/presetter/source/preset/resolution/script.ts
@@ -0,0 +1,54 @@
+import { substitute } from '../../template';
+
+import { resolveNodeContent } from './content';
+import Debug from './debugger';
+import { resolveVariables } from './variable';
+
+import type { PresetContext, PresetNode, Scripts } from 'presetter-types';
+
+/**
+ * resolves all scripts for a given preset node
+ * @param node the preset node
+ * @param context the context in which the node is being resolved
+ * @returns a promise that resolves to the RESOLVED scripts
+ */
+export async function resolveScripts(
+ node: PresetNode,
+ context: PresetContext,
+): Promise {
+ const name = 'SCRIPTS';
+ const debug = Debug.extend(name);
+
+ // resolve variables for the node
+ const variables = await resolveVariables(node, context);
+
+ debug('RESOLVING SCRIPTS (INITIAL PASS)');
+
+ // resolve the initial value of the scripts
+ const initial = await resolveNodeContent({
+ name,
+ node,
+ context,
+ variables,
+ select: ({ scripts }) => scripts,
+ });
+
+ debug('RESOLVED SCRIPTS (INITIAL PASS)\n%O', initial);
+
+ debug('RESOLVING SCRIPTS (FINAL PASS)');
+
+ // resolve the final value of the scripts, considering overrides
+ const final =
+ (await resolveNodeContent({
+ name,
+ node,
+ context,
+ initial,
+ variables,
+ select: ({ override }) => override?.scripts,
+ })) ?? {};
+
+ debug('RESOLVED SCRIPTS (FINAL PASS)\n%O', final);
+
+ return substitute(final, variables);
+}
diff --git a/packages/presetter/source/preset/resolution/variable.ts b/packages/presetter/source/preset/resolution/variable.ts
new file mode 100644
index 00000000..a2411203
--- /dev/null
+++ b/packages/presetter/source/preset/resolution/variable.ts
@@ -0,0 +1,46 @@
+import { resolveNodeContent } from './content';
+import Debug from './debugger';
+
+import type { PresetContext, PresetNode, Variables } from 'presetter-types';
+
+/**
+ * resolves all variables for a given preset node
+ * @param node the preset node
+ * @param context the context in which the node is being resolved
+ * @returns a promise that resolves to the RESOLVED variables
+ */
+export async function resolveVariables(
+ node: PresetNode,
+ context: PresetContext,
+): Promise {
+ const name = 'VARIABLES';
+ const debug = Debug.extend(name);
+
+ debug('RESOLVING VARIABLES (INITIAL PASS)');
+
+ // resolve the initial value of the variables
+ const initial = await resolveNodeContent({
+ name,
+ node,
+ context,
+ select: ({ variables }) => variables,
+ });
+
+ debug('RESOLVED VARIABLES (INITIAL PASS)\n%O', initial);
+
+ debug('RESOLVING VARIABLES (FINAL PASS)');
+
+ // resolve the final value of the variables, considering overrides
+ const final =
+ (await resolveNodeContent({
+ name,
+ node,
+ context,
+ initial,
+ select: ({ override }) => override?.variables,
+ })) ?? {};
+
+ debug('RESOLVED VARIABLES (FINAL PASS)\n%O', final);
+
+ return final;
+}
diff --git a/packages/presetter/source/preset/scripts.ts b/packages/presetter/source/preset/scripts.ts
index fd6debd8..4397d192 100644
--- a/packages/presetter/source/preset/scripts.ts
+++ b/packages/presetter/source/preset/scripts.ts
@@ -1,8 +1,6 @@
-import { resolveContext, resolveScripts } from '../content';
-import debug from '../debugger';
-
import { getContext } from './context';
-import { getPresetGraph } from './graph';
+import { resolveProjectPreset } from './project';
+import { resolveScripts } from './resolution';
/**
* get the merged scripts templates
@@ -10,12 +8,9 @@ import { getPresetGraph } from './graph';
*/
export async function getScripts(): Promise> {
const context = await getContext();
- const graph = await getPresetGraph(context);
- const resolvedContext = await resolveContext({ graph, context });
-
- const script = await resolveScripts({ graph, context: resolvedContext });
+ const node = await resolveProjectPreset(context);
- debug('SCRIPT PROVIDED BY PRESET\n%O', script);
+ const script = await resolveScripts(node, context);
return script;
}
diff --git a/packages/presetter/source/preset/setup.ts b/packages/presetter/source/preset/setup.ts
deleted file mode 100644
index c07f9faa..00000000
--- a/packages/presetter/source/preset/setup.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-import { info } from 'node:console';
-import { dirname } from 'node:path';
-
-import { readPackage } from 'read-pkg';
-import { writePackage } from 'write-pkg';
-
-import {
- arePeerPackagesAutoInstalled,
- getPackage,
- reifyDependencies,
-} from '../package';
-
-import { bootstrapContent } from './content';
-import { getContext } from './context';
-import { updatePresetterRC } from './presetterRC';
-
-import type { PackageJson } from 'read-pkg';
-
-/** collection of options for bootstrapping */
-interface BootstrapOptions {
- /** whether to skip all checks */
- force?: boolean;
-}
-
-/**
- * bootstrap the preset to the current project root
- * @param options collection of options
- */
-export async function bootstrapPreset(
- options?: BootstrapOptions,
-): Promise {
- const context = await getContext();
-
- // install all related packages first
- if (!arePeerPackagesAutoInstalled()) {
- await reifyDependencies({ root: context.target.root });
- }
-
- // generate configurations
- await bootstrapContent(context, options);
-}
-
-/**
- * adopt a preset to the project
- * @param uris list of name or git url of the preset
- */
-export async function setupPreset(...uris: string[]): Promise {
- // NOTE: comparing packages before and after installation is the only reliable way
- // to extract the name of the preset in case it's given as a git url or file path etc.
- const { path } = await getPackage();
- const root = dirname(path);
- const packageBefore = (await readPackage({ cwd: root })).devDependencies;
-
- // install presetter & the preset
- info(`Installing ${uris.join(' ')}... it may take a few moment...`);
- await reifyDependencies({
- root,
- add: ['presetter', ...uris],
- saveAs: 'dev',
- lockFile: true,
- });
-
- // extract the name of the installed preset
- const packageAfter = (await readPackage({ cwd: root })).devDependencies;
- const newPackages = getNewPackages({ ...packageBefore }, { ...packageAfter });
- const preset = newPackages.filter((name) => name !== 'presetter');
-
- info('Updating .presetterrc.json & package.json');
- // update .presetterrc.json
- await updatePresetterRC(root, { preset });
-
- // bootstrap configuration files with the new .presetterrc.json
- const context = await getContext();
- await bootstrapContent(context);
-
- // insert post install script if not preset
- const json = context.target.package;
- const scripts = { prepare: 'presetter bootstrap', ...json.scripts };
- const patched = { ...json, scripts };
- await writePackage(root, patched as PackageJson & Record);
-
- info('Done. Enjoy coding!');
-}
-
-/**
- * get a list of new packages installed by comparing the before and after state of devDependencies in package.json
- * @param before before state of devDependencies in package.json
- * @param after after state of devDependencies in package.json
- * @returns list of new package names
- */
-function getNewPackages(
- before: Record,
- after: Record,
-): string[] {
- return Object.keys(after).filter((name): name is string => !before[name]);
-}
diff --git a/packages/presetter/source/preset/unset.ts b/packages/presetter/source/preset/unset.ts
deleted file mode 100644
index 24223700..00000000
--- a/packages/presetter/source/preset/unset.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { resolveContext, resolveTemplate } from '../content';
-import { unlinkFiles } from '../io';
-
-import { getContext } from './context';
-import { getPresetGraph } from './graph';
-import { getDestinationMap } from './mapping';
-
-/**
- * uninstall the preset from the current project root
- */
-export async function unsetPreset(): Promise {
- const context = await getContext();
- const graph = await getPresetGraph(context);
- const resolvedContext = await resolveContext({ graph, context });
- const content = await resolveTemplate({ graph, context: resolvedContext });
- const configurationLink = await getDestinationMap(content, resolvedContext);
-
- unlinkFiles(context.target.root, configurationLink);
-}
diff --git a/packages/presetter/source/resolution.ts b/packages/presetter/source/resolution.ts
deleted file mode 100644
index 948a33e4..00000000
--- a/packages/presetter/source/resolution.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import { existsSync } from 'node:fs';
-import { basename, extname } from 'node:path';
-
-import { loadFile } from './io';
-
-import type {
- DynamicAsset,
- DynamicAssetField,
- Generator,
- PresetAsset,
- PresetterConfig,
- RequiredResolution,
- ResolvedPresetContext,
- Template,
-} from 'presetter-types';
-
-/**
- * compute the corresponding field within the config field of .presetterrc
- * @param filename link name
- * @returns field name in config
- */
-export function getConfigKey(filename: string): string {
- return basename(filename, extname(filename))
- .replace(/^\./, '')
- .replace(/rc$/, '')
- .replace(/\.config$/, '');
-}
-
-/**
- * resolve a dynamic asset content
- * @param map a dynamic map to be resolved
- * @param context arguments to be passed to the generator function
- * @returns content of the resolved field
- */
-export async function loadDynamicMap(
- map: PresetAsset[F],
- context: ResolvedPresetContext>,
-): Promise>> {
- // load templated configuration from presets
- return Object.fromEntries(
- await Promise.all(
- Object.entries(
- map instanceof Function
- ? await map(context as ResolvedPresetContext)
- : { ...map },
- ).map(
- async ([relativePath, value]): Promise<[string, any]> => [
- relativePath,
- await loadDynamic(value, context as ResolvedPresetContext),
- ],
- ),
- ),
- );
-}
-
-/**
- * load a potentially dynamic content
- * @param value content to be loaded
- * @param context context to be supplied to the generator
- * @returns resolved content
- */
-export async function loadDynamic<
- R extends Template | string[],
- K extends keyof PresetterConfig,
->(
- value:
- | string // path to a template file
- | R // template content
- | Generator,
- context: ResolvedPresetContext,
-): Promise {
- if (typeof value === 'function') {
- return value(context);
- } else if (typeof value === 'string' && existsSync(value)) {
- return loadFile(value) as R;
- } else {
- return value as R;
- }
-}
diff --git a/packages/presetter/source/resolve.ts b/packages/presetter/source/resolve.ts
new file mode 100644
index 00000000..de6ed541
--- /dev/null
+++ b/packages/presetter/source/resolve.ts
@@ -0,0 +1,41 @@
+import { dirname, relative, sep } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+import {
+ getContext,
+ resolveAsset,
+ resolvePreset,
+ resolvePresetterConfig,
+ resolveVariables,
+} from './preset';
+
+/**
+ * resolves a dynamic asset from a file URL
+ * @param url the URL of the asset
+ * @returns a promise that resolves to the RESOLVED asset
+ */
+export async function resolve(url: string): Promise> {
+ const path = fileURLToPath(url);
+ const context = await getContext(dirname(path));
+
+ // get the relative name of the asset
+ const name = relative(context.root, path).split(sep).join('/'); // make the path OS agnostic
+
+ // get the preset configuration
+ const preset = await resolvePresetterConfig(context.root);
+
+ // resolve the preset node
+ const node = await resolvePreset(preset, context);
+
+ // resolve variables for the node
+ const variables = await resolveVariables(node, context);
+
+ // resolve the asset
+ const asset = await resolveAsset({ name, node, context, variables });
+
+ if (!asset) {
+ throw new Error(`asset ${name} not found in preset defined at ${path}`);
+ }
+
+ return asset as Record;
+}
diff --git a/packages/presetter/source/serialization.ts b/packages/presetter/source/serialization.ts
new file mode 100644
index 00000000..9c2aa801
--- /dev/null
+++ b/packages/presetter/source/serialization.ts
@@ -0,0 +1,59 @@
+import { extname } from 'node:path';
+
+import { dump } from 'js-yaml';
+
+import type { ResolvedPresetAsset } from 'presetter-types';
+
+// JSON format
+const INDENT = 2;
+
+/**
+ * generates dynamic ES module content for the specified preset
+ * @param exports an array of export names to include in the generated content
+ * @returns a string representing the dynamically generated ES module content
+ */
+export function buildEsmFile(exports: string[]): string {
+ return [
+ `import { resolve } from 'presetter';`,
+ '',
+ `const assets = await resolve(import.meta.url);`,
+ '',
+ ...exports.map((exportName) =>
+ exportName === 'default'
+ ? `export default assets['${exportName}'];`
+ : `export const ${exportName} = assets['${exportName}'];`,
+ ),
+ ].join('\n');
+}
+
+/**
+ * serialize a configuration content to the right format according to its destination
+ * @param destination the path to which the content will be written
+ * @param content configuration content
+ * @returns serialized content
+ */
+export function serialize(
+ destination: string,
+ content: ResolvedPresetAsset,
+): string | Buffer {
+ if (Buffer.isBuffer(content)) {
+ return content;
+ }
+
+ switch (extname(destination)) {
+ case '.yaml':
+ case '.yml':
+ return dump(content);
+ case '.json':
+ return JSON.stringify(content, null, INDENT);
+ case '.js':
+ case '.mjs':
+ case '.ts':
+ case '.mts':
+ return buildEsmFile(Object.keys(content as Record));
+ default:
+ return Array.isArray(content)
+ ? content.join('\n') // for ignore list, e.g., .gitignore
+ : JSON.stringify(content, null, INDENT); // for JSON config not with a .json extension, e.g., .prettierrc
+ }
+}
diff --git a/packages/presetter/source/template/merge.ts b/packages/presetter/source/template/merge.ts
index 51aae273..ab62f89b 100644
--- a/packages/presetter/source/template/merge.ts
+++ b/packages/presetter/source/template/merge.ts
@@ -1,8 +1,6 @@
-import { basename, extname } from 'node:path';
+import { isPlainObject } from '../utilities';
-import { isJsonObject } from '../utilities';
-
-import type { JsonObject, UnknownRecord } from 'type-fest';
+import type { UnknownRecord } from 'type-fest';
type MergedType = A extends UnknownRecord
? B extends UnknownRecord
@@ -31,13 +29,20 @@ export function merge(source: S, target?: T): MergedType {
// Object | replace | MERGE | replace
// Primitive | replace | replace | replace
- if (Array.isArray(source)) {
+ if (source === target) {
+ // increase performance, especially for large objects, since no change is needed anyway
+ return source as MergedType;
+ } else if (Array.isArray(source)) {
return mergeArray(source, target) as MergedType;
- } else if (isJsonObject(source)) {
+ } else if (isPlainObject(source)) {
return mergeObject(source, target) as MergedType;
}
- return (target ?? source) as MergedType;
+ // NOTE:
+ // if target is undefined, return the source
+ // otherwise, return the target, even it is null
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ return (target === undefined ? source : target) as MergedType;
}
/**
@@ -55,7 +60,7 @@ export function mergeArray(source: S[], target?: T): MergedArray {
if (Array.isArray(target)) {
return mergeArrays(source, target) as MergedArray;
} else if (
- isJsonObject(target) &&
+ isPlainObject(target) &&
[...Object.keys(target)].every((key) => parseInt(key) >= 0)
) {
return [...source].map((value, key) =>
@@ -64,7 +69,8 @@ export function mergeArray(source: S[], target?: T): MergedArray {
}
// if a merge isn't possible return the replacement or the original if no replacement is found
- return (target ?? source) as MergedArray;
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ return (target === undefined ? source : target) as MergedArray;
}
/**
@@ -77,28 +83,7 @@ export function mergeArrays(
source: S[],
target: T[],
): MergedArray {
- const isPrimitive =
- source.every((value) => !isJsonObject(value)) &&
- target.every((value) => !isJsonObject(value));
-
- // if there is no object in both list, perform an union
- // (['a'], ['a']) => ['a']
- // (['a'], ['b']) => ['a', 'b']
- // (['a'], ['a', 'b']) => ['a', 'b']
-
- if (isPrimitive) {
- return [...new Set([...source, ...target])] as MergedArray;
- }
-
- // if there is an object in any of the list, perform a replacement
- // (['a', 'b'], [{ c: 1 }]) => [{ c: 1 }]
- // ([{ a: 1 }], [{ b: 2 }]) => [{ b: 2 }]
- // ([{ a: 1 }, 'c'], [{ b: 2 }]) => [{ b: 2 }]
- // (['a'], ['b', { c: 1 }]) => ['b', { c: 1 }]
- // (['a', { c: 1 }], ['b', { d: 2 }]) => ['b', { d: 2 }]
-
- // if a merge isn't possible return the replacement
- return target as MergedArray;
+ return [...new Set([...source, ...target])] as MergedArray;
}
/**
@@ -107,11 +92,11 @@ export function mergeArrays(
* @param target new replacement
* @returns merged value
*/
-export function mergeObject(
+export function mergeObject(
source: S,
target?: T,
): MergedType {
- if (isJsonObject(target)) {
+ if (isPlainObject(target)) {
// merge two objects together
const mergedSource = Object.fromEntries(
Object.entries(source).map(([key, value]) => [
@@ -128,48 +113,6 @@ export function mergeObject(
}
// otherwise replace the source with target
- return (target ?? source) as MergedType;
-}
-
-/**
- * merge templates
- * @param source current template
- * @param target new template content
- * @returns customized configuration
- */
-export function mergeTemplate(
- source: Record,
- target: Record,
-): Record {
- const mergedSource = Object.fromEntries(
- Object.entries(source).map(([path, template]) => {
- const replacement = target[path] as Partial[string];
- const isIgnoreFile =
- !extname(path) &&
- basename(path).startsWith('.') &&
- typeof template === 'string' &&
- typeof replacement === 'string';
-
- // NOTE
- // for JSON content, merge with the specified mode
- // for string content, there are two scenarios:
- // 1. if the content is a list such as an ignore file, merge as appendion
- // 2. for others such as a typescript file, merge as override
-
- if (isIgnoreFile) {
- const mergedSet = new Set([
- ...template.split('\n'),
- ...replacement.split('\n'),
- ]);
-
- return [path, [...mergedSet].join('\n')];
- } else if (typeof template === typeof replacement) {
- return [path, merge(template, replacement)];
- }
-
- return [path, template];
- }),
- );
-
- return { ...target, ...mergedSource };
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ return (target === undefined ? source : target) as MergedType;
}
diff --git a/packages/presetter/source/template/substitute.ts b/packages/presetter/source/template/substitute.ts
index 44eb4678..97610773 100644
--- a/packages/presetter/source/template/substitute.ts
+++ b/packages/presetter/source/template/substitute.ts
@@ -2,36 +2,29 @@ import pupa from 'pupa';
import { isJsonObject } from '../utilities';
+import type { Variables } from 'presetter-types';
+
/**
* replace parameters in the template
* @param content template content
- * @param parameter variables to be substituted in the template
+ * @param variables variables to be substituted in the template
* @returns customized configuration
*/
-export function substitute(
- content: string,
- parameter: Record,
-): string;
+export function substitute(content: string, variables: Variables): string;
export function substitute | unknown[]>(
content: Content,
- parameter: Record,
+ variables: Variables,
): Content;
-export function substitute(
- content: unknown,
- parameter: Record,
-): unknown;
-export function substitute(
- content: unknown,
- parameter: Record,
-): unknown {
+export function substitute(content: unknown, variables: Variables): unknown;
+export function substitute(content: unknown, variables: Variables): unknown {
if (typeof content === 'string') {
- return pupa(content, parameter, { ignoreMissing: true });
+ return pupa(content, variables, { ignoreMissing: true });
} else if (Array.isArray(content)) {
- return content.map((value: unknown) => substitute(value, parameter));
+ return content.map((value: unknown) => substitute(value, variables));
} else if (isJsonObject(content)) {
return Object.fromEntries(
Object.entries(content).map(([key, value]) => {
- return [substitute(key, parameter), substitute(value, parameter)];
+ return [substitute(key, variables), substitute(value, variables)];
}),
);
} else {
diff --git a/packages/presetter/source/utilities/display.ts b/packages/presetter/source/utilities/display.ts
new file mode 100644
index 00000000..ef5c3303
--- /dev/null
+++ b/packages/presetter/source/utilities/display.ts
@@ -0,0 +1,55 @@
+/**
+ * displays the content of a variable
+ * @param content the content to display
+ * @param level the level of nesting
+ * @returns a string representation of the content
+ */
+export function display(content: unknown, level = 0): string {
+ if (typeof content === 'function') {
+ return 'Function';
+ } else if (Array.isArray(content)) {
+ return `Array(${content.length})`;
+ } else if (content instanceof Buffer) {
+ return 'Buffer';
+ } else if (content === null) {
+ return 'null';
+ } else if (typeof content === 'object') {
+ return `Object({${
+ level > 1
+ ? '...' // skip displaying nested objects
+ : '\n' +
+ Object.keys(content)
+ .map(
+ (key) =>
+ // indent the content based on the level
+ ' '.repeat(level + 1) +
+ `${key}: ` +
+ display(content[key], level + 1),
+ )
+ .join('\n') +
+ '\n' +
+ ' '.repeat(level)
+ }})`;
+ }
+
+ return typeof content;
+}
+
+/**
+ * displays the content of a variable with a prefix
+ * @param prefix the prefix to add to the display
+ * @param content the content to display
+ * @returns a string representation of the content with the prefix
+ */
+export function prefixDisplay(prefix: string, content: unknown): string {
+ const representation = display(content);
+ const indent = ' '.repeat(prefix.length);
+
+ return (
+ prefix +
+ representation
+ .split('\n')
+ .join('\n' + indent)
+ .trim()
+ );
+}
diff --git a/packages/presetter/source/utilities/filter.ts b/packages/presetter/source/utilities/filter.ts
deleted file mode 100644
index a81ce462..00000000
--- a/packages/presetter/source/utilities/filter.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import type { IgnorePath, IgnoreRule } from 'presetter-types';
-
-/**
- * remove part of the template content according to the given rules
- * @param subject an object to be filtered
- * @param ignores a list of ignore rules
- * @returns filtered content
- */
-export function filter>(
- subject: C,
- ...ignores: IgnoreRule[]
-): C;
-export function filter(subject: unknown[], ...ignores: IgnoreRule[]): unknown[];
-export function filter(
- subject: Record | unknown[],
- ...ignores: IgnoreRule[]
-): Record | unknown[] {
- // compute the list of fields in config to be ignored
- const fieldsToIgnore = ignores.filter(
- (ignore): ignore is string | number => typeof ignore !== 'object',
- );
-
- // filter the unwanted item in an array
- if (Array.isArray(subject)) {
- return subject.filter((_, key) => !fieldsToIgnore.includes(key));
- }
-
- // filter the unwanted fields below
- const distilled = Object.fromEntries(
- Object.entries(subject).filter(([key, _]) => !fieldsToIgnore.includes(key)),
- );
-
- // compute the left over and process them further below
- const moreRules = ignores.filter(
- (ignore): ignore is Record =>
- typeof ignore !== 'string',
- );
-
- // continue filtering the left over
- return moreRules.reduce(
- (furtherDistilled, ignoreTree) =>
- Object.fromEntries(
- Object.entries(furtherDistilled).map(
- ([key, value]): [string, unknown] => [
- key,
- filterByPath(value, ignoreTree[key]),
- ],
- ),
- ),
- distilled,
- );
-}
-
-/**
- * filter a value according to the supplied ignore rules
- * @param value value to be filtered
- * @param path ignore rule to be applied
- * @returns filtered value
- */
-function filterByPath(value: unknown, path?: IgnorePath): unknown {
- return path && typeof value === 'object'
- ? filter(
- value as Record,
- // NOTE
- // if rule is an array, it means that it contains a list of fields to be ignored
- // otherwise, it contains rules in a tree form
- ...(Array.isArray(path) ? path : [path]),
- )
- : value;
-}
diff --git a/packages/presetter/source/utilities/index.ts b/packages/presetter/source/utilities/index.ts
index dca90a32..1af3e927 100644
--- a/packages/presetter/source/utilities/index.ts
+++ b/packages/presetter/source/utilities/index.ts
@@ -1,5 +1,5 @@
/* v8 ignore start */
-export * from './filter';
+export * from './display';
export * from './mapping';
export * from './object';
diff --git a/packages/presetter/source/utilities/object.ts b/packages/presetter/source/utilities/object.ts
index 071a89c4..1daaa503 100644
--- a/packages/presetter/source/utilities/object.ts
+++ b/packages/presetter/source/utilities/object.ts
@@ -41,6 +41,8 @@ export function isPlainObject(
return (
!!subject &&
typeof subject === 'object' &&
- (Object.getPrototypeOf(subject) as object | null)?.constructor === Object
+ ((Object.getPrototypeOf(subject) as object | null)?.constructor ===
+ Object ||
+ Object.getPrototypeOf(subject) === null)
);
}
diff --git a/packages/presetter/spec/content/resolveContext.spec.ts b/packages/presetter/spec/content/resolveContext.spec.ts
deleted file mode 100644
index 5030673d..00000000
--- a/packages/presetter/spec/content/resolveContext.spec.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-import { describe, expect, it } from 'vitest';
-
-import { resolveContext } from '#content';
-
-describe('fn:resolveContext', () => {
- it('make those required fields available', async () => {
- expect(
- await resolveContext({
- graph: [],
- context: {
- target: {
- name: 'client',
- root: '/project',
- package: {},
- },
- custom: {
- preset: 'preset',
- config: {},
- variable: {},
- noSymlinks: [],
- },
- },
- }),
- ).toMatchObject({
- custom: { config: {}, noSymlinks: [], variable: {} },
- });
- });
-
- it('compute the final variables', async () => {
- expect(
- await resolveContext({
- graph: [
- { name: 'preset1', asset: { variable: { var1: 'var1' } }, nodes: [] },
- {
- name: 'preset1',
- asset: { variable: { var1: 'changed', var2: 'var2' } },
- nodes: [],
- },
- ],
- context: {
- target: {
- name: 'client',
- root: '/project',
- package: {},
- },
- custom: {
- preset: 'preset',
- config: {},
- variable: {},
- noSymlinks: [],
- },
- },
- }),
- ).toMatchObject({
- custom: { variable: { var1: 'changed', var2: 'var2' } },
- });
- });
-
- it('pass on symlinks', async () => {
- expect(
- await resolveContext({
- graph: [],
- context: {
- target: {
- name: 'client',
- root: '/project',
- package: {},
- },
- custom: {
- preset: 'preset',
- config: {},
- variable: {},
- noSymlinks: ['noSymlink'],
- },
- },
- }),
- ).toMatchObject({
- custom: { noSymlinks: ['noSymlink'] },
- });
- });
-
- it('consolidate all symlinks both provided by presets and presetterrc', async () => {
- expect(
- await resolveContext({
- graph: [
- {
- name: 'preset1',
- asset: { noSymlinks: () => ['preset'] },
- nodes: [],
- },
- ],
- context: {
- target: {
- name: 'client',
- root: '/project',
- package: {},
- },
- custom: {
- preset: 'preset',
- config: {},
- variable: {},
- noSymlinks: ['custom'],
- },
- },
- }),
- ).toMatchObject({
- custom: { noSymlinks: ['preset', 'custom'] },
- });
- });
-
- it('pass on custom configs', async () => {
- expect(
- await resolveContext({
- graph: [],
- context: {
- target: {
- name: 'client',
- root: '/project',
- package: {},
- },
- custom: {
- preset: 'preset',
- config: { list: ['line'] },
- variable: {},
- noSymlinks: [],
- },
- },
- }),
- ).toMatchObject({
- custom: { config: { list: ['line'] } },
- });
- });
-});
diff --git a/packages/presetter/spec/content/resolveNoSymlinks.spec.ts b/packages/presetter/spec/content/resolveNoSymlinks.spec.ts
deleted file mode 100644
index 15901d6d..00000000
--- a/packages/presetter/spec/content/resolveNoSymlinks.spec.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { describe, expect, it } from 'vitest';
-
-import { resolveNoSymlinks } from '#content';
-
-describe('fn:resolveNoSymlinks', () => {
- it('compute the final no symlinks list', async () => {
- expect(
- await resolveNoSymlinks({
- graph: [
- {
- name: 'preset1',
- asset: {
- noSymlinks: ['file1', 'file2'],
- },
- nodes: [],
- },
- {
- name: 'preset2',
- asset: {
- noSymlinks: ['file1', 'file3'],
- },
- nodes: [
- {
- name: 'preset3',
- asset: {
- noSymlinks: ['file4'],
- },
- nodes: [],
- },
- ],
- },
- ],
- context: {
- target: {
- name: 'client',
- root: '/project',
- package: {},
- },
- custom: {
- preset: 'preset',
- variable: {},
- },
- },
- }),
- ).toEqual(['file1', 'file2', 'file4', 'file3']);
- });
-});
diff --git a/packages/presetter/spec/content/resolveScripts.spec.ts b/packages/presetter/spec/content/resolveScripts.spec.ts
deleted file mode 100644
index cc7a52bb..00000000
--- a/packages/presetter/spec/content/resolveScripts.spec.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import { posix, relative, resolve, sep } from 'node:path';
-
-import { describe, expect, it, vi } from 'vitest';
-
-import { resolveScripts } from '#content';
-
-vi.mock('#resolution', async (importActual) => {
- const getFileContext = (path) => {
- // ensure that the paths below is compatible with windows
- const posixPath = relative(resolve('/'), path).split(sep).join(posix.sep);
- switch (posixPath) {
- case 'path/to/script.yaml':
- return { task: 'cmd' };
- default:
- throw new Error(`loadFile: missing path ${path}`);
- }
- };
-
- const loadDynamic = (value, context) => {
- if (typeof value === 'function') {
- return value(context);
- } else if (typeof value === 'string') {
- return getFileContext(value);
- } else {
- return value;
- }
- };
-
- return {
- ...(await importActual()),
- loadDynamic: vi.fn(loadDynamic),
- loadDynamicMap: vi.fn((map, context) =>
- Object.fromEntries(
- Object.entries({ ...map }).map(
- ([relativePath, value]): [string, any] => [
- relativePath,
- loadDynamic(value, context),
- ],
- ),
- ),
- ),
- };
-});
-
-describe('fn:resolveScripts', () => {
- it('combine script templates from presets', async () => {
- const scripts = await resolveScripts({
- graph: [
- {
- name: 'preset1',
- asset: {},
- nodes: [
- {
- name: 'preset11',
- asset: {
- scripts: '/path/to/script.yaml',
- },
- nodes: [],
- },
- {
- name: 'preset12',
- asset: {
- scripts: '/path/to/script.yaml',
- },
- nodes: [],
- },
- ],
- },
- {
- name: 'preset2',
- asset: {
- scripts: {
- task_from_preset2: 'cmd',
- },
- },
- nodes: [],
- },
- ],
- context: {
- target: {
- name: 'client',
- root: '/project',
- package: {},
- },
- custom: { preset: 'preset', config: {}, variable: {}, noSymlinks: [] },
- },
- });
-
- expect(scripts).toEqual({ task: 'cmd', task_from_preset2: 'cmd' });
- });
-});
diff --git a/packages/presetter/spec/content/resolveSupplementaryConfig.spec.ts b/packages/presetter/spec/content/resolveSupplementaryConfig.spec.ts
deleted file mode 100644
index eb0cefe6..00000000
--- a/packages/presetter/spec/content/resolveSupplementaryConfig.spec.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { describe, expect, it } from 'vitest';
-
-import { resolveSupplementaryConfig } from '#content';
-
-describe('fn:resolveSupplementaryConfig', () => {
- it('compute the final rules to overwrite those form the presets', async () => {
- expect(
- await resolveSupplementaryConfig({
- graph: [
- {
- name: 'preset1',
- asset: {
- supplementaryConfig: {
- config: { flag: true },
- },
- },
- nodes: [],
- },
- {
- name: 'preset2',
- asset: {
- supplementaryConfig: {
- config: { extra: true },
- },
- },
- nodes: [
- {
- name: 'preset3',
- asset: {
- supplementaryConfig: {
- config: { flag: false },
- },
- },
- nodes: [],
- },
- ],
- },
- ],
- context: {
- target: {
- name: 'client',
- root: '/project',
- package: {},
- },
- custom: {
- preset: 'preset',
- config: {
- config: { extra: false },
- },
- variable: {},
- },
- },
- }),
- ).toEqual({ config: { flag: false, extra: false } });
- });
-});
diff --git a/packages/presetter/spec/content/resolveSupplementaryScripts.spec.ts b/packages/presetter/spec/content/resolveSupplementaryScripts.spec.ts
deleted file mode 100644
index 15433710..00000000
--- a/packages/presetter/spec/content/resolveSupplementaryScripts.spec.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { describe, expect, it } from 'vitest';
-
-import { resolveSupplementaryScripts } from '#content';
-
-describe('fn:resolveSupplementaryScripts', () => {
- it('compute the final scripts to overwrite those form the presets', async () => {
- expect(
- await resolveSupplementaryScripts({
- graph: [
- {
- name: 'preset1',
- asset: {
- supplementaryScripts: {
- task1: 'replaced',
- },
- },
- nodes: [],
- },
- {
- name: 'preset2',
- asset: {
- supplementaryScripts: {
- task1: 'command1',
- },
- },
- nodes: [
- {
- name: 'preset3',
- asset: {
- supplementaryScripts: {
- task2: 'command2',
- },
- },
- nodes: [],
- },
- ],
- },
- ],
- context: {
- target: {
- name: 'client',
- root: '/project',
- package: {},
- },
- custom: {
- preset: 'preset',
- scripts: {
- task3: 'command3',
- },
- variable: {},
- },
- },
- }),
- ).toEqual({ task1: 'command1', task2: 'command2', task3: 'command3' });
- });
-});
diff --git a/packages/presetter/spec/content/resolveTemplate.spec.ts b/packages/presetter/spec/content/resolveTemplate.spec.ts
deleted file mode 100644
index a7a5aa4c..00000000
--- a/packages/presetter/spec/content/resolveTemplate.spec.ts
+++ /dev/null
@@ -1,209 +0,0 @@
-import { posix, relative, resolve, sep } from 'node:path';
-
-import { describe, expect, it, vi } from 'vitest';
-
-import { resolveTemplate } from '#content';
-
-import type { PresetGraph } from 'presetter-types';
-
-vi.mock('#resolution', async (importActual) => {
- const getFileContext = (path) => {
- // ensure that the paths below is compatible with windows
- const posixPath = relative(resolve('/'), path).split(sep).join(posix.sep);
- switch (posixPath) {
- case 'path/to/config.json':
- return { json: true, list: [0] };
- case 'path/to/config.yaml':
- return { yaml: true };
- case 'path/to/file1':
- return 'file1';
- case 'path/to/file2':
- return 'file2';
- case 'path/to/list1':
- return 'list1';
- case 'path/to/list2':
- return 'list2';
- default:
- throw new Error(`loadFile: missing path ${path}`);
- }
- };
-
- const loadDynamic = (value, context) => {
- if (typeof value === 'function') {
- return value(context);
- } else if (typeof value === 'string') {
- return getFileContext(value);
- } else {
- return value;
- }
- };
-
- return {
- ...(await importActual()),
- loadDynamic: vi.fn(loadDynamic),
- loadDynamicMap: vi.fn((map, context) =>
- Object.fromEntries(
- Object.entries({ ...map }).map(
- ([relativePath, value]): [string, any] => [
- relativePath,
- loadDynamic(value, context),
- ],
- ),
- ),
- ),
- };
-});
-
-const graph: PresetGraph = [
- {
- name: 'preset1',
- asset: {
- template: {
- '.config.json': '/path/to/config.json',
- '.list': '/path/to/list1',
- },
- supplementaryIgnores: () => ['.ignore'],
- },
- nodes: [
- {
- name: 'preset11',
- asset: {
- template: {
- '.config.json': { list: [1] },
- '.list': '/path/to/list1',
- 'general.file': '/path/to/file1',
- '.ignore': '/path/to/file1',
- },
- },
- nodes: [],
- },
- {
- name: 'preset12',
- asset: {
- template: {
- '.config.json': { list: [2] },
- '.list': '/path/to/list1',
- },
- },
- nodes: [],
- },
- ],
- },
- {
- name: 'preset2',
- asset: {
- template: {
- '.config.json': '/path/to/config.yaml',
- '.list': '/path/to/list2',
- 'general.file': '/path/to/file2',
- },
- },
- nodes: [],
- },
-];
-
-describe('fn:resolveTemplate', () => {
- it('give an empty object if no template is given at all', async () => {
- expect(
- await resolveTemplate({
- graph: [],
- context: {
- target: {
- name: 'client',
- root: '/project',
- package: {},
- },
- custom: {
- preset: 'preset',
- config: {},
- variable: {},
- noSymlinks: [],
- },
- },
- }),
- ).toEqual({});
- });
-
- it('merge config from all presets', async () => {
- expect(
- await resolveTemplate({
- graph,
- context: {
- target: {
- name: 'client',
- root: '/project',
- package: {},
- },
- custom: {
- preset: 'preset',
- config: {},
- variable: {},
- noSymlinks: [],
- },
- },
- }),
- ).toEqual({
- '.config.json': { json: true, yaml: true, list: [1, 2, 0] },
- '.list': 'list1\nlist2',
- 'general.file': 'file2',
- });
- });
-
- it('return a customized configuration', async () => {
- expect(
- await resolveTemplate({
- graph,
- context: {
- target: {
- name: 'client',
- root: '/project',
- package: {},
- },
- custom: {
- preset: 'preset',
- config: {
- config: { json: false, yaml: false, extra: true, list: [99] },
- list: ['list3'],
- },
- variable: {},
- noSymlinks: [],
- },
- },
- }),
- ).toEqual({
- '.config.json': {
- json: false,
- yaml: false,
- extra: true,
- list: [1, 2, 0, 99],
- },
- '.list': 'list1\nlist2\nlist3',
- 'general.file': 'file2',
- });
- });
-
- it('filter out unwanted items', async () => {
- expect(
- await resolveTemplate({
- graph,
- context: {
- target: {
- name: 'client',
- root: '/project',
- package: {},
- },
- custom: {
- preset: 'preset',
- config: {},
- variable: {},
- noSymlinks: [],
- ignores: ['general.file', { '.config.json': ['list'] }],
- },
- },
- }),
- ).toEqual({
- '.config.json': { json: true, yaml: true },
- '.list': 'list1\nlist2',
- });
- });
-});
diff --git a/packages/presetter/spec/content/resolveVariable.spec.ts b/packages/presetter/spec/content/resolveVariable.spec.ts
deleted file mode 100644
index db97767f..00000000
--- a/packages/presetter/spec/content/resolveVariable.spec.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { describe, expect, it } from 'vitest';
-
-import { resolveVariable } from '#content';
-
-describe('fn:resolveVariable', () => {
- it('compute the final variables', () => {
- expect(
- resolveVariable({
- graph: [
- {
- name: 'preset1',
- asset: {
- variable: {
- a: 'a',
- },
- },
- nodes: [],
- },
- {
- name: 'preset2',
- asset: {
- variable: {
- b: 'b',
- },
- },
- nodes: [
- {
- name: 'preset3',
- asset: {
- variable: {
- b: 'other',
- },
- },
- nodes: [],
- },
- ],
- },
- ],
- config: { preset: 'preset', variable: { c: 'c' } },
- }),
- ).toEqual({ a: 'a', b: 'b', c: 'c' });
- });
-});
diff --git a/packages/presetter/spec/directive/isApplyDirective.spec.ts b/packages/presetter/spec/directive/isApplyDirective.spec.ts
deleted file mode 100644
index c0f1dc81..00000000
--- a/packages/presetter/spec/directive/isApplyDirective.spec.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { describe, expect, it } from 'vitest';
-
-import { isApplyDirective } from '#directive';
-
-describe('fn:isApplyDirective', () => {
- it('return true for a simple package', () => {
- expect(isApplyDirective('@apply package')).toEqual(true);
- });
-
- it('return true for a package with special characters', () => {
- expect(isApplyDirective('@apply p_c-a.e')).toEqual(true);
- });
-
- it('return true for a scoped package', () => {
- expect(isApplyDirective('@apply @scope/package')).toEqual(true);
- });
-
- it('return true for a named import', () => {
- expect(isApplyDirective('@apply @scope/package[named]')).toEqual(true);
- });
-
- it('return true for sibling import', () => {
- expect(isApplyDirective('@apply ./sibling')).toEqual(true);
- });
-
- it('return true for parental import', () => {
- expect(isApplyDirective('@apply ../parent')).toEqual(true);
- });
-
- it('return false for missing named import', () => {
- expect(isApplyDirective('@apply missing_name[]')).toEqual(false);
- });
-
- it('return false for including trailing slash', () => {
- expect(isApplyDirective('@apply no_ending_slash/')).toEqual(false);
- });
-
- it('return false for an invalid scoped package name', () => {
- expect(isApplyDirective('@apply @@invalid_scope')).toEqual(false);
- });
-});
diff --git a/packages/presetter/spec/directive/isDirective.spec.ts b/packages/presetter/spec/directive/isDirective.spec.ts
deleted file mode 100644
index 74a3a878..00000000
--- a/packages/presetter/spec/directive/isDirective.spec.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { describe, expect, it } from 'vitest';
-
-import { isDirective } from '#directive';
-
-describe('fn:isDirective', () => {
- it('return true for valid directives', () => {
- const validDirectives = ['@import package', '@import package'];
- for (const directive of validDirectives) {
- expect(isDirective(directive)).toEqual(true);
- }
- });
-
- it('return false for invalid directives', () => {
- const invalidDirectives = [{}];
- for (const directive of invalidDirectives) {
- expect(isDirective(directive)).toEqual(false);
- }
- });
-});
diff --git a/packages/presetter/spec/directive/isImportDirective.spec.ts b/packages/presetter/spec/directive/isImportDirective.spec.ts
deleted file mode 100644
index 99fe8e7b..00000000
--- a/packages/presetter/spec/directive/isImportDirective.spec.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { describe, expect, it } from 'vitest';
-
-import { isImportDirective } from '#directive';
-
-describe('fn:isImportDirective', () => {
- it('return true for a simple package', () => {
- expect(isImportDirective('@import package')).toEqual(true);
- });
-
- it('return true for a package with special characters', () => {
- expect(isImportDirective('@import p_c-a.e')).toEqual(true);
- });
-
- it('return true for a scoped package', () => {
- expect(isImportDirective('@import @scope/package')).toEqual(true);
- });
-
- it('return true for a named import', () => {
- expect(isImportDirective('@import @scope/package[named]')).toEqual(true);
- });
-
- it('return true for sibling import', () => {
- expect(isImportDirective('@import ./sibling')).toEqual(true);
- });
-
- it('return true for parental import', () => {
- expect(isImportDirective('@import ../parent')).toEqual(true);
- });
-
- it('return false for missing named import', () => {
- expect(isImportDirective('@import missing_name[]')).toEqual(false);
- });
-
- it('return false for including trailing slash', () => {
- expect(isImportDirective('@import no_ending_slash/')).toEqual(false);
- });
-
- it('return false for an invalid scoped package name', () => {
- expect(isImportDirective('@import @@invalid_scope')).toEqual(false);
- });
-});
diff --git a/packages/presetter/spec/directive/resolveDirective.spec.ts b/packages/presetter/spec/directive/resolveDirective.spec.ts
deleted file mode 100644
index a5c3c7dd..00000000
--- a/packages/presetter/spec/directive/resolveDirective.spec.ts
+++ /dev/null
@@ -1,168 +0,0 @@
-import { describe, expect, it } from 'vitest';
-
-import { resolveDirective } from '#directive';
-
-import type { PresetContext } from 'presetter-types';
-
-describe('fn:resolveDirective', () => {
- const context: PresetContext = {
- target: {
- name: 'target',
- root: '/path/to/target',
- package: {},
- },
- custom: {
- preset: 'preset',
- },
- };
-
- it('export a stringified JSON if no directive is involved', () => {
- expect(
- resolveDirective(
- {
- boolean: true,
- number: 0,
- string: 'string',
- array: [0, 1],
- },
- context,
- ),
- ).toEqual({
- importMap: {},
- stringifiedConfig:
- '{"boolean": true, "number": 0, "string": "string", "array": [0, 1]}',
- });
- });
-
- it('replace content via the import directive', () => {
- expect(resolveDirective({ imported: '@import another' }, context)).toEqual({
- importMap: { another: 'import0' },
- stringifiedConfig: '{"imported": import0}',
- });
- });
-
- it('replace content with a named import', () => {
- expect(
- resolveDirective({ imported: '@import another[name]' }, context),
- ).toEqual({
- importMap: { another: 'import0' },
- stringifiedConfig: '{"imported": import0.name}',
- });
- });
-
- it('replace content with a function call via the apply directive', () => {
- expect(resolveDirective({ applied: ['@apply another'] }, context)).toEqual({
- importMap: { another: 'import0' },
- stringifiedConfig: '{"applied": import0(...([] as const))}',
- });
- });
-
- it('replace content with a named function call via the apply directive', () => {
- expect(
- resolveDirective({ applied: ['@apply another[named]'] }, context),
- ).toEqual({
- importMap: { another: 'import0' },
- stringifiedConfig: '{"applied": import0.named(...([] as const))}',
- });
- });
-
- it('replace content with a function call with options via the apply directive', () => {
- expect(
- resolveDirective(
- { applied: ['@apply another', { options: null }] },
- context,
- ),
- ).toEqual({
- importMap: { another: 'import0' },
- stringifiedConfig:
- '{"applied": import0(...([{"options": null}] as const))}',
- });
- });
-
- it('does not do anything with an ordinary array', () => {
- expect(resolveDirective({ array: [['item0', 'item1']] }, context)).toEqual({
- importMap: {},
- stringifiedConfig: '{"array": [["item0", "item1"]]}',
- });
- });
-
- it('does not do anything with the apply directive if it is not in the array form', () => {
- expect(resolveDirective({ applied: '@apply another' }, context)).toEqual({
- importMap: {},
- stringifiedConfig: '{"applied": "@apply another"}',
- });
- });
-
- it('handle the apply directive together with the import directive as the argument', () => {
- expect(
- resolveDirective(
- { applied: ['@apply another', '@import options[name]'] },
- context,
- ),
- ).toEqual({
- importMap: { options: 'import0', another: 'import1' },
- stringifiedConfig: '{"applied": import1(...([import0.name] as const))}',
- });
- });
-
- it('handle the apply directive together with the import directive as part of the argument', () => {
- expect(
- resolveDirective(
- {
- applied: [
- '@apply another',
- { extra: true, name: '@import options[name]' },
- ],
- },
- context,
- ),
- ).toEqual({
- importMap: { options: 'import0', another: 'import1' },
- stringifiedConfig:
- '{"applied": import1(...([{"extra": true, "name": import0.name}] as const))}',
- });
- });
-
- it('always generate an unique import map despite multiple usage', () => {
- expect(
- resolveDirective(
- {
- apply0: ['@apply import0', '@import options0[name]'],
- apply1: ['@apply import1', '@import options0[name]'],
- apply2: ['@apply import0', '@import options1[name]'],
- apply3: ['@apply import1', '@import options1[name]'],
- },
- context,
- ),
- ).toEqual({
- importMap: {
- import0: 'import1',
- import1: 'import2',
- options0: 'import0',
- options1: 'import3',
- },
- stringifiedConfig:
- '{"apply0": import1(...([import0.name] as const)), "apply1": import2(...([import0.name] as const)), "apply2": import1(...([import3.name] as const)), "apply3": import2(...([import3.name] as const))}',
- });
- });
-
- it('handle mixed use cases', () => {
- expect(
- resolveDirective(
- {
- extra: true,
- applied: [
- '@apply another',
- { extra: true, name: '@import options[name]' },
- ],
- imported: '@import another[name]',
- },
- context,
- ),
- ).toEqual({
- importMap: { options: 'import0', another: 'import1' },
- stringifiedConfig:
- '{"extra": true, "applied": import1(...([{"extra": true, "name": import0.name}] as const)), "imported": import1.name}',
- });
- });
-});
diff --git a/packages/presetter/spec/error/wrap.spec.ts b/packages/presetter/spec/error/wrap.spec.ts
deleted file mode 100644
index 53ef88da..00000000
--- a/packages/presetter/spec/error/wrap.spec.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { describe, expect, it, vi } from 'vitest';
-
-import { wrap } from '#error';
-
-describe('fn:wrap', () => {
- it('return what a promise resolves', async () => {
- const fn = vi.fn(async () => true);
- expect(await wrap(fn(), 'message')).toEqual(true);
- });
-
- it('prefix an additional message to a promise rejection', async () => {
- const fn = vi.fn(async () => {
- throw new Error('error');
- });
- await expect(wrap(fn(), 'better explanation')).rejects.toThrow(
- 'better explanation: error',
- );
- });
-
- it('does not change anything if a promise rejection is not an error', async () => {
- const fn = vi.fn(async () => {
- throw 'message';
- });
- await expect(wrap(fn(), 'additional message')).rejects.toEqual('message');
- });
-});
diff --git a/packages/presetter/spec/executable/entry.spec.ts b/packages/presetter/spec/executable/entry.spec.ts
index e4ddbbf7..201cc024 100644
--- a/packages/presetter/spec/executable/entry.spec.ts
+++ b/packages/presetter/spec/executable/entry.spec.ts
@@ -1,8 +1,8 @@
-import { describe, expect, it, vi } from 'vitest';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
import { entry } from '#executable/entry';
import { reifyDependencies } from '#package';
-import { bootstrapPreset, setupPreset, unsetPreset } from '#preset';
+import { bootstrap } from '#preset';
import { run } from '#run';
vi.mock('node:fs', () => ({
@@ -20,9 +20,7 @@ vi.mock('#package', () => ({
}));
vi.mock('#preset', () => ({
- bootstrapPreset: vi.fn(),
- setupPreset: vi.fn(),
- unsetPreset: vi.fn(),
+ bootstrap: vi.fn(),
}));
vi.mock('#run', () => ({
@@ -30,43 +28,25 @@ vi.mock('#run', () => ({
}));
describe('fn:entry', () => {
- describe('use', () => {
- it('use preset', async () => {
- await entry(['use', 'preset']);
-
- expect(setupPreset).toBeCalledWith('preset');
- });
-
- it('take multiple presets', async () => {
- await entry(['use', 'preset1', 'preset2']);
-
- expect(setupPreset).toBeCalledWith('preset1', 'preset2');
- });
- });
+ beforeEach(() => vi.clearAllMocks());
describe('bootstrap', () => {
- it('bootstrap by default', async () => {
+ it('should bootstrap by default', async () => {
await entry(['bootstrap']);
- expect(bootstrapPreset).toBeCalledWith({ force: false });
+ expect(bootstrap).toHaveBeenCalled();
});
- it('bootstrap if the specified file exists', async () => {
+ it('should bootstrap if the specified file exists', async () => {
await entry(['bootstrap', '--only', 'exist']);
- expect(bootstrapPreset).toBeCalledWith({ force: false });
+ expect(bootstrap).toHaveBeenCalled();
});
- it('bootstrap with force', async () => {
- await entry(['bootstrap', '--force']);
-
- expect(bootstrapPreset).toBeCalledWith({ force: true });
- });
-
- it('skip bootstrap if the specified file is missing', async () => {
+ it('should skip bootstrap if the specified file is missing', async () => {
await entry(['bootstrap', '--only', 'no-such-file']);
- expect(bootstrapPreset).not.toBeCalled();
+ expect(bootstrap).not.toHaveBeenCalled();
});
});
@@ -74,7 +54,7 @@ describe('fn:entry', () => {
it('should run a single task with provided arguments', async () => {
await entry(['run', 'task', '--', '"arg 1"', "'arg 2'"]);
- expect(run).toBeCalledWith(
+ expect(run).toHaveBeenCalledWith(
[
{
selector: 'task',
@@ -90,7 +70,7 @@ describe('fn:entry', () => {
it('should run a single task without arguments', async () => {
await entry(['run-s', 'task', '--', 'arg-1', '--arg-2']);
- expect(run).toBeCalledWith([
+ expect(run).toHaveBeenCalledWith([
{
selector: 'task',
args: [],
@@ -101,7 +81,7 @@ describe('fn:entry', () => {
it('should run multiple tasks without arguments', async () => {
await entry(['run-s', 'task1', 'task2', '--', 'arg-1', '--arg-2']);
- expect(run).toBeCalledWith([
+ expect(run).toHaveBeenCalledWith([
{
selector: 'task1',
args: [],
@@ -123,7 +103,7 @@ describe('fn:entry', () => {
'--arg-7',
]);
- expect(run).toBeCalledWith([
+ expect(run).toHaveBeenCalledWith([
{
selector: 'task1',
args: ['arg-1', '--arg-2', 'arg 3'],
@@ -140,7 +120,7 @@ describe('fn:entry', () => {
it('should run multiple tasks in parallel', async () => {
await entry(['run-p', 'task1', 'task2']);
- expect(run).toBeCalledWith(
+ expect(run).toHaveBeenCalledWith(
[
{
selector: 'task1',
@@ -156,20 +136,11 @@ describe('fn:entry', () => {
});
});
- describe('unset', () => {
- it('unset', async () => {
- await entry(['unset']);
-
- expect(unsetPreset).toBeCalledWith();
- });
- });
-
- it('does not do anything if the command cannot be recognized', async () => {
+ it('should not do anything if the command cannot be recognized', async () => {
await entry(['unknown', '--arg-1']);
- expect(reifyDependencies).toBeCalledTimes(0);
- expect(bootstrapPreset).toBeCalledTimes(0);
- expect(unsetPreset).toBeCalledTimes(0);
- expect(run).toBeCalledTimes(0);
+ expect(reifyDependencies).toHaveBeenCalledTimes(0);
+ expect(bootstrap).toHaveBeenCalledTimes(0);
+ expect(run).toHaveBeenCalledTimes(0);
});
});
diff --git a/packages/presetter/spec/executable/error.spec.ts b/packages/presetter/spec/executable/error.spec.ts
index 3c89a297..4aa56815 100644
--- a/packages/presetter/spec/executable/error.spec.ts
+++ b/packages/presetter/spec/executable/error.spec.ts
@@ -1,4 +1,4 @@
-import { describe, expect, it, vi } from 'vitest';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
import { handleError } from '#executable/error';
@@ -17,6 +17,8 @@ const ansiPattern = [
const ansi = new RegExp(ansiPattern, 'g');
describe.sequential('fn:handleError', () => {
+ beforeEach(() => vi.clearAllMocks());
+
it('should print the error if there is no tty', async () => {
process.stdout.isTTY = false;
diff --git a/packages/presetter/spec/io/ensureFile.spec.ts b/packages/presetter/spec/io/ensureFile.spec.ts
new file mode 100644
index 00000000..f23fa4c3
--- /dev/null
+++ b/packages/presetter/spec/io/ensureFile.spec.ts
@@ -0,0 +1,33 @@
+import { mkdirSync, writeFileSync } from 'node:fs';
+
+import { describe, expect, it, vi } from 'vitest';
+
+import { ensureFile } from '#io';
+
+vi.mock('node:fs', () => ({
+ mkdirSync: vi.fn(),
+ writeFileSync: vi.fn(),
+}));
+
+describe('fn:ensureFile', () => {
+ it('should create a file with the given content', () => {
+ ensureFile('/path/to/file.txt', 'file content');
+
+ expect(mkdirSync).toHaveBeenCalledWith('/path/to', { recursive: true });
+ expect(writeFileSync).toHaveBeenCalledWith(
+ '/path/to/file.txt',
+ 'file content',
+ );
+ });
+
+ it('should create a file with buffer content', () => {
+ const bufferContent = Buffer.from('buffer content');
+ ensureFile('/path/to/file.txt', bufferContent);
+
+ expect(mkdirSync).toHaveBeenCalledWith('/path/to', { recursive: true });
+ expect(writeFileSync).toHaveBeenCalledWith(
+ '/path/to/file.txt',
+ bufferContent,
+ );
+ });
+});
diff --git a/packages/presetter/spec/io/linkFiiles.spec.ts b/packages/presetter/spec/io/linkFiiles.spec.ts
deleted file mode 100644
index 9394425c..00000000
--- a/packages/presetter/spec/io/linkFiiles.spec.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-import { mkdirSync, symlinkSync, unlinkSync } from 'node:fs';
-import { posix, relative, resolve, sep } from 'node:path';
-
-import { describe, expect, it, vi } from 'vitest';
-
-import { linkFiles } from '#io';
-
-import type { Stats } from 'node:fs';
-
-vi.mock('node:console', () => ({
- info: vi.fn(),
-}));
-
-vi.mock('node:fs', async (importActual) => ({
- ...(await importActual()),
- lstatSync: vi.fn(
- (
- path: string,
- options: { throwIfNoEntry: boolean },
- ): Partial | void => {
- const { throwIfNoEntry } = options;
-
- // ensure that the paths below is compatible with windows
- const posixPath = relative(resolve('/'), path).split(sep).join(posix.sep);
- switch (posixPath) {
- case 'path/to/config.json':
- case 'project/old/link/rewritten/by/user':
- case 'relative/path/to/config':
- return { isSymbolicLink: () => false };
- case 'project/old/link/by/presetter':
- return { isSymbolicLink: () => true };
-
- default:
- if (throwIfNoEntry) {
- throw new Error();
- }
- }
- },
- ),
- statSync: vi.fn(
- (
- path: string,
- options: { throwIfNoEntry: boolean },
- ): Partial | void => {
- const { throwIfNoEntry } = options;
-
- // ensure that the paths below is compatible with windows
- const posixPath = relative(resolve('/'), path).split(sep).join(posix.sep);
- switch (posixPath) {
- case 'project/old/link/rewritten/by/user':
- return { nlink: 1 };
- case 'project/old/link/by/presetter':
- return { nlink: 2 };
- default:
- if (throwIfNoEntry) {
- throw new Error();
- }
- }
- },
- ),
- readlinkSync: vi.fn(() => ''),
- mkdirSync: vi.fn(),
- symlinkSync: vi.fn(),
- unlinkSync: vi.fn(),
-}));
-
-describe('fn:linkFiles', () => {
- it('link generated files to the project', async () => {
- await linkFiles('/project', {
- 'old/link/rewritten/by/user': resolve('/project/relative/path/to/config'),
- 'old/link/by/presetter': resolve('/project/relative/path/to/config'),
- 'new/link/config.json': resolve('/project/relative/path/to/config'),
- });
-
- expect(mkdirSync).toHaveBeenCalledTimes(2);
- expect(mkdirSync).toHaveBeenCalledWith(resolve('/project/old/link/by'), {
- recursive: true,
- });
- expect(mkdirSync).toHaveBeenCalledWith(resolve('/project/new/link'), {
- recursive: true,
- });
- expect(unlinkSync).toHaveBeenCalledTimes(1);
- expect(unlinkSync).toHaveBeenCalledWith(
- resolve('/project/old/link/by/presetter'),
- );
- expect(symlinkSync).toHaveBeenCalledTimes(2);
- expect(symlinkSync).toHaveBeenCalledWith(
- '../../../relative/path/to/config'.split(posix.sep).join(sep),
- resolve('/project/old/link/by/presetter'),
- );
- expect(symlinkSync).toHaveBeenCalledWith(
- '../../relative/path/to/config'.split(posix.sep).join(sep),
- resolve('/project/new/link/config.json'),
- );
- });
-
- it('ignore any configuration that are written on the project root', async () => {
- await linkFiles('/project', {
- 'on/project/root': resolve('/project/on/project/root'),
- });
- expect(mkdirSync).toHaveBeenCalledTimes(0);
- expect(symlinkSync).toHaveBeenCalledTimes(0);
- });
-
- it('replace existing files if force is set', async () => {
- await linkFiles(
- '/',
- {
- 'path/to/config.json': resolve('/relative/path/to/config'),
- },
- { force: true },
- );
-
- expect(mkdirSync).toHaveBeenCalledTimes(1);
- expect(mkdirSync).toHaveBeenCalledWith(resolve('/path/to'), {
- recursive: true,
- });
-
- expect(unlinkSync).toHaveBeenCalledTimes(1);
-
- expect(symlinkSync).toHaveBeenCalledTimes(1);
- expect(symlinkSync).toHaveBeenCalledWith(
- '../../relative/path/to/config'.split(posix.sep).join(sep),
- resolve('/path/to/config.json'),
- );
- });
-});
diff --git a/packages/presetter/spec/io/loadFile.spec.ts b/packages/presetter/spec/io/loadFile.spec.ts
index a18c0ee1..ce76964d 100644
--- a/packages/presetter/spec/io/loadFile.spec.ts
+++ b/packages/presetter/spec/io/loadFile.spec.ts
@@ -1,50 +1,64 @@
-import { posix, relative, resolve, sep } from 'node:path';
+import { existsSync, readFileSync } from 'node:fs';
import { describe, expect, it, vi } from 'vitest';
import { loadFile } from '#io';
-vi.mock('node:fs', async (importActual) => ({
- ...(await importActual()),
- readFileSync: vi.fn((path: string) => {
- // ensure that the paths below is compatible with windows
- const posixPath = relative(resolve('/'), path).split(sep).join(posix.sep);
- switch (posixPath) {
- case 'path/to/config.json':
- return Buffer.from('{ "json": true }');
- case 'path/to/config.yaml':
- case 'path/to/config.yml':
- return Buffer.from('yaml: true');
- case 'path/to/text':
- return Buffer.from('{"text": true}');
- default:
- throw new Error(`readFile: missing ${path}`);
- }
- }),
+vi.mock('node:fs', () => ({
+ existsSync: vi.fn(),
+ readFileSync: vi.fn(),
}));
describe('fn:loadFile', () => {
- it('load a json file', async () => {
- expect(await loadFile('/path/to/config.json')).toEqual({
- json: true,
- });
+ it('should load a json file', () => {
+ vi.mocked(existsSync).mockReturnValue(true);
+ vi.mocked(readFileSync).mockReturnValue(Buffer.from('{ "json": true }'));
+
+ const result = loadFile('/path/to/config.json');
+ const expected = { json: true };
+
+ expect(result).toEqual(expected);
});
- it('load a yaml file', async () => {
- expect(await loadFile('/path/to/config.yaml')).toEqual({
- yaml: true,
- });
+ it('should load a yaml file', () => {
+ vi.mocked(existsSync).mockReturnValue(true);
+ vi.mocked(readFileSync).mockReturnValue(Buffer.from('yaml: true'));
+
+ const result = loadFile('/path/to/config.yaml');
+ const expected = { yaml: true };
- expect(await loadFile('/path/to/config.yml')).toEqual({
- yaml: true,
- });
+ expect(result).toEqual(expected);
});
- it('load a text file', async () => {
- expect(await loadFile('/path/to/text')).toEqual('{"text": true}');
+ it('should load an ignore file', () => {
+ vi.mocked(existsSync).mockReturnValue(true);
+ vi.mocked(readFileSync).mockReturnValue(
+ Buffer.from('line1\nline2\n#comment\nline3'),
+ );
+
+ const result = loadFile('/path/to/file.ignore');
+ const expected = ['line1', 'line2', 'line3'];
+
+ expect(result).toEqual(expected);
});
- it('load a text file but assume it as a json', async () => {
- expect(await loadFile('/path/to/text', 'json')).toEqual({ text: true });
+ it('should load an arbitrary file', () => {
+ const buffer = Buffer.from('file content');
+
+ vi.mocked(existsSync).mockReturnValue(true);
+ vi.mocked(readFileSync).mockReturnValue(buffer);
+
+ const result = loadFile('/path/to/file.txt');
+ const expected = buffer;
+
+ expect(result).toEqual(expected);
+ });
+
+ it('should throw an error if the file does not exist', () => {
+ vi.mocked(existsSync).mockReturnValue(false);
+
+ expect(() => loadFile('/path/to/missing.file')).toThrow(
+ 'file not found: /path/to/missing.file',
+ );
});
});
diff --git a/packages/presetter/spec/io/serializeContent.spec.ts b/packages/presetter/spec/io/serializeContent.spec.ts
deleted file mode 100644
index c66e9646..00000000
--- a/packages/presetter/spec/io/serializeContent.spec.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { describe, expect, it } from 'vitest';
-
-import { serializeContent } from '#io';
-
-describe('fn:serializeContent', () => {
- it('treat string as a string', () => {
- expect(serializeContent('/path/to/config.json', '{ "json": true }'));
- });
-
- it('convert an object to a json format', () => {
- expect(serializeContent('/path/to/config.json', { json: true })).toEqual(
- '{\n "json": true\n}',
- );
- });
-
- it('convert an object to a yaml format', () => {
- expect(serializeContent('/path/to/config.yaml', { yaml: true })).toEqual(
- 'yaml: true\n',
- );
- });
-});
diff --git a/packages/presetter/spec/io/unlinkFiles.spec.ts b/packages/presetter/spec/io/unlinkFiles.spec.ts
deleted file mode 100644
index 3dafce9d..00000000
--- a/packages/presetter/spec/io/unlinkFiles.spec.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import { info } from 'node:console';
-import { unlinkSync } from 'node:fs';
-import { posix, relative, resolve, sep } from 'node:path';
-
-import { describe, expect, it, vi } from 'vitest';
-
-import { unlinkFiles } from '#io';
-
-import type { Stats } from 'node:fs';
-
-vi.mock('node:console', () => ({
- info: vi.fn(),
-}));
-
-vi.mock('node:fs', async (importActual) => ({
- ...(await importActual()),
- lstatSync: vi.fn(
- (
- path: string,
- options: { throwIfNoEntry: boolean },
- ): Partial | void => {
- const { throwIfNoEntry } = options;
-
- // ensure that the paths below is compatible with windows
- const posixPath = relative(resolve('/'), path).split(sep).join(posix.sep);
- switch (posixPath) {
- case 'path/to/config.json':
- case 'path/to/config.yaml':
- case 'path/to/list1':
- case 'path/to/list2':
- case 'project/old/symlink/rewritten/by/user':
- return { isSymbolicLink: () => false };
- case 'project/old/symlink/by/presetter':
- case 'project/old/symlink/pointed/to/other':
- return { isSymbolicLink: () => true };
- default:
- if (throwIfNoEntry) {
- throw new Error();
- }
- }
- },
- ),
- readlinkSync: vi.fn((path: string): string => {
- // ensure that the paths below is compatible with windows
- const posixPath = relative(resolve('/'), path).split(sep).join(posix.sep);
- switch (posixPath) {
- case 'project/old/symlink/by/presetter':
- return 'relative/path/to/config'.split(posix.sep).join(sep);
- case 'project/old/symlink/pointed/to/other':
- return 'other/path'.split(posix.sep).join(sep);
- default:
- throw new Error();
- }
- }),
- unlinkSync: vi.fn(),
-}));
-
-describe('fn:unlinkFiles', () => {
- it('clean up any artifacts installed on the project root', async () => {
- await unlinkFiles('/project', {
- 'old/symlink/by/presetter': resolve('/project/relative/path/to/config'),
- 'old/symlink/pointed/to/other': resolve(
- '/project/relative/path/to/config',
- ),
- 'old/symlink/rewritten/by/user': resolve(
- '/project/relative/path/to/config',
- ),
- });
-
- expect(info).toHaveBeenCalledTimes(3);
- expect(info).toHaveBeenCalledWith('Removing old/symlink/by/presetter');
- expect(info).toHaveBeenCalledWith('Skipping old/symlink/pointed/to/other');
- expect(info).toHaveBeenCalledWith('Skipping old/symlink/rewritten/by/user');
- expect(unlinkSync).toHaveBeenCalledTimes(1);
- expect(unlinkSync).toHaveBeenCalledWith(
- resolve('/project/old/symlink/by/presetter'),
- );
- });
-
- it('ignore any configuration that are written on the project root', async () => {
- await unlinkFiles('/project', {
- 'on/project/root': resolve('/project/on/project/root'),
- });
- expect(info).toHaveBeenCalledTimes(1);
- expect(info).toHaveBeenCalledWith('Skipping on/project/root');
- expect(unlinkSync).toHaveBeenCalledTimes(0);
- });
-});
diff --git a/packages/presetter/spec/io/writeFiles.spec.ts b/packages/presetter/spec/io/writeFiles.spec.ts
deleted file mode 100644
index 2cf5290d..00000000
--- a/packages/presetter/spec/io/writeFiles.spec.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import { mkdirSync, writeFileSync } from 'node:fs';
-import { posix, relative, resolve, sep } from 'node:path';
-
-import { describe, expect, it, vi } from 'vitest';
-
-import { writeFiles } from '#io';
-
-import type { Stats } from 'node:fs';
-
-vi.mock('node:console', () => ({
- info: vi.fn(),
-}));
-
-vi.mock('node:fs', async (importActual) => ({
- ...(await importActual()),
- lstatSync: vi.fn(
- (
- path: string,
- options: { throwIfNoEntry: boolean },
- ): Partial | void => {
- const { throwIfNoEntry } = options;
-
- // ensure that the paths below is compatible with windows
- const posixPath = relative(resolve('/'), path).split(sep).join(posix.sep);
- switch (posixPath) {
- case 'path/to/config.yaml':
- return { isSymbolicLink: () => false };
- default:
- if (throwIfNoEntry) {
- throw new Error();
- }
- }
- },
- ),
- mkdirSync: vi.fn(),
- unlinkSync: vi.fn(),
- writeFileSync: vi.fn(),
-}));
-
-describe('fn:writeFiles', () => {
- it('write content to the disk if the destination path does not exist', async () => {
- await writeFiles(
- '/path/to',
- { 'new.yaml': { yaml: true } },
- { 'new.yaml': '/path/to/new.yaml' },
- );
-
- expect(mkdirSync).toBeCalledWith('/path/to', { recursive: true });
- expect(writeFileSync).toBeCalledWith('/path/to/new.yaml', 'yaml: true\n');
- });
-
- it('write content to the disk for symlinked templates', async () => {
- await writeFiles(
- '/path/to',
- { 'config.yaml': { yaml: true } },
- { 'config.yaml': '/config.yaml' },
- );
-
- expect(mkdirSync).toBeCalledWith('/', { recursive: true });
- expect(writeFileSync).toBeCalledWith('/config.yaml', 'yaml: true\n');
- });
-
- it('skip content to the disk if the destination already exist on the project root', async () => {
- await writeFiles(
- '/path/to',
- { 'config.yaml': { yaml: true } },
- { 'config.yaml': resolve('/path/to/config.yaml') },
- );
-
- expect(mkdirSync).toBeCalledTimes(0);
- expect(writeFileSync).toBeCalledTimes(0);
- });
-
- it('write content to the disk if force is set', async () => {
- await writeFiles(
- '/path/to',
- { 'config.yaml': { yaml: true } },
- { 'config.yaml': '/path/to/config.yaml' },
- { force: true },
- );
-
- expect(mkdirSync).toBeCalledWith('/path/to', { recursive: true });
- expect(writeFileSync).toBeCalledWith(
- '/path/to/config.yaml',
- 'yaml: true\n',
- );
- });
-});
diff --git a/packages/presetter/spec/preset/bootstrap.spec.ts b/packages/presetter/spec/preset/bootstrap.spec.ts
new file mode 100644
index 00000000..c0ad1b53
--- /dev/null
+++ b/packages/presetter/spec/preset/bootstrap.spec.ts
@@ -0,0 +1,108 @@
+import { resolve } from 'node:path';
+
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { ensureFile } from '#io';
+import { arePeerPackagesAutoInstalled, reifyDependencies } from '#package';
+
+import { bootstrap } from '#preset/bootstrap';
+import { resolveAssets } from '#preset/resolution';
+
+vi.mock(
+ 'node:console',
+ () =>
+ ({
+ info: vi.fn(), // disable console output
+ }) satisfies Partial,
+);
+
+vi.mock(
+ '#io',
+ async () =>
+ ({
+ ensureFile: vi.fn(),
+ }) satisfies Partial,
+);
+
+vi.mock(
+ '#preset/context',
+ () =>
+ ({
+ getContext: vi.fn(async () => ({
+ root: resolve('/path/to/project'),
+ package: {},
+ })),
+ }) satisfies Partial,
+);
+
+vi.mock(
+ '#preset/project',
+ () =>
+ ({
+ resolveProjectPreset: vi.fn(async () => ({
+ definition: { id: 'test-preset' },
+ nodes: [],
+ })),
+ }) satisfies Partial,
+);
+
+vi.mock(
+ '#preset/resolution',
+ () =>
+ ({
+ resolveAssets: vi.fn(async () => ({})),
+ }) satisfies Partial,
+);
+
+vi.mock('#package', () => ({
+ arePeerPackagesAutoInstalled: vi.fn(),
+ reifyDependencies: vi.fn(
+ async ({ add }: Parameters[0]) =>
+ add?.map((name) => ({ name, version: '*' })),
+ ),
+}));
+
+describe('fn:bootstrap', () => {
+ beforeEach(() => vi.clearAllMocks());
+
+ it('should generate files from templates and place them to the target project root', async () => {
+ vi.mocked(resolveAssets).mockResolvedValue({
+ 'config.json': {
+ foo: 'bar',
+ },
+ });
+
+ await bootstrap();
+
+ expect(vi.mocked(ensureFile)).toHaveBeenCalledWith(
+ resolve('/path/to/project/config.json'),
+ JSON.stringify({ foo: 'bar' }, null, 2),
+ );
+ });
+
+ it('should skip generating files if the asset is marked as ignored (null)', async () => {
+ vi.mocked(resolveAssets).mockResolvedValue({
+ 'config.json': null,
+ });
+
+ await bootstrap();
+
+ expect(vi.mocked(ensureFile)).not.toHaveBeenCalled();
+ });
+
+ it('should install packages specified by the preset if peer packages are not automatically installed by the package manager', async () => {
+ vi.mocked(arePeerPackagesAutoInstalled).mockReturnValue(false);
+
+ await bootstrap();
+
+ expect(reifyDependencies).toHaveBeenCalledTimes(1);
+ });
+
+ it('should skip installing peer packages manually if auto peers install is supported by package manager', async () => {
+ vi.mocked(arePeerPackagesAutoInstalled).mockReturnValue(true);
+
+ await bootstrap();
+
+ expect(reifyDependencies).not.toHaveBeenCalled();
+ });
+});
diff --git a/packages/presetter/spec/preset/config/resolve.spec.ts b/packages/presetter/spec/preset/config/resolve.spec.ts
new file mode 100644
index 00000000..27812b68
--- /dev/null
+++ b/packages/presetter/spec/preset/config/resolve.spec.ts
@@ -0,0 +1,66 @@
+import { createJiti } from 'jiti';
+import { describe, expect, it, vi } from 'vitest';
+
+import { resolvePresetterConfig } from '#preset/config/resolve';
+
+import type { Jiti } from 'jiti';
+
+vi.mock(
+ 'jiti',
+ () =>
+ ({
+ createJiti: vi.fn(
+ () =>
+ ({
+ import: vi.fn(async () => ({ id: 'test-preset' })),
+ }) as Partial as Jiti,
+ ),
+ }) satisfies Partial,
+);
+
+vi.mock(
+ '#preset/config/search',
+ () =>
+ ({
+ searchPresetterConfigs: vi.fn(async (base: string) =>
+ base === '/missing/config'
+ ? []
+ : [
+ '/path/to/project/presetter.config.ts',
+ '/path/to/project/presetter.config.js',
+ ],
+ ),
+ }) as Partial,
+);
+
+describe('fn:resolvePresetterConfig', () => {
+ it('resolves the presetter configuration from the project root', async () => {
+ const root = '/path/to/project';
+
+ const result = await resolvePresetterConfig(root);
+ const expected = { id: 'test-preset' };
+
+ expect(result).toEqual(expected);
+ expect(vi.mocked(createJiti)).toHaveBeenCalledWith(root, {
+ debug: false,
+ });
+ });
+
+ it('makes jiti debuggable if the DEBUG environment variable includes "presetter"', async () => {
+ vi.stubEnv('DEBUG', 'presetter');
+
+ const root = '/path/to/project';
+
+ const result = await resolvePresetterConfig(root);
+ const expected = { id: 'test-preset' };
+
+ expect(result).toEqual(expected);
+ expect(vi.mocked(createJiti)).toHaveBeenCalledWith(root, {
+ debug: true,
+ });
+ });
+
+ it('throws an error if no configuration file is found', async () => {
+ await expect(resolvePresetterConfig('/missing/config')).rejects.toThrow();
+ });
+});
diff --git a/packages/presetter/spec/preset/config/search.spec.ts b/packages/presetter/spec/preset/config/search.spec.ts
new file mode 100644
index 00000000..2157bde3
--- /dev/null
+++ b/packages/presetter/spec/preset/config/search.spec.ts
@@ -0,0 +1,73 @@
+import { resolve } from 'node:path';
+
+import { describe, expect, it, vi } from 'vitest';
+
+import { searchPresetterConfigs } from '#preset/config/search';
+
+import type { Options } from 'read-pkg-up';
+
+vi.mock(
+ 'node:fs',
+ () =>
+ ({
+ existsSync: vi.fn((path) =>
+ [
+ '/path/to/project/presetter.config.mts',
+ '/monorepo/presetter.config.mjs',
+ '/monorepo/packages/presetter.config.ts',
+ '/monorepo/packages/package1/presetter.config.ts',
+ '/monorepo/packages/package2/presetter.config.mts',
+ ]
+ .map((file) => resolve(file))
+ .includes(path),
+ ),
+ }) satisfies Partial,
+);
+
+vi.mock(
+ 'read-pkg-up',
+ () =>
+ ({
+ readPackageUp: vi.fn(async (options?: Options) => {
+ if ((options?.cwd as string).includes('/monorepo')) {
+ return {
+ path: resolve('/monorepo/package.json'),
+ packageJson: {
+ name: 'monorepo',
+ version: '1.0.0',
+ readme: 'README.md',
+ _id: 'id',
+ },
+ };
+ }
+
+ return undefined;
+ }),
+ }) satisfies Partial,
+);
+
+describe('fn:searchPresetterConfigs', () => {
+ it('returns configuration files from the base directory', async () => {
+ const base = '/path/to/project';
+
+ const result = await searchPresetterConfigs(base);
+ const expected = [resolve(base, 'presetter.config.mts')];
+
+ expect(result).toEqual(expected);
+ });
+
+ it('returns configuration files from the base directory and its parent', async () => {
+ const monorepoBase = '/monorepo';
+ const packagesBase = '/monorepo/packages';
+ const projectBase = '/monorepo/packages/package1';
+
+ const result = await searchPresetterConfigs(projectBase);
+ const expected = [
+ resolve(projectBase, 'presetter.config.ts'),
+ resolve(packagesBase, 'presetter.config.ts'),
+ resolve(monorepoBase, 'presetter.config.mjs'),
+ ];
+
+ expect(result).toEqual(expected);
+ });
+});
diff --git a/packages/presetter/spec/preset/content.spec.ts b/packages/presetter/spec/preset/content.spec.ts
deleted file mode 100644
index 051ae0f1..00000000
--- a/packages/presetter/spec/preset/content.spec.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-import { resolve } from 'node:path';
-
-import { beforeEach, describe, expect, it, vi } from 'vitest';
-
-import {
- createDummyContext,
- makeResolveRelative,
- mockIO,
- mockModuleResolution,
-} from './mock';
-
-makeResolveRelative();
-mockIO();
-mockModuleResolution();
-
-const { linkFiles, writeFiles } = await import('#io');
-
-const { bootstrapContent } = await import('#preset/content');
-
-describe('fn:bootstrapContent', () => {
- beforeEach(() => {
- vi.mocked(writeFiles).mockReset();
- vi.mocked(linkFiles).mockReset();
- });
-
- it('write configuration and link symlinks', async () => {
- await bootstrapContent(
- createDummyContext({ noSymlinks: ['path/to/file'] }),
- { force: false },
- );
-
- expect(writeFiles).toBeCalledWith(
- '/project',
- {
- 'link/pointed/to/other': { template: true },
- 'link/pointed/to/preset': { template: true },
- 'link/rewritten/by/project': { template: true },
- 'path/to/file': { template: true },
- },
- {
- 'link/pointed/to/other': resolve(
- '/.presetter/client/link/pointed/to/other',
- ),
- 'link/pointed/to/preset': resolve(
- '/.presetter/client/link/pointed/to/preset',
- ),
- 'link/rewritten/by/project': resolve(
- '/.presetter/client/link/rewritten/by/project',
- ),
- 'path/to/file': resolve('/project/path/to/file'),
- },
- { force: false },
- );
- expect(linkFiles).toBeCalledWith(
- '/project',
- {
- 'path/to/file': resolve('/project/path/to/file'),
- 'link/pointed/to/preset': resolve(
- '/.presetter/client/link/pointed/to/preset',
- ),
- 'link/pointed/to/other': resolve(
- '/.presetter/client/link/pointed/to/other',
- ),
- 'link/rewritten/by/project': resolve(
- '/.presetter/client/link/rewritten/by/project',
- ),
- },
- { force: false },
- );
- });
-
- it('ignore configuration', async () => {
- await bootstrapContent(
- createDummyContext({
- config: {
- 'path/to/file': { name: 'path/to/file' },
- 'link/pointed/to/preset': { name: 'link/pointed/to/preset' },
- 'link/pointed/to/other': { name: 'link/pointed/to/other' },
- 'link/rewritten/by/project': { name: 'link/rewritten/by/project' },
- },
- noSymlinks: ['path/to/file'],
- ignores: [
- 'link/pointed/to/preset',
- 'link/pointed/to/other',
- 'link/rewritten/by/project',
- { 'path/to/file': ['name'] },
- ],
- }),
- { force: false },
- );
-
- expect(writeFiles).toBeCalledWith(
- '/project',
- { 'path/to/file': { template: true } },
- { 'path/to/file': resolve('/project/path/to/file') },
- { force: false },
- );
- expect(linkFiles).toBeCalledWith(
- '/project',
- {
- 'path/to/file': resolve('/project/path/to/file'),
- },
- { force: false },
- );
- });
-
- it('honours ignore rules supplied by presets', async () => {
- await bootstrapContent(
- createDummyContext({
- preset: 'virtual:extension-preset',
- }),
- { force: false },
- );
-
- expect(writeFiles).toBeCalledWith(
- '/project',
- { 'path/to/file': { template: true } },
- { 'path/to/file': resolve('/project/path/to/file') },
- { force: false },
- );
- expect(linkFiles).toBeCalledWith(
- '/project',
- {
- 'path/to/file': resolve('/project/path/to/file'),
- },
- { force: false },
- );
- });
-});
diff --git a/packages/presetter/spec/preset/context.spec.ts b/packages/presetter/spec/preset/context.spec.ts
index 8fc64fc9..c4bd9136 100644
--- a/packages/presetter/spec/preset/context.spec.ts
+++ b/packages/presetter/spec/preset/context.spec.ts
@@ -1,50 +1,34 @@
-import { describe, expect, it, vi } from 'vitest';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { getPackage } from '#package';
import { getContext } from '#preset/context';
-vi.mock('#package', () => ({
- getPackage: vi.fn(async () => ({
- path: '/project/package.json',
- json: {
- name: 'client',
- scripts: {
- test: 'test',
- },
- dependencies: {},
- },
- })),
-}));
+import type { PresetContext } from 'presetter-types';
-vi.mock('#preset/presetterRC', () => ({
- getPresetterRC: vi.fn((root: string) => {
- switch (root) {
- case '/project':
- return {
- preset: 'preset',
- };
- default:
- throw new Error(`getPresetterRC: missing ${root}`);
- }
- }),
-}));
+vi.mock(
+ '#package',
+ () =>
+ ({
+ getPackage: vi.fn(async () => ({
+ json: { name: 'test-package' },
+ path: '/path/to/project/package.json',
+ })),
+ }) as Partial,
+);
describe('fn:getContext', () => {
- it('compute the current context', async () => {
- expect(await getContext()).toEqual({
- target: {
- name: 'client',
- root: '/project',
- package: {
- dependencies: {},
- name: 'client',
- scripts: {
- test: 'test',
- },
- },
- },
- custom: {
- preset: 'preset',
- },
- });
+ beforeEach(() => vi.clearAllMocks());
+
+ it('should be able to resolve the current context', async () => {
+ const cwd = '/path/to/project';
+
+ const result = await getContext(cwd);
+ const expected: PresetContext = {
+ root: '/path/to/project',
+ package: { name: 'test-package' },
+ };
+
+ expect(result).toEqual(expected);
+ expect(vi.mocked(getPackage)).toHaveBeenCalledWith(cwd);
});
});
diff --git a/packages/presetter/spec/preset/graph.spec.ts b/packages/presetter/spec/preset/graph.spec.ts
deleted file mode 100644
index 98c6eb63..00000000
--- a/packages/presetter/spec/preset/graph.spec.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-import { describe, expect, it } from 'vitest';
-
-import { createDummyContext, mockModuleResolution } from './mock';
-
-mockModuleResolution();
-
-const dummyContext = createDummyContext();
-
-const { getPresetAsset, getPresetGraph } = await import('#preset/graph');
-
-describe('fn:getPresetAsset', () => {
- it('return the preset asset', async () => {
- expect(
- await getPresetAsset('virtual:no-symlink-preset', dummyContext),
- ).toEqual({
- template: {
- 'path/to/file': '/path/to/template',
- },
- scripts: '/path/to/no-symlink-preset/scripts.yaml',
- });
- });
-});
-
-describe('fn:getPresetGraph', () => {
- it('compute the preset graph', async () => {
- expect(await getPresetGraph(dummyContext)).toEqual([
- {
- name: 'virtual:no-symlink-preset',
- asset: {
- template: {
- 'path/to/file': '/path/to/template',
- },
- scripts: '/path/to/no-symlink-preset/scripts.yaml',
- },
- nodes: [],
- },
- {
- name: 'virtual:symlink-only-preset',
- asset: {
- template: {
- 'link/pointed/to/preset': '/path/to/template',
- 'link/pointed/to/other': '/path/to/template',
- 'link/rewritten/by/project': '/path/to/template',
- },
- scripts: '/path/to/symlink-only-preset/scripts.yaml',
- },
- nodes: [],
- },
- ]);
- });
-
- it('add and merge extended presets', async () => {
- const graph = await getPresetGraph(
- createDummyContext({ preset: 'virtual:extension-preset' }),
- );
-
- expect(graph.length).toEqual(1);
- expect(graph).toMatchObject([
- {
- name: 'virtual:extension-preset',
- asset: {
- extends: ['virtual:no-symlink-preset', 'virtual:symlink-only-preset'],
- },
- nodes: [
- {
- name: 'virtual:no-symlink-preset',
- asset: {
- template: {
- 'path/to/file': '/path/to/template',
- },
- scripts: '/path/to/no-symlink-preset/scripts.yaml',
- },
- nodes: [],
- },
- {
- name: 'virtual:symlink-only-preset',
- asset: {
- template: {
- 'link/pointed/to/preset': '/path/to/template',
- 'link/pointed/to/other': '/path/to/template',
- 'link/rewritten/by/project': '/path/to/template',
- },
- scripts: '/path/to/symlink-only-preset/scripts.yaml',
- },
- nodes: [],
- },
- ],
- },
- ]);
- });
-
- it('warn about any missing presets', async () => {
- await expect(async () =>
- getPresetGraph({
- target: {
- name: 'client',
- root: '/missing-preset',
- package: {},
- },
- custom: { preset: 'virtual:missing-preset' },
- }),
- ).rejects.toThrow();
- });
-});
diff --git a/packages/presetter/spec/preset/mapping.spec.ts b/packages/presetter/spec/preset/mapping.spec.ts
deleted file mode 100644
index f8bfcbb8..00000000
--- a/packages/presetter/spec/preset/mapping.spec.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { resolve } from 'node:path';
-
-import { describe, expect, it } from 'vitest';
-
-import {
- createDummyContext,
- makeResolveRelative,
- mockModuleResolution,
-} from './mock';
-
-makeResolveRelative();
-mockModuleResolution();
-
-const { getDestinationMap } = await import('#preset/mapping');
-
-describe('fn:getDestinationMap', () => {
- it('compute the correct output paths', async () => {
- expect(
- await getDestinationMap(
- {
- config: '/path/to/template',
- },
- createDummyContext(),
- ),
- ).toEqual({
- config: resolve('/.presetter/client/config'),
- });
- });
-
- it('compute the correct output paths', async () => {
- expect(
- await getDestinationMap(
- {
- noSymlink: '/path/to/template',
- symlink: '/path/to/template',
- },
- createDummyContext({ noSymlinks: ['noSymlink'] }),
- ),
- ).toEqual({
- noSymlink: resolve('/project/noSymlink'),
- symlink: resolve('/.presetter/client/symlink'),
- });
- });
-});
diff --git a/packages/presetter/spec/preset/mock.ts b/packages/presetter/spec/preset/mock.ts
deleted file mode 100644
index f964f534..00000000
--- a/packages/presetter/spec/preset/mock.ts
+++ /dev/null
@@ -1,174 +0,0 @@
-import { dirname, posix, relative, resolve, sep } from 'node:path';
-
-import { vi } from 'vitest';
-
-import type * as pathNode from 'node:path';
-
-import type { PresetContext, ResolvedPresetContext } from 'presetter-types';
-
-/**
- *
- */
-export function makeResolveRelative() {
- vi.doMock('node:path', async (importActual) => {
- const actual = await importActual();
-
- return {
- ...actual,
- resolve: vi.fn((...pathSegments: string[]): string => {
- const relativePath = actual.relative(
- resolve(dirname(import.meta.url), '..'),
- resolve(...pathSegments),
- );
-
- return actual.resolve('/', relativePath);
- }),
- };
- });
-}
-
-/**
- *
- * @param file
- */
-export function mockIO(file?: Record) {
- vi.doMock('node:fs', () => ({
- existsSync: vi.fn((path: string): boolean => {
- // ensure that the paths below is compatible with windows
- const posixPath = relative(resolve('/'), path).split(sep).join(posix.sep);
-
- switch (posixPath) {
- case 'path/to/template':
- case 'path/to/no-symlink-preset/scripts.yaml':
- case 'path/to/symlink-only-preset/scripts.yaml':
- case `project/.presetterrc`:
- return true;
- default:
- return Object.keys({ ...file }).includes(posixPath);
- }
- }),
- writeFileSync: vi.fn(),
- }));
-
- vi.doMock('#io', () => ({
- linkFiles: vi.fn(),
- loadFile: vi.fn((path: string) => {
- // ensure that the paths below is compatible with windows
- const posixPath = relative(resolve('/'), path).split(sep).join(posix.sep);
- switch (posixPath) {
- case 'path/to/template':
- return { template: true };
- case 'path/to/no-symlink-preset/scripts.yaml':
- return { task: 'command_from_file' };
- case 'path/to/symlink-only-preset/scripts.yaml':
- return { task: 'command_from_symlink' };
- case `project/.presetterrc`:
- return {
- preset: [
- 'virtual:no-symlink-preset',
- 'virtual:symlink-only-preset',
- ],
- noSymlinks: ['path/to/file'],
- };
- default:
- if (Object.keys({ ...file }).includes(posixPath)) {
- return file![posixPath];
- } else {
- throw new Error(`loadFile: missing ${path}`);
- }
- }
- }),
- unlinkFiles: vi.fn(),
- writeFiles: vi.fn(),
- }));
-}
-
-/**
- *
- */
-export function mockModuleResolution() {
- vi.doMock('resolve-pkg', () => ({
- default: (name: string): string => name,
- }));
-
- vi.doMock('virtual:extension-preset', () => ({
- default: async () => ({
- extends: ['virtual:no-symlink-preset', 'virtual:symlink-only-preset'],
- supplementaryIgnores: () => [
- 'link/pointed/to/preset',
- 'link/pointed/to/other',
- 'link/rewritten/by/project',
- ],
- }),
- }));
-
- vi.doMock('virtual:no-symlink-preset', () => ({
- default: async () => ({
- template: {
- 'path/to/file': '/path/to/template',
- },
- scripts: '/path/to/no-symlink-preset/scripts.yaml',
- }),
- }));
-
- vi.doMock('virtual:symlink-only-preset', () => ({
- default: async () => ({
- template: {
- 'link/pointed/to/preset': '/path/to/template',
- 'link/pointed/to/other': '/path/to/template',
- 'link/rewritten/by/project': '/path/to/template',
- },
- scripts: '/path/to/symlink-only-preset/scripts.yaml',
- }),
- }));
-}
-
-/**
- *
- * @param custom
- */
-export function mockContext(custom?: PresetContext['custom']) {
- vi.doMock('#preset/context', () => ({
- getContext: vi.fn(async () => ({
- ...defaultDummyContext,
- custom: {
- ...defaultDummyContext.custom,
- ...custom,
- },
- })),
- }));
-}
-
-/**
- *
- * @param custom
- */
-export function createDummyContext(custom?: Partial) {
- return {
- ...defaultDummyContext,
- custom: {
- ...defaultDummyContext.custom,
- ...custom,
- },
- };
-}
-
-export const defaultDummyContext: ResolvedPresetContext = {
- target: {
- name: 'client',
- root: '/project',
- package: {
- name: 'client',
- scripts: {
- test: 'test',
- },
- dependencies: {},
- },
- },
- custom: {
- preset: ['virtual:no-symlink-preset', 'virtual:symlink-only-preset'],
- config: {},
- noSymlinks: ['path/to/file'],
- variable: {},
- },
-};
diff --git a/packages/presetter/spec/preset/presetter.config.ts b/packages/presetter/spec/preset/presetter.config.ts
deleted file mode 100644
index 0a997bfb..00000000
--- a/packages/presetter/spec/preset/presetter.config.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import esm from 'presetter-preset-esm';
-import strict from 'presetter-preset-strict';
-
-import type { Preset, PresetAsset } from 'presetter';
-
-export default (context: ProjectContext): PresetAsset =>
- ({
- name: 'optional-name',
- extends: [strict, esm],
- template: {
- 'eslint.config.ts': (configs) => [...configs],
- },
- }) satisfies Preset;
diff --git a/packages/presetter/spec/preset/presetterRC.spec.ts b/packages/presetter/spec/preset/presetterRC.spec.ts
deleted file mode 100644
index 52daf0da..00000000
--- a/packages/presetter/spec/preset/presetterRC.spec.ts
+++ /dev/null
@@ -1,120 +0,0 @@
-import { posix, relative, resolve, sep } from 'node:path';
-
-import { describe, expect, it, vi } from 'vitest';
-
-import { mockIO } from './mock';
-
-vi.doMock('read-pkg-up', () => ({
- readPackageUp: vi.fn(({ cwd }: { cwd: string }) => {
- // ensure that the paths below is compatible with windows
- const posixPath = relative(resolve('/'), cwd).split(sep).join(posix.sep);
- switch (posixPath) {
- case `monorepo`:
- case `monorepo/packages`:
- return { path: '/monorepo/package.json' };
- default:
- return undefined;
- }
- }),
-}));
-
-mockIO({
- 'monorepo/packages/package1/.presetterrc': {},
- 'monorepo/.presetterrc': {},
-});
-
-const { writeFileSync } = await import('node:fs');
-
-const {
- assertPresetterRC,
- getPresetterRC,
- getPresetterRCPaths,
- updatePresetterRC,
-} = await import('#preset/presetterRC');
-
-describe('fn:assertPresetterRC', () => {
- it('throw an error if the given value is not an object at all', () => {
- expect(() => assertPresetterRC(null)).toThrow();
- });
-
- it('throw an error if the given configuration misses the preset field', () => {
- expect(() => assertPresetterRC({})).toThrow();
- });
-
- it('throw an error if the preset field does not contain a preset name', () => {
- expect(() =>
- assertPresetterRC({ preset: { not: { a: { name: true } } } }),
- ).toThrow();
- });
-
- it('pass if a valid preset is given', () => {
- expect(() => assertPresetterRC({ preset: 'preset' })).not.toThrow();
- });
-
- it('pass if multiple valid presets are given', () => {
- expect(() =>
- assertPresetterRC({ preset: ['preset1', 'preset2'] }),
- ).not.toThrow();
- });
-});
-
-describe('fn:getPresetterRC', () => {
- it('accept an alternative file extension', async () => {
- expect(await getPresetterRC('/project')).toEqual({
- preset: ['virtual:no-symlink-preset', 'virtual:symlink-only-preset'],
- noSymlinks: ['path/to/file'],
- });
- });
-
- it('throw an error if no configuration file is found', async () => {
- expect(async () =>
- getPresetterRC('/missing-presetterrc'),
- ).rejects.toThrow();
- });
-});
-
-describe('fn:getPresetterRCPaths', () => {
- it('return the path to the configuration file in a single project repo', async () => {
- expect(await getPresetterRCPaths('/project')).toEqual([
- resolve('/project/.presetterrc'),
- ]);
- });
-
- it('return paths to the configuration files in a monorepo', async () => {
- expect(await getPresetterRCPaths('/monorepo/packages/package1')).toEqual([
- resolve('/monorepo/.presetterrc'),
- resolve('/monorepo/packages/package1/.presetterrc'),
- ]);
- });
-});
-
-describe('fn:updatePresetterRC', () => {
- it('create a new .presetterrc if it is inexistent', async () => {
- await updatePresetterRC('/missing-presetterrc', { preset: ['new-preset'] });
-
- expect(writeFileSync).toBeCalledWith(
- resolve('/missing-presetterrc/.presetterrc.json'),
- JSON.stringify({ preset: ['new-preset'] }, null, 2),
- );
- });
-
- it('merge with the existing .presetterrc', async () => {
- await updatePresetterRC('/project', { preset: ['new-preset'] });
-
- expect(writeFileSync).toBeCalledWith(
- resolve('/project/.presetterrc.json'),
- JSON.stringify(
- {
- preset: [
- 'virtual:no-symlink-preset',
- 'virtual:symlink-only-preset',
- 'new-preset',
- ],
- noSymlinks: ['path/to/file'],
- },
- null,
- 2,
- ),
- );
- });
-});
diff --git a/packages/presetter/spec/preset/project.spec.ts b/packages/presetter/spec/preset/project.spec.ts
new file mode 100644
index 00000000..71524a4f
--- /dev/null
+++ b/packages/presetter/spec/preset/project.spec.ts
@@ -0,0 +1,40 @@
+import { describe, expect, it, vi } from 'vitest';
+import Xception from 'xception';
+
+import { resolvePresetterConfig } from '#preset/config';
+import { resolveProjectPreset } from '#preset/project';
+
+import type { PresetContext, PresetNode } from 'presetter-types';
+
+vi.mock('#preset/config', () => ({
+ resolvePresetterConfig: vi.fn(),
+}));
+
+const context = {
+ root: '/path/to/project',
+ package: { name: 'test-package' },
+} satisfies PresetContext;
+
+describe('fn:resolveProjectPreset', () => {
+ it('resolves the project preset successfully', async () => {
+ vi.mocked(resolvePresetterConfig).mockResolvedValueOnce({
+ id: 'test-preset',
+ });
+
+ const result = await resolveProjectPreset(context);
+ const expected: PresetNode = {
+ definition: { id: 'test-preset' },
+ nodes: [],
+ };
+
+ expect(result).toEqual(expected);
+ });
+
+ it('throws an error if preset resolution fails', async () => {
+ vi.mocked(resolvePresetterConfig).mockRejectedValueOnce(
+ new Error('something went wrong'),
+ );
+
+ await expect(resolveProjectPreset(context)).rejects.toThrow(Xception);
+ });
+});
diff --git a/packages/presetter/spec/preset/resolution/asset.spec.ts b/packages/presetter/spec/preset/resolution/asset.spec.ts
new file mode 100644
index 00000000..b24c1cae
--- /dev/null
+++ b/packages/presetter/spec/preset/resolution/asset.spec.ts
@@ -0,0 +1,206 @@
+import { describe, expect, it, vi } from 'vitest';
+
+import {
+ listAssetNames,
+ resolveAsset,
+ resolveAssets,
+} from '#preset/resolution/asset';
+import { resolveNodeContent } from '#preset/resolution/content';
+
+import type {
+ PresetAssets,
+ PresetContentContext,
+ PresetNode,
+ ResolvedPresetAsset,
+} from 'presetter-types';
+
+vi.mock('#preset/resolution/content', { spy: true });
+
+const context = {
+ root: '/',
+ package: {},
+ variables: {
+ customAssetPath: 'path/to/custom/asset',
+ },
+} satisfies PresetContentContext;
+
+describe('fn:listAssetNames', () => {
+ it('should list all asset names in a preset node', () => {
+ const node = {
+ definition: {
+ id: 'parent',
+ assets: {
+ asset1: {},
+ asset2: {},
+ },
+ override: {
+ assets: {
+ 'asset1': {},
+ 'non-existent-asset': {}, // excluded as there is noting to be overridden
+ },
+ },
+ },
+ nodes: [
+ {
+ definition: {
+ id: 'child1',
+ assets: ({ variables }: PresetContentContext) =>
+ ({
+ [variables.customAssetPath!]: {
+ foo: 'bar',
+ } as ResolvedPresetAsset,
+ }) as PresetAssets,
+ },
+ nodes: [],
+ },
+ {
+ definition: {
+ id: 'child2',
+ assets: {
+ asset3: {},
+ },
+ },
+ nodes: [],
+ },
+ ],
+ } satisfies PresetNode;
+
+ const result = listAssetNames(node, context);
+ const expected = ['asset1', 'asset2', 'path/to/custom/asset', 'asset3'];
+
+ expect(result).toEqual(expected);
+ });
+
+ it('should handle nodes with no assets', () => {
+ const node = {
+ definition: { id: 'test-preset' },
+ nodes: [],
+ } satisfies PresetNode;
+
+ const result = listAssetNames(node, context);
+ const expected = [];
+
+ expect(result).toEqual(expected);
+ });
+});
+
+describe('fn:resolveAsset', () => {
+ it('should resolve an asset with initial and final pass', async () => {
+ const variables = { key: 'value' };
+ const node = {
+ definition: {
+ id: 'test-preset',
+ variables,
+ assets: { path: { content: 'initial' } },
+ override: {
+ assets: { path: { content: 'final' } },
+ },
+ },
+ nodes: [],
+ } satisfies PresetNode;
+
+ const result = await resolveAsset({
+ name: 'path',
+ node,
+ context,
+ variables,
+ });
+ const expected = { content: 'final' };
+
+ expect(result).toEqual(expected);
+ expect(resolveNodeContent).toHaveBeenCalledWith({
+ name: 'path',
+ node,
+ context,
+ variables,
+ select: expect.any(Function),
+ });
+ expect(resolveNodeContent).toHaveBeenCalledWith({
+ name: 'path',
+ node,
+ context,
+ initial: { content: 'initial' },
+ variables,
+ select: expect.any(Function),
+ });
+ });
+
+ it('should handle null and undefined assets', async () => {
+ const variables = { key: 'value' };
+ const node = { definition: { id: 'test-preset', variables }, nodes: [] };
+
+ const result = await resolveAsset({
+ name: '/missing/asset',
+ node,
+ context,
+ variables,
+ });
+ const expected = undefined;
+
+ expect(result).toEqual(expected);
+ expect(resolveNodeContent).toHaveBeenCalledWith({
+ name: '/missing/asset',
+ node,
+ context,
+ variables,
+ select: expect.any(Function),
+ });
+ expect(resolveNodeContent).toHaveBeenCalledWith({
+ name: '/missing/asset',
+ node,
+ context,
+ initial: undefined,
+ variables,
+ select: expect.any(Function),
+ });
+ });
+});
+
+describe('fn:resolveAssets', () => {
+ it('should resolve all assets', async () => {
+ const node = {
+ definition: {
+ id: 'parent',
+ assets: {
+ asset1: { key: 'value1' },
+ },
+ },
+ nodes: [
+ {
+ definition: {
+ id: 'child1',
+ assets: {
+ asset2: { key: 'value2' },
+ },
+ },
+ nodes: [],
+ },
+ {
+ definition: {
+ id: 'child2',
+ },
+ nodes: [
+ {
+ definition: {
+ id: 'grandchild',
+ assets: {
+ asset3: { key: 'value3' },
+ },
+ },
+ nodes: [],
+ },
+ ],
+ },
+ ],
+ } satisfies PresetNode;
+
+ const result = await resolveAssets(node, context);
+ const expected = {
+ asset1: { key: 'value1' },
+ asset2: { key: 'value2' },
+ asset3: { key: 'value3' },
+ };
+
+ expect(result).toEqual(expected);
+ });
+});
diff --git a/packages/presetter/spec/preset/resolution/content.spec.ts b/packages/presetter/spec/preset/resolution/content.spec.ts
new file mode 100644
index 00000000..3a26fc44
--- /dev/null
+++ b/packages/presetter/spec/preset/resolution/content.spec.ts
@@ -0,0 +1,158 @@
+import { describe, expect, it, vi } from 'vitest';
+
+import { loadFile } from '#io';
+import { resolveContent, resolveNodeContent } from '#preset/resolution/content';
+
+import type {
+ PresetContentContext,
+ PresetDefinition,
+ PresetNode,
+} from 'presetter-types';
+
+vi.mock('#io', () => ({
+ loadFile: vi.fn(),
+}));
+
+const context = {
+ root: '/',
+ package: {},
+ variables: {},
+} satisfies PresetContentContext;
+
+describe('fn:resolveContent', () => {
+ it('should resolve dynamic content from a function', async () => {
+ const content = vi.fn().mockResolvedValue({ key: 'value' });
+
+ const result = await resolveContent({ content, context });
+ const expected = { key: 'value' };
+
+ expect(result).toEqual(expected);
+ expect(content).toHaveBeenCalledWith(undefined, {
+ ...context,
+ variables: {},
+ });
+ });
+
+ it('should load content from a file', async () => {
+ vi.mocked(loadFile).mockReturnValue({ key: 'value' });
+
+ const result = await resolveContent({ content: '/path/to/file', context });
+ const expected = { key: 'value' };
+
+ expect(result).toEqual(expected);
+ expect(loadFile).toHaveBeenCalledWith('/path/to/file', {});
+ });
+
+ it('should merge current content with resolved content', async () => {
+ const result = await resolveContent({
+ content: { key: 'value' },
+ current: { existing: 'content' },
+ context,
+ });
+ const expected = { existing: 'content', key: 'value' };
+
+ expect(result).toEqual(expected);
+ });
+
+ it('should pass variables to content function', async () => {
+ const content = vi.fn().mockResolvedValue({ key: 'value' });
+
+ const variables = { key: 'value' };
+
+ const result = await resolveContent({ content, context, variables });
+ const expected = { key: 'value' };
+
+ expect(result).toEqual(expected);
+ expect(content).toHaveBeenCalledWith(undefined, {
+ ...context,
+ variables,
+ });
+ });
+
+ it('should pass variables to file loader', async () => {
+ vi.mocked(loadFile).mockReturnValue({ key: 'value' });
+
+ const variables = { key: 'value' };
+
+ const result = await resolveContent({
+ content: '/path/to/file',
+ context,
+ variables,
+ });
+ const expected = { key: 'value' };
+
+ expect(result).toEqual(expected);
+ expect(loadFile).toHaveBeenCalledWith('/path/to/file', variables);
+ });
+});
+
+describe('fn:resolveNodeContent', () => {
+ it('should resolve content of a node within a preset', async () => {
+ const node = {
+ definition: {
+ id: 'test-preset',
+ assets: {
+ '/path/to/file': { key: 'value' },
+ },
+ },
+ nodes: [],
+ } satisfies PresetNode;
+ const select = (definition: PresetDefinition) =>
+ definition.assets?.['/path/to/file'];
+
+ const result = await resolveNodeContent({
+ name: '/path/to/file',
+ node,
+ context,
+ select,
+ });
+ const expected = { key: 'value' };
+
+ expect(result).toEqual(expected);
+ });
+
+ it('should merge content from nested nodes', async () => {
+ const node = {
+ definition: {
+ id: 'parent',
+ assets: { '/path/to/file': { foo: 'foo' } },
+ },
+ nodes: [
+ {
+ definition: {
+ id: 'child1',
+ assets: { '/path/to/file': { source: 'child1' } },
+ },
+ nodes: [
+ {
+ definition: {
+ id: 'grandchild',
+ assets: { '/path/to/file': { bar: 'bar' } },
+ },
+ nodes: [],
+ },
+ ],
+ },
+ {
+ definition: {
+ id: 'child2',
+ assets: { '/path/to/file': { source: 'child2' } },
+ },
+ nodes: [],
+ },
+ ],
+ } satisfies PresetNode;
+ const select = (definition: PresetDefinition) =>
+ definition.assets?.['/path/to/file'];
+
+ const result = await resolveNodeContent({
+ name: '/path/to/file',
+ node,
+ context,
+ select,
+ });
+ const expected = { foo: 'foo', source: 'child2', bar: 'bar' };
+
+ expect(result).toEqual(expected);
+ });
+});
diff --git a/packages/presetter/spec/preset/resolution/obect.spec.ts b/packages/presetter/spec/preset/resolution/obect.spec.ts
new file mode 100644
index 00000000..7c6cc66a
--- /dev/null
+++ b/packages/presetter/spec/preset/resolution/obect.spec.ts
@@ -0,0 +1,34 @@
+import { describe, expect, it } from 'vitest';
+
+import { resolveObject } from '#preset/resolution/object';
+
+import type { PresetContentContext } from 'presetter-types';
+
+const context = {
+ root: '/',
+ package: {},
+ variables: {},
+} satisfies PresetContentContext;
+
+describe('fn:resolveObject', () => {
+ it('should resolve an object when it is a function', () => {
+ const object = (initial: PresetContentContext) => ({
+ root: initial.root,
+ resolved: true,
+ });
+
+ const result = resolveObject(object, context);
+ const expected = { root: '/', resolved: true };
+
+ expect(result).toEqual(expected);
+ });
+
+ it('should return the object as is when it is not a function', () => {
+ const object = { resolved: true };
+
+ const result = resolveObject(object, context);
+ const expected = object;
+
+ expect(result).toEqual(expected);
+ });
+});
diff --git a/packages/presetter/spec/preset/resolution/preset.spec.ts b/packages/presetter/spec/preset/resolution/preset.spec.ts
new file mode 100644
index 00000000..bd0d84ad
--- /dev/null
+++ b/packages/presetter/spec/preset/resolution/preset.spec.ts
@@ -0,0 +1,101 @@
+import { describe, expect, it } from 'vitest';
+
+import { resolvePreset } from '#preset/resolution/preset';
+
+import type { Preset, PresetContext, PresetNode } from 'presetter-types';
+
+const context = {
+ root: '/path/to/project',
+ package: { name: 'test-package' },
+} satisfies PresetContext;
+
+describe('fn:resolvePreset', () => {
+ it('should able to resolve a plan object preset successfully', async () => {
+ const preset: Preset = {
+ id: 'test-preset',
+ assets: {
+ 'config.json': {},
+ },
+ };
+
+ const result = await resolvePreset(preset, context);
+ const expected: PresetNode = {
+ definition: {
+ id: 'test-preset',
+ assets: {
+ 'config.json': {},
+ },
+ },
+ nodes: [],
+ };
+
+ expect(result).toEqual(expected);
+ });
+
+ it('should able to resolve a preset function', async () => {
+ const preset: Preset = Object.assign(
+ () => ({
+ assets: {
+ 'config.json': {},
+ },
+ }),
+ { id: 'test-preset' },
+ );
+
+ const result = await resolvePreset(preset, context);
+ const expected: PresetNode = {
+ definition: {
+ id: 'test-preset',
+ assets: {
+ 'config.json': {},
+ },
+ },
+ nodes: [],
+ };
+
+ expect(result).toEqual(expected);
+ });
+
+ it('should able to resolve a preset with nested presets', async () => {
+ const preset: Preset = {
+ id: 'parent',
+ extends: [
+ Object.assign(
+ () => ({
+ assets: {
+ 'child.json': {},
+ },
+ }),
+ { id: 'child' },
+ ),
+ ],
+ assets: {
+ 'parent.json': {},
+ },
+ };
+
+ const result = await resolvePreset(preset, context);
+ const expected: PresetNode = {
+ definition: {
+ id: 'parent',
+ extends: preset.extends,
+ assets: {
+ 'parent.json': {},
+ },
+ },
+ nodes: [
+ {
+ definition: {
+ id: 'child',
+ assets: {
+ 'child.json': {},
+ },
+ },
+ nodes: [],
+ },
+ ],
+ };
+
+ expect(result).toEqual(expected);
+ });
+});
diff --git a/packages/presetter/spec/preset/resolution/script.spec.ts b/packages/presetter/spec/preset/resolution/script.spec.ts
new file mode 100644
index 00000000..64839721
--- /dev/null
+++ b/packages/presetter/spec/preset/resolution/script.spec.ts
@@ -0,0 +1,109 @@
+import { describe, expect, it, vi } from 'vitest';
+
+import { resolveNodeContent } from '#preset/resolution/content';
+import { resolveScripts } from '#preset/resolution/script';
+
+import type { PresetContext, PresetNode } from 'presetter-types';
+
+vi.mock('#preset/resolution/content', { spy: true });
+
+const context = {
+ root: '/path/to/project',
+ package: {},
+} satisfies PresetContext;
+
+describe('fn:resolveScripts', () => {
+ it('should resolve scripts with initial and final pass', async () => {
+ const node = {
+ definition: {
+ id: 'test-preset',
+ scripts: { script: 'echo "initial"' },
+ override: {
+ scripts: { script: 'echo "final"' },
+ },
+ },
+ nodes: [],
+ } satisfies PresetNode;
+
+ const result = await resolveScripts(node, context);
+ const expected = { script: 'echo "final"' };
+
+ expect(result).toEqual(expected);
+ expect(resolveNodeContent).toHaveBeenCalledWith({
+ name: 'SCRIPTS',
+ node,
+ context,
+ variables: {},
+ select: expect.any(Function),
+ });
+ expect(resolveNodeContent).toHaveBeenCalledWith({
+ name: 'SCRIPTS',
+ node,
+ context,
+ initial: { script: 'echo "initial"' },
+ variables: {},
+ select: expect.any(Function),
+ });
+ });
+
+ it('should resolve scripts with variables', async () => {
+ const node = {
+ definition: {
+ id: 'test-preset',
+ scripts: { script: 'echo "initial"' },
+ override: {
+ scripts: { script: 'echo "{var}"' },
+ variables: { var: 'final' },
+ },
+ },
+ nodes: [],
+ } satisfies PresetNode;
+
+ const result = await resolveScripts(node, context);
+ const expected = { script: 'echo "final"' };
+
+ expect(result).toEqual(expected);
+ expect(resolveNodeContent).toHaveBeenCalledWith({
+ name: 'SCRIPTS',
+ node,
+ context,
+ variables: { var: 'final' },
+ select: expect.any(Function),
+ });
+ expect(resolveNodeContent).toHaveBeenCalledWith({
+ name: 'SCRIPTS',
+ node,
+ context,
+ initial: { script: 'echo "initial"' },
+ variables: { var: 'final' },
+ select: expect.any(Function),
+ });
+ });
+
+ it('should handle empty scripts', async () => {
+ const node = {
+ definition: { id: 'test-preset' },
+ nodes: [],
+ } satisfies PresetNode;
+
+ const result = await resolveScripts(node, context);
+ const expected = {};
+
+ expect(result).toEqual(expected);
+ expect(resolveNodeContent).toHaveBeenCalledWith({
+ name: 'SCRIPTS',
+ node,
+ context,
+ variables: {},
+ select: expect.any(Function),
+ });
+ expect(resolveNodeContent).toHaveBeenCalledWith({
+ name: 'SCRIPTS',
+ node,
+ context,
+ initial: undefined,
+ variables: {},
+ select: expect.any(Function),
+ });
+ });
+});
diff --git a/packages/presetter/spec/preset/resolution/variable.spec.ts b/packages/presetter/spec/preset/resolution/variable.spec.ts
new file mode 100644
index 00000000..52808761
--- /dev/null
+++ b/packages/presetter/spec/preset/resolution/variable.spec.ts
@@ -0,0 +1,109 @@
+import { describe, expect, it, vi } from 'vitest';
+
+import { resolveNodeContent } from '#preset/resolution/content';
+import { resolveVariables } from '#preset/resolution/variable';
+
+import type { PresetContext, PresetNode } from 'presetter-types';
+
+vi.mock('#preset/resolution/content', { spy: true });
+
+const context = {
+ root: '/path/to/project',
+ package: {},
+} satisfies PresetContext;
+
+describe('fn:resolveVariables', () => {
+ it('should resolve initial variables', async () => {
+ const node = {
+ definition: {
+ id: 'test-preset',
+ variables: { key: 'value' },
+ },
+ nodes: [],
+ } satisfies PresetNode;
+
+ const result = await resolveVariables(node, context);
+ const expected = { key: 'value' };
+
+ expect(result).toEqual(expected);
+ expect(resolveNodeContent).toHaveBeenCalledWith({
+ name: 'VARIABLES',
+ node,
+ context,
+ select: expect.any(Function),
+ });
+ });
+
+ it('should resolve final variables with overrides', async () => {
+ const node = {
+ definition: {
+ id: 'test-preset',
+ variables: { key: 'initialValue' },
+ override: { variables: { key: 'finalValue' } },
+ },
+ nodes: [],
+ } satisfies PresetNode;
+
+ const result = await resolveVariables(node, context);
+ const expected = { key: 'finalValue' };
+
+ expect(result).toEqual(expected);
+ expect(resolveNodeContent).toHaveBeenCalledWith({
+ name: 'VARIABLES',
+ node,
+ context,
+ select: expect.any(Function),
+ });
+ expect(resolveNodeContent).toHaveBeenCalledWith({
+ name: 'VARIABLES',
+ node,
+ context,
+ initial: { key: 'initialValue' },
+ select: expect.any(Function),
+ });
+ });
+
+ it('should resolve an empty object when no variables are defined', async () => {
+ const node = {
+ definition: { id: 'test-preset' },
+ nodes: [],
+ } satisfies PresetNode;
+
+ const result = await resolveVariables(node, context);
+ const expected = {};
+
+ expect(result).toEqual(expected);
+ expect(resolveNodeContent).toHaveBeenCalledWith({
+ name: 'VARIABLES',
+ node,
+ context,
+ select: expect.any(Function),
+ });
+ });
+
+ it('handle missing initial variables', async () => {
+ const node = {
+ definition: { id: 'parent' },
+ nodes: [
+ {
+ definition: {
+ id: 'child',
+ override: { variables: { key: 'value' } },
+ },
+ nodes: [],
+ },
+ ],
+ } satisfies PresetNode;
+
+ const result = await resolveVariables(node, context);
+ const expected = { key: 'value' };
+
+ expect(result).toEqual(expected);
+ expect(resolveNodeContent).toHaveBeenCalledWith({
+ name: 'VARIABLES',
+ node,
+ context,
+ select: expect.any(Function),
+ });
+ });
+});
diff --git a/packages/presetter/spec/preset/scripts.spec.ts b/packages/presetter/spec/preset/scripts.spec.ts
index 11fc20f7..924c81f5 100644
--- a/packages/presetter/spec/preset/scripts.spec.ts
+++ b/packages/presetter/spec/preset/scripts.spec.ts
@@ -1,17 +1,41 @@
-import { describe, expect, it } from 'vitest';
+import { describe, expect, it, vi } from 'vitest';
-import { mockContext, mockIO, mockModuleResolution } from './mock';
+import { getScripts } from '#preset/scripts';
-mockContext();
-mockModuleResolution();
-mockIO();
+vi.mock(
+ '#preset/context',
+ () =>
+ ({
+ getContext: vi.fn(async () => ({
+ root: '/path/to/project',
+ package: {},
+ })),
+ }) satisfies Partial,
+);
-const { getScripts } = await import('#preset/scripts');
+vi.mock(
+ '#preset/project',
+ () =>
+ ({
+ resolveProjectPreset: vi.fn(async () => ({
+ definition: {
+ id: 'test-preset',
+ scripts: {
+ test: 'exit 0',
+ },
+ },
+ nodes: [],
+ })),
+ }) satisfies Partial,
+);
describe('fn:getScripts', () => {
- it('return the scripts of the given preset', async () => {
- expect(await getScripts()).toEqual({
- task: 'command_from_symlink',
- });
+ it('should be able to resolves scripts templates', async () => {
+ const result = await getScripts();
+ const expected = {
+ test: 'exit 0',
+ };
+
+ expect(result).toEqual(expected);
});
});
diff --git a/packages/presetter/spec/preset/setup.spec.ts b/packages/presetter/spec/preset/setup.spec.ts
deleted file mode 100644
index 83c74f1c..00000000
--- a/packages/presetter/spec/preset/setup.spec.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest';
-
-// import { writePackage } from 'write-pkg';
-
-// import { bootstrapContent } from '#preset/content';
-// import { updatePresetterRC } from '#preset/presetterRC';
-
-// import type { setupPreset } from '#preset/setup';
-import {
- defaultDummyContext,
- mockContext,
- mockIO,
- mockModuleResolution,
-} from './mock';
-
-vi.doMock('node:console', () => ({
- info: vi.fn(),
-}));
-
-vi.doMock('read-pkg', () => ({
- readPackage: vi
- .fn()
- .mockReturnValueOnce({
- devDependencies: {
- other: '*',
- },
- })
- .mockReturnValueOnce({
- devDependencies: {
- other: '*',
- presetter: '*',
- preset1: '*',
- preset2: '*',
- },
- }),
-}));
-
-vi.doMock('write-pkg', () => ({
- writePackage: vi.fn(),
-}));
-
-vi.doMock('#package', () => ({
- arePeerPackagesAutoInstalled: vi.fn(),
- getPackage: vi.fn(() => ({
- path: '/project/package.json',
- json: {
- name: 'client',
- scripts: {
- test: 'test',
- },
- dependencies: {},
- },
- })),
- reifyDependencies: vi.fn(
- async ({ add }: Parameters[0]) =>
- add?.map((name) => ({ name, version: '*' })),
- ),
-}));
-
-vi.doMock('#preset/content', () => ({
- bootstrapContent: vi.fn(),
-}));
-
-vi.doMock('#preset/presetterRC', async (importActual) => ({
- updatePresetterRC: vi.fn(),
-}));
-
-mockContext();
-mockIO();
-mockModuleResolution();
-
-const { writePackage } = await import('write-pkg');
-const { arePeerPackagesAutoInstalled, reifyDependencies } = await import(
- '#package'
-);
-const { bootstrapContent } = await import('#preset/content');
-const { updatePresetterRC } = await import('#preset/presetterRC');
-const { bootstrapPreset, setupPreset } = await import('#preset/setup');
-
-describe('fn:bootstrapPreset', () => {
- beforeEach(() => {
- vi.mocked(reifyDependencies).mockReset();
- });
-
- it('should install packages specified by the preset if peer packages are not automatically installed by the package manager', async () => {
- vi.mocked(arePeerPackagesAutoInstalled).mockReturnValue(false);
-
- await bootstrapPreset();
-
- expect(reifyDependencies).toHaveBeenCalledTimes(1);
- });
-
- it('should skip installing peer packages manually if auto peers install is supported by package manager', async () => {
- vi.mocked(arePeerPackagesAutoInstalled).mockReturnValue(true);
-
- await bootstrapPreset();
-
- expect(reifyDependencies).not.toHaveBeenCalled();
- });
-});
-
-describe('fn:setupPreset', () => {
- it('should install presetter and the preset', async () => {
- await setupPreset('preset1', 'preset2');
-
- // it should install presetter and the preset
- expect(reifyDependencies).toHaveBeenCalledTimes(1);
- expect(reifyDependencies).toBeCalledWith({
- add: ['presetter', 'preset1', 'preset2'],
- root: '/project',
- saveAs: 'dev',
- lockFile: true,
- });
-
- // it should write to .presetterrc.json
- expect(updatePresetterRC).toBeCalledWith('/project', {
- preset: ['preset1', 'preset2'],
- });
-
- // it should bootstrap the client project
- expect(bootstrapContent).toBeCalledWith(defaultDummyContext);
-
- // it should merge the bootstrapping script to package.json
- expect(writePackage).toBeCalledWith(
- '/project',
- expect.objectContaining({
- scripts: {
- prepare: 'presetter bootstrap',
- test: 'test',
- },
- }),
- );
- });
-});
diff --git a/packages/presetter/spec/preset/unset.spec.ts b/packages/presetter/spec/preset/unset.spec.ts
deleted file mode 100644
index fd4e2864..00000000
--- a/packages/presetter/spec/preset/unset.spec.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { resolve } from 'node:path';
-
-import { describe, expect, it } from 'vitest';
-
-import {
- makeResolveRelative,
- mockContext,
- mockIO,
- mockModuleResolution,
-} from './mock';
-
-makeResolveRelative();
-mockContext();
-mockIO();
-mockModuleResolution();
-
-const { unlinkFiles } = await import('#io');
-
-const { unsetPreset } = await import('#preset/unset');
-
-describe('fn:unsetPreset', () => {
- it('clean up any artifacts installed on the project root', async () => {
- await unsetPreset();
-
- expect(unlinkFiles).toHaveBeenCalledWith('/project', {
- 'link/pointed/to/other': resolve(
- '/.presetter/client/link/pointed/to/other',
- ),
- 'link/pointed/to/preset': resolve(
- '/.presetter/client/link/pointed/to/preset',
- ),
- 'link/rewritten/by/project': resolve(
- '/.presetter/client/link/rewritten/by/project',
- ),
- 'path/to/file': resolve('/project/path/to/file'),
- });
- });
-});
diff --git a/packages/presetter/spec/resolution/getConfigKey.spec.ts b/packages/presetter/spec/resolution/getConfigKey.spec.ts
deleted file mode 100644
index 811c7a79..00000000
--- a/packages/presetter/spec/resolution/getConfigKey.spec.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { describe, expect, it } from 'vitest';
-
-import { getConfigKey } from '#resolution';
-
-describe('fn:getConfigKey', () => {
- it('get config keys based on the filename', () => {
- expect(getConfigKey('.tsconfig.json')).toEqual('tsconfig');
- expect(getConfigKey('.npmignore')).toEqual('npmignore');
- expect(getConfigKey('rollup.config.ts')).toEqual('rollup');
- });
-});
diff --git a/packages/presetter/spec/resolution/loadDynamic.spec.ts b/packages/presetter/spec/resolution/loadDynamic.spec.ts
deleted file mode 100644
index c22437d3..00000000
--- a/packages/presetter/spec/resolution/loadDynamic.spec.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { posix, relative, resolve, sep } from 'node:path';
-
-import { describe, expect, it, vi } from 'vitest';
-
-import { loadDynamic } from '#resolution';
-
-import type { ResolvedPresetContext } from 'presetter-types';
-
-vi.mock('node:fs', () => ({
- existsSync: vi.fn((path: string): boolean => {
- // ensure that the paths below is compatible with windows
- const posixPath = relative(resolve('/'), path).split(sep).join(posix.sep);
- switch (posixPath) {
- case 'path/to/config.json':
- return true;
- default:
- return false;
- }
- }),
-}));
-
-vi.mock('#io', () => ({
- loadFile: vi.fn(async (path: string) => {
- // ensure that the paths below is compatible with windows
- const posixPath = relative(resolve('/'), path).split(sep).join(posix.sep);
-
- switch (posixPath) {
- case 'path/to/config.json':
- return { json: true };
- default:
- throw new Error(`readFile: missing ${path}`);
- }
- }),
-}));
-
-describe('fn:loadDynamic', () => {
- const context: ResolvedPresetContext<'variable'> = {
- target: { name: 'name', root: 'root', package: {} },
- custom: { preset: 'preset', variable: { key: 'value' } },
- };
-
- it('load the content from a dynamic generator', async () => {
- expect(
- await loadDynamic(
- (args: ResolvedPresetContext<'variable'>) => ({
- key: args.custom.variable.key,
- }),
- context,
- ),
- ).toEqual({
- key: 'value',
- });
- });
-
- it('load the content from a file if it is a valid path', async () => {
- expect(await loadDynamic('/path/to/config.json', context)).toEqual({
- json: true,
- });
- });
-
- it('return any non-generator content directly', async () => {
- expect(await loadDynamic('text', context)).toEqual('text');
- expect(await loadDynamic({ a: 0 }, context)).toEqual({ a: 0 });
- });
-});
diff --git a/packages/presetter/spec/resolution/loadDynamicMap.spec.ts b/packages/presetter/spec/resolution/loadDynamicMap.spec.ts
deleted file mode 100644
index 8b50594c..00000000
--- a/packages/presetter/spec/resolution/loadDynamicMap.spec.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { describe, expect, it } from 'vitest';
-
-import { loadDynamicMap } from '#resolution';
-
-import type { ResolvedPresetContext } from 'presetter-types';
-
-describe('fn:loadDynamicMap', () => {
- const context: ResolvedPresetContext = {
- target: { name: 'name', root: 'root', package: {} },
- custom: {
- preset: 'preset',
- config: {},
- noSymlinks: [],
- variable: { key: 'value' },
- },
- };
-
- it('pass on map if no generator is supplied', async () => {
- expect(
- await loadDynamicMap({ template: { form: 'literal' } }, context),
- ).toMatchObject({ template: { form: 'literal' } });
- });
-
- it('compute a field via a generator', async () => {
- expect(
- await loadDynamicMap(
- { template: () => ({ form: 'field generator' }) },
- context,
- ),
- ).toMatchObject({ template: { form: 'field generator' } });
- });
-
- it('compute map via generators', async () => {
- expect(
- await loadDynamicMap(
- () => ({
- template: () => ({ form: 'map generator' }),
- }),
- context,
- ),
- ).toMatchObject({ template: { form: 'map generator' } });
- });
-});
diff --git a/packages/presetter/spec/resolve.spec.ts b/packages/presetter/spec/resolve.spec.ts
new file mode 100644
index 00000000..675bb112
--- /dev/null
+++ b/packages/presetter/spec/resolve.spec.ts
@@ -0,0 +1,48 @@
+import { resolve as resolvePath } from 'node:path';
+
+import { describe, expect, it, vi } from 'vitest';
+
+import { getContext } from '#preset';
+import { resolve } from '#resolve';
+
+import type { PresetContext } from 'presetter-types';
+
+const { configTs } = vi.hoisted(() => ({ configTs: { foo: () => 'bar' } }));
+
+vi.mock('#preset', async (importActual) => {
+ return {
+ ...(await importActual()),
+ getContext: vi.fn(
+ async (): Promise => ({
+ root: resolvePath('/path/to/project'),
+ package: {},
+ }),
+ ),
+ resolvePresetterConfig: vi.fn(async () => ({
+ id: 'test-preset',
+ assets: {
+ 'nested/config.ts': configTs,
+ },
+ })),
+ } satisfies Partial;
+});
+
+describe('fn:resolve', () => {
+ it('should resolve an asset given its file url', async () => {
+ const url = `file://${resolvePath('/path/to/project/nested/config.ts')}`;
+ const result = await resolve(url);
+ const expected = configTs;
+
+ expect(result).toEqual(expected);
+ expect(vi.mocked(getContext)).toHaveBeenCalledWith(
+ resolvePath('/path/to/project/nested'),
+ );
+ });
+
+ it('should throw an error if the asset is not found', async () => {
+ const url = `file://${resolvePath('/path/to/project/missing.ts')}`;
+ await expect(resolve(url)).rejects.toThrow(
+ `asset missing.ts not found in preset defined at ${resolvePath('/path/to/project/missing.ts')}`,
+ );
+ });
+});
diff --git a/packages/presetter/spec/run/run.spec.ts b/packages/presetter/spec/run/run.spec.ts
index 234c4450..6ae265f2 100644
--- a/packages/presetter/spec/run/run.spec.ts
+++ b/packages/presetter/spec/run/run.spec.ts
@@ -1,4 +1,4 @@
-import { describe, expect, it, vi } from 'vitest';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
import { run } from '#run';
import npmRunScript from '@npmcli/run-script';
@@ -40,6 +40,8 @@ vi.mock('#preset', () => ({
}));
describe('fn:run', () => {
+ beforeEach(() => vi.clearAllMocks());
+
it('should run tasks via listr', async () => {
const selectors = [
{ selector: 'task', args: [] },
@@ -94,6 +96,7 @@ describe('fn:run', () => {
it('should exit with an error code when any one of the tasks fails', async () => {
await run([{ selector: 'error', args: [] }]);
+ // eslint-disable-next-line @typescript-eslint/unbound-method
expect(process.exit).toHaveBeenCalledWith(1);
});
diff --git a/packages/presetter/spec/scripts/composeScripts.spec.ts b/packages/presetter/spec/scripts/composeScripts.spec.ts
index 1352f5e8..24664c89 100644
--- a/packages/presetter/spec/scripts/composeScripts.spec.ts
+++ b/packages/presetter/spec/scripts/composeScripts.spec.ts
@@ -1,32 +1,33 @@
import { describe, expect, it, vi } from 'vitest';
-import yargs from 'yargs';
-
import { composeScripts } from '#scripts';
import type { Script } from '#scripts';
-vi.mock('yargs', () => ({
- __esModule: true,
- default: {
- parse: vi.fn((path: string) =>
- path
- ? yargs.parse(path)
- : {
- // mimic the current runner
- $0: 'run',
- },
- ),
- },
-}));
+vi.mock('yargs', async (importActual) => {
+ const { default: yargs } = await importActual();
+
+ return {
+ default: {
+ parse: vi.fn((path: string) =>
+ path
+ ? yargs().parse(path)
+ : {
+ // mimic the current runner
+ $0: 'run',
+ },
+ ),
+ },
+ };
+});
/**
* a helper function for generating tests on translation
* @param description description of the test
* @param detail input and expected output
- * @param detail.template
- * @param detail.target
- * @param detail.result
+ * @param detail.template script definitions from scripts.yaml
+ * @param detail.target script definitions from target project's package.json
+ * @param detail.result expected output, or an error
*/
function should(
description: string,
@@ -41,7 +42,7 @@ function should(
): void {
const { template, target, result } = detail;
- it(description, () => {
+ it(`should ${description}`, () => {
const compute = () =>
composeScripts({
template,
diff --git a/packages/presetter/spec/serialization.spec.ts b/packages/presetter/spec/serialization.spec.ts
new file mode 100644
index 00000000..9ecf32ad
--- /dev/null
+++ b/packages/presetter/spec/serialization.spec.ts
@@ -0,0 +1,104 @@
+import { describe, expect, it } from 'vitest';
+
+import { buildEsmFile, serialize } from '#serialization';
+
+describe('buildEsmFile', () => {
+ it('should generate ES module content with named and default exports', () => {
+ const exportList = ['default', 'namedExport'];
+
+ const result = buildEsmFile(exportList);
+
+ const expectedOutput = [
+ `import { resolve } from 'presetter';`,
+ '',
+ `const assets = await resolve(import.meta.url);`,
+ '',
+ `export default assets['default'];`,
+ `export const namedExport = assets['namedExport'];`,
+ ].join('\n');
+
+ expect(result).toEqual(expectedOutput);
+ });
+
+ it('should generate ES module content with only default export', () => {
+ const exportList = ['default'];
+
+ const result = buildEsmFile(exportList);
+
+ const expectedOutput = [
+ `import { resolve } from 'presetter';`,
+ '',
+ `const assets = await resolve(import.meta.url);`,
+ '',
+ `export default assets['default'];`,
+ ].join('\n');
+
+ expect(result).toEqual(expectedOutput);
+ });
+
+ it('should generate ES module content with only named exports', () => {
+ const exportList = ['namedExport'];
+
+ const result = buildEsmFile(exportList);
+
+ const expectedOutput = [
+ `import { resolve } from 'presetter';`,
+ '',
+ `const assets = await resolve(import.meta.url);`,
+ '',
+ `export const namedExport = assets['namedExport'];`,
+ ].join('\n');
+
+ expect(result).toEqual(expectedOutput);
+ });
+});
+
+describe('fn:serialize', () => {
+ it('should return Buffer content as is', () => {
+ const content = Buffer.from('buffer content');
+ const result = serialize('/path/to/file.bin', content);
+ expect(result).toEqual(content);
+ });
+
+ it('should convert an object to JSON format', () => {
+ const content = { json: true };
+ const result = serialize('/path/to/config.json', content);
+ const expected = JSON.stringify(content, null, 2);
+ expect(result).toEqual(expected);
+ });
+
+ it('should convert an object to YAML format', () => {
+ const content = { yaml: true };
+ const result = serialize('/path/to/config.yaml', content);
+ const expected = 'yaml: true\n';
+ expect(result).toEqual(expected);
+ });
+
+ it('should generate dynamic ES module content', () => {
+ const content = { default: 'defaultExport', named: 'namedExport' };
+ const result = serialize('/path/to/module.js', content);
+ const expected = [
+ `import { resolve } from 'presetter';`,
+ '',
+ `const assets = await resolve(import.meta.url);`,
+ '',
+ `export default assets['default'];`,
+ `export const named = assets['named'];`,
+ ].join('\n');
+ expect(result).toEqual(expected);
+ });
+
+ it('should join array content with newlines', () => {
+ const content = ['line1', 'line2', 'line3'];
+ const result = serialize('/path/to/.gitignore', content);
+ const expected = content.join('\n');
+ expect(result).toEqual(expected);
+ });
+
+ it('should convert an object to JSON format for non-.json extensions', () => {
+ const content = { json: true };
+ const result = serialize('/path/to/.prettierrc', content);
+ const expected = JSON.stringify(content, null, 2);
+ expect(result).toEqual(expected);
+ });
+});
diff --git a/packages/presetter/spec/task/parseGlobalArgs.spec.ts b/packages/presetter/spec/task/parseGlobalArgs.spec.ts
index 0ee0a01a..1deb522b 100644
--- a/packages/presetter/spec/task/parseGlobalArgs.spec.ts
+++ b/packages/presetter/spec/task/parseGlobalArgs.spec.ts
@@ -5,7 +5,7 @@ import { parseGlobalArgs } from '#task';
import type { Arguments } from 'yargs-parser';
describe('fn:parseGlobalArgs', () => {
- it('return an array of cleaned up global arguments as strings', () => {
+ it('should return an array of cleaned up global arguments as strings', () => {
const argv: Arguments = {
'_': [],
'--': ['"hello"', "'world'", 123, 'no-quotes'],
@@ -17,7 +17,7 @@ describe('fn:parseGlobalArgs', () => {
expect(result).toEqual(expectedResult);
});
- it('return an empty array when no global arguments are present', () => {
+ it('should return an empty array when no global arguments are present', () => {
const argv: Arguments = {
_: [],
};
diff --git a/packages/presetter/spec/task/parseTaskSpec.spec.ts b/packages/presetter/spec/task/parseTaskSpec.spec.ts
index 4dfaff30..71be5bf2 100644
--- a/packages/presetter/spec/task/parseTaskSpec.spec.ts
+++ b/packages/presetter/spec/task/parseTaskSpec.spec.ts
@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest';
import { parseTaskSpec } from '#task';
describe('fn:parseTaskSpec', () => {
- it('return the correct selector and empty args if no args provided', () => {
+ it('should return the correct selector and empty args if no args provided', () => {
const taskString = 'selector';
const globalArgs: string[] = [];
const expectedResult: { selector: string; args: string[] } = {
@@ -14,7 +14,7 @@ describe('fn:parseTaskSpec', () => {
expect(parseTaskSpec(taskString, globalArgs)).toEqual(expectedResult);
});
- it('return the correct selector and args without globalArgs', () => {
+ it('should return the correct selector and args without globalArgs', () => {
const taskString = 'selector -- --arg1=value1 --arg2="value with spaces"';
const globalArgs: string[] = [];
const expectedResult: { selector: string; args: string[] } = {
@@ -25,7 +25,7 @@ describe('fn:parseTaskSpec', () => {
expect(parseTaskSpec(taskString, globalArgs)).toEqual(expectedResult);
});
- it('return the correct selector and args with globalArgs', () => {
+ it('should return the correct selector and args with globalArgs', () => {
const taskString = 'selector -- {@} --arg1=value1';
const globalArgs: string[] = ['--globalArg1=value1', '--globalArg2=value2'];
const expectedResult: { selector: string; args: string[] } = {
diff --git a/packages/presetter/spec/task/selectTasks.spec.ts b/packages/presetter/spec/task/selectTasks.spec.ts
index 1b16699e..e802575c 100644
--- a/packages/presetter/spec/task/selectTasks.spec.ts
+++ b/packages/presetter/spec/task/selectTasks.spec.ts
@@ -18,26 +18,26 @@ describe('fn:selectTasks', () => {
'task2:subtask1:subsubtask2',
];
- it('return the exact task when no wildcard is provided', () => {
+ it('should return the exact task when no wildcard is provided', () => {
expect(selectTasks(tasks, 'task1')).toEqual(['task1']);
});
describe('with single-level wildcard (*)', () => {
- it('return only the direct subtasks matching the wildcard', () => {
+ it('should return only the direct subtasks matching the wildcard', () => {
expect(selectTasks(tasks, 'task1:*')).toEqual([
'task1:subtask1',
'task1:subtask2',
]);
});
- it('return only the direct subtasks matching the wildcard at a deeper level', () => {
+ it('should return only the direct subtasks matching the wildcard at a deeper level', () => {
expect(selectTasks(tasks, 'task2:subtask1:*')).toEqual([
'task2:subtask1:subsubtask1',
'task2:subtask1:subsubtask2',
]);
});
- it('return subsubtasks at any subtask level matching the wildcard', () => {
+ it('should return subsubtasks at any subtask level matching the wildcard', () => {
expect(selectTasks(tasks, 'task1:*:subsubtask1')).toEqual([
'task1:subtask1:subsubtask1',
'task1:subtask2:subsubtask1',
@@ -46,7 +46,7 @@ describe('fn:selectTasks', () => {
});
describe('with multi-level wildcard (**)', () => {
- it('return all subtasks matching the wildcard at any level', () => {
+ it('should return all subtasks matching the wildcard at any level', () => {
expect(selectTasks(tasks, 'task2:**')).toEqual([
'task2:subtask1',
'task2:subtask2',
diff --git a/packages/presetter/spec/template/merge.spec.ts b/packages/presetter/spec/template/merge.spec.ts
index f607e042..7bbf3598 100644
--- a/packages/presetter/spec/template/merge.spec.ts
+++ b/packages/presetter/spec/template/merge.spec.ts
@@ -1,138 +1,127 @@
import { describe, expect, it } from 'vitest';
-import { merge, mergeTemplate } from '#template/merge';
+import { merge } from '#template/merge';
describe('fn:merge', () => {
- it('overwite list content', () => {
+ it('should overwite list content', () => {
expect(merge('list1', 'list2')).toEqual('list2');
});
- it('just return the replacement if the supplied customization cannot be mergeed', () => {
+ it('should just return the replacement if the supplied customization cannot be mergeed', () => {
expect(merge('line', { object: true })).toEqual({ object: true });
expect(merge({ object: true }, 'line')).toEqual('line');
});
- it('return the original if no replacement is given', () => {
+ it('should return the original if no replacement is given', () => {
expect(merge(0)).toEqual(0);
expect(merge({ a: 0 })).toEqual({ a: 0 });
expect(merge([0])).toEqual([0]);
});
- it('merge two independent objects', () => {
+ it('should return the original if the replacement is undefined', () => {
+ expect(merge(0, undefined)).toEqual(0);
+ expect(merge({ a: 0 }, undefined)).toEqual({ a: 0 });
+ expect(merge([0], undefined)).toEqual([0]);
+ });
+
+ it('should return null if the replacement is null', () => {
+ expect(merge(0, null)).toEqual(null);
+ expect(merge({ a: 0 }, null)).toEqual(null);
+ expect(merge([0], null)).toEqual(null);
+ });
+
+ it('should merge two independent objects', () => {
expect(merge({ a: 0 }, { b: 1 })).toEqual({ a: 0, b: 1 });
});
- it('merge a list', () => {
+ it('should merge a list', () => {
expect(merge([0], [1])).toEqual([0, 1]);
});
- it('overwrite a list', () => {
+ it('should overwrite a list', () => {
expect(merge([0], 1)).toEqual(1);
});
- it('overwrite a primitive property', () => {
+ it('should overwrite a primitive property', () => {
expect(merge({ a: 0 }, { a: 1 })).toEqual({ a: 1 });
});
- it('overwrite a list', () => {
+ it('should overwrite a list', () => {
expect(merge({ a: [0, 1] }, { a: { 0: 1 } })).toEqual({ a: [1, 1] });
});
- it('leave a list untouched', () => {
+ it('should leave a list untouched', () => {
expect(merge({ a: [0] }, {})).toEqual({ a: [0] });
});
- it('deep merge a primitive', () => {
+ it('should deep merge a primitive', () => {
expect(merge({ a: { b: 0 }, c: 1 }, { a: { b: 1 } })).toEqual({
a: { b: 1 },
c: 1,
});
});
- it('deep merge an object', () => {
+ it('should deep merge an object', () => {
expect(merge({ a: { b: 0 } }, { a: { c: 1 } })).toEqual({
a: { b: 0, c: 1 },
});
});
- it('deep merge an object in overwrite mode', () => {
+ it('should deep merge an object in overwrite mode', () => {
expect(merge({ a: { b: 0 } }, { a: { c: 1 } })).toEqual({
a: { b: 0, c: 1 },
});
});
- it('deep merge a list', () => {
+ it('should deep merge a list', () => {
expect(merge({ a: [{ b: 0 }] }, { a: { 0: { c: 1 } } })).toEqual({
a: [{ b: 0, c: 1 }],
});
});
- it('deep extend a list', () => {
+ it('should deep extend a primitive list', () => {
expect(merge({ a: { b: [0] } }, { a: { b: [1] } })).toEqual({
a: { b: [0, 1] },
});
});
- it('deep overwrite a list', () => {
+ it('should deep extend an object list', () => {
expect(
- merge(
- { a: { b: [0, { options: false }] } },
- { a: { b: [1, { options: true }] } },
- ),
+ merge({ a: { b: [{ name: 'foo' }] } }, { a: { b: [{ name: 'bar' }] } }),
).toEqual({
- a: { b: [1, { options: true }] },
+ a: { b: [{ name: 'foo' }, { name: 'bar' }] },
});
});
- it('deep merge a list uniquely', () => {
- expect(merge({ a: [0] }, { a: [0] })).toEqual({ a: [0] });
- });
+ it('should deep extend an object list uniquely', () => {
+ const item = { name: 'foo' };
- it('deep overwrite a list item', () => {
- expect(merge({ a: { b: [0, 1] } }, { a: { b: { 0: 1 } } })).toEqual({
- a: { b: [1, 1] },
+ expect(merge({ a: { b: [item] } }, { a: { b: [item] } })).toEqual({
+ a: { b: [{ name: 'foo' }] },
});
});
- it('deep overwrite a list item', () => {
- expect(merge({ a: { b: [0] } }, { a: { b: 1 } })).toEqual({
- a: { b: 1 },
- });
- });
-});
-
-describe('fn:mergeTemplate', () => {
- it('keep source template if the target does not have one', () => {
+ it('should deep extend an object list without checking its content', () => {
expect(
- mergeTemplate(
- { common: 'to be written', sourceOnly: 'source_only' },
- { common: 'common' },
- ),
+ merge({ a: { b: [{ name: 'foo' }] } }, { a: { b: [{ name: 'foo' }] } }),
).toEqual({
- common: 'common',
- sourceOnly: 'source_only',
+ a: { b: [{ name: 'foo' }, { name: 'foo' }] },
});
});
- it('merge a JSON template', () => {
- expect(mergeTemplate({ json: { a: true } }, { json: { b: true } })).toEqual(
- {
- json: { a: true, b: true },
- },
- );
+ it('should deep merge a list uniquely', () => {
+ expect(merge({ a: [0] }, { a: [0] })).toEqual({ a: [0] });
});
- it('merge a list', () => {
- expect(mergeTemplate({ '.list': 'line1' }, { '.list': 'line2' })).toEqual({
- '.list': 'line1\nline2',
+ it('should deep overwrite a list item', () => {
+ expect(merge({ a: { b: [0, 1] } }, { a: { b: { 0: 1 } } })).toEqual({
+ a: { b: [1, 1] },
});
});
- it('overwhite value if the template file is not a list', () => {
- expect(
- mergeTemplate({ 'notalist.file': 'line1' }, { 'notalist.file': 'line2' }),
- ).toEqual({
- 'notalist.file': 'line2',
+ it('should deep overwrite a list item', () => {
+ expect(merge({ a: { b: [0] } }, { a: { b: 1 } })).toEqual({
+ a: { b: 1 },
});
});
});
diff --git a/packages/presetter/spec/template/substitute.spec.ts b/packages/presetter/spec/template/substitute.spec.ts
index 8318a5bb..ca683e6f 100644
--- a/packages/presetter/spec/template/substitute.spec.ts
+++ b/packages/presetter/spec/template/substitute.spec.ts
@@ -3,23 +3,23 @@ import { describe, expect, it } from 'vitest';
import { substitute } from '#template/substitute';
describe('fn:substitute', () => {
- it('replace a simple string', () => {
+ it('should replace a simple string', () => {
expect(substitute('{value}', { value: 'value' })).toEqual('value');
});
- it('replace content in an array', () => {
+ it('should replace content in an array', () => {
expect(
substitute(['{key}', '{value}'], { key: 'key', value: 'value' }),
).toEqual(['key', 'value']);
});
- it('replace content in an object', () => {
+ it('should replace content in an object', () => {
expect(
substitute({ '{key}': '{value}' }, { key: 'key', value: 'value' }),
).toEqual({ key: 'value' });
});
- it('return the original content if not recognized', () => {
+ it('should return the original content if not recognized', () => {
expect(substitute(null, {})).toEqual(null);
});
});
diff --git a/packages/presetter/spec/utilities/display.spec.ts b/packages/presetter/spec/utilities/display.spec.ts
new file mode 100644
index 00000000..9ae11ea1
--- /dev/null
+++ b/packages/presetter/spec/utilities/display.spec.ts
@@ -0,0 +1,84 @@
+import { describe, expect, it } from 'vitest';
+
+import { display, prefixDisplay } from '#utilities/display';
+
+describe('fn:display', () => {
+ it('should return "Function" for function content', () => {
+ const result = display(() => {});
+ const expected = 'Function';
+
+ expect(result).toEqual(expected);
+ });
+
+ it('should return "Array(length)" for array content', () => {
+ const result = display([1, 2, 3]);
+ const expected = 'Array(3)';
+
+ expect(result).toEqual(expected);
+ });
+
+ it('should return "Buffer" for buffer content', () => {
+ const result = display(Buffer.from('test'));
+ const expected = 'Buffer';
+
+ expect(result).toEqual(expected);
+ });
+
+ it('should return "null" for null content', () => {
+ const result = display(null);
+ const expected = 'null';
+
+ expect(result).toEqual(expected);
+ });
+
+ it('should return "Object({...})" for object content', () => {
+ const result = display({ key: 'value' });
+ const expected = 'Object({\n key: string\n})';
+
+ expect(result).toEqual(expected);
+ });
+
+ it('should return the type of primitive content', () => {
+ expect(display(123)).toEqual('number');
+ expect(display('test')).toEqual('string');
+ expect(display(true)).toEqual('boolean');
+ });
+
+ it('should handle nested objects correctly', () => {
+ const result = display({ a: { b: { c: 'value' } } });
+ const expected = 'Object({\n a: Object({\n b: Object({...})\n })\n})';
+
+ expect(result).toEqual(expected);
+ });
+});
+
+describe('fn:prefixDisplay', () => {
+ it('should prefix the display output with the given prefix', () => {
+ const result = prefixDisplay('> ', { key: 'value' });
+ const expected = `
+> Object({
+ key: string
+ })`.trim();
+
+ expect(result).toEqual(expected);
+ });
+
+ it('should handle different prefixes correctly', () => {
+ const result = prefixDisplay('>> ', [1, 2, 3]);
+ const expected = '>> Array(3)';
+
+ expect(result).toEqual(expected);
+ });
+
+ it('should handle nested objects with prefix correctly', () => {
+ const result = prefixDisplay('> ', { a: { b: { c: 'value' } } });
+ const expected = `
+> Object({
+ a: Object({
+ b: Object({...})
+ })
+ })`.trim();
+
+ expect(result).toEqual(expected);
+ });
+});
diff --git a/packages/presetter/spec/utilities/filter.spec.ts b/packages/presetter/spec/utilities/filter.spec.ts
deleted file mode 100644
index 078c9c9b..00000000
--- a/packages/presetter/spec/utilities/filter.spec.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { describe, expect, it } from 'vitest';
-
-import { filter } from '#utilities/filter';
-
-describe('fn:filter', () => {
- it('return the subject without filtering', () => {
- expect(filter({ a: 'a' })).toEqual({
- a: 'a',
- });
- });
-
- it('filter out fields that are in the list', () => {
- expect(filter({ a: 'a', b: 'b', c: 'c' }, 'b', 'c')).toEqual({
- a: 'a',
- });
- });
-
- it('filter out fields deep', () => {
- expect(
- filter({ a: { aa: { aaa: 'aaa', aab: 'aab' } } }, { a: { aa: ['aab'] } }),
- ).toEqual({ a: { aa: { aaa: 'aaa' } } });
- });
-
- it('filter out a list deep', () => {
- expect(
- filter({ a: { aa: [0, 1, 2, 3, 4] } }, { a: { aa: [1, 3] } }),
- ).toEqual({ a: { aa: [0, 2, 4] } });
- });
-
- it('merge filtering rules', () => {
- expect(
- filter(
- { a: { aa: { aaa: 'aaa', aab: 'aab' } } },
- { a: { aa: ['aaa'] } },
- { a: { aa: ['aab'] } },
- ),
- ).toEqual({ a: { aa: {} } });
- });
-
- it('ignore non-existent paths', () => {
- expect(
- filter({ a: { aa: { aaa: 'aaa' } }, b: 'b' }, { a: { aa: ['aab'] } }),
- ).toEqual({ a: { aa: { aaa: 'aaa' } }, b: 'b' });
-
- expect(filter({ a: { aa: { aaa: 'aaa' } }, b: 'b' }, { a: ['b'] })).toEqual(
- { a: { aa: { aaa: 'aaa' } }, b: 'b' },
- );
-
- expect(filter({ a: { aa: { aaa: 'aaa' } }, b: 'b' }, { b: ['c'] })).toEqual(
- { a: { aa: { aaa: 'aaa' } }, b: 'b' },
- );
-
- expect(filter({ a: { aa: { aaa: 'aaa' } }, b: 'b' }, 'c')).toEqual({
- a: { aa: { aaa: 'aaa' } },
- b: 'b',
- });
- });
-});
diff --git a/packages/presetter/spec/utilities/object.spec.ts b/packages/presetter/spec/utilities/object.spec.ts
index 06ca781f..05bfcb01 100644
--- a/packages/presetter/spec/utilities/object.spec.ts
+++ b/packages/presetter/spec/utilities/object.spec.ts
@@ -1,3 +1,5 @@
+import * as testModule from 'node:assert';
+
import { describe, expect, it } from 'vitest';
import {
@@ -7,15 +9,15 @@ import {
} from '#utilities/object';
describe('fn:isJsonObject', () => {
- it('return true for a valid json object', () => {
+ it('should return true for a valid json object', () => {
expect(isJsonObject({})).toEqual(true);
});
- it('return false for an array', () => {
+ it('should return false for an array', () => {
expect(isJsonObject([])).toEqual(false);
});
- it('return false for an non json object', () => {
+ it('should return false for an non json object', () => {
expect(
isJsonObject({
foo: Buffer.from('foo'),
@@ -70,6 +72,9 @@ describe('fn:isPlainObject', () => {
expect(isPlainObject(new Date())).toEqual(false);
expect(isPlainObject(/foo/)).toEqual(false);
expect(isPlainObject(() => {})).toEqual(false);
- expect(isPlainObject(Object.create(null))).toEqual(false);
+ expect(isPlainObject(Object.create(null))).toEqual(true);
+ expect(isPlainObject(Object.create({}))).toEqual(true);
+ expect(isPlainObject(new Map())).toEqual(false);
+ expect(isPlainObject(testModule)).toEqual(true);
});
});
diff --git a/packages/types/package.json b/packages/types/package.json
index e0f4a517..cfa0fd4b 100644
--- a/packages/types/package.json
+++ b/packages/types/package.json
@@ -20,11 +20,14 @@
"url": "git+https://github.com/alvis/presetter.git"
},
"scripts": {
- "prepare": "tsc --declaration --moduleResolution node --module esnext --target esnext --outdir lib source/index.ts",
+ "prepare": "tsc --declaration --moduleResolution node --module esnext --target esnext --outdir lib source/index.ts && tsc-esm-fix --sourceMap --target lib",
"bootstrap": "presetter bootstrap",
"build": "run build",
+ "coverage": "run coverage --",
"lint": "run lint --",
- "prepublishOnly": "run prepare && run prepublishOnly"
+ "prepublishOnly": "run prepare && run prepublishOnly",
+ "test": "run test --",
+ "watch": "run watch --"
},
"dependencies": {
"type-fest": "catalog:"
diff --git a/packages/types/source/asset.ts b/packages/types/source/asset.ts
index bc0b0a4f..5d89906f 100644
--- a/packages/types/source/asset.ts
+++ b/packages/types/source/asset.ts
@@ -1,53 +1,75 @@
/* v8 ignore start */
-import type { Config } from './config';
-import type {
- ConfigMap,
- ConfigMapGenerator,
- Generator,
- TemplateMap,
- TemplateMapGenerator,
-} from './generator';
-import type { IgnorePath, IgnoreRule } from './ignore';
-import type { Template } from './template';
-
-/** 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 symlinks */
- noSymlinks?: string[] | Generator;
- /** path to the scripts template */
- scripts?: Record | string;
- /** variables to be substituted in templates */
- variable?: Record;
- /** supplementary configuration applied to .presetterrc for enriching other presets */
- supplementaryConfig?: ConfigMap | ConfigMapGenerator;
- /** a list of files not to be linked or fields to be ignores */
- supplementaryIgnores?: IgnoreRule[] | IgnoreRulesGenerator;
- /** path to the scripts template to be applied at end of preset merging */
- supplementaryScripts?: Record | string;
+import type { JsonObject } from 'type-fest';
+
+import type { Path } from './auxiliaries';
+import type { PresetContent } from './content';
+
+/**
+ * defines the structure for preset assets, including JSON, YAML, JS, TS, or custom file formats
+ */
+export interface PresetAssets {
+ /** defines ignored file patterns */
+ [list: `${Path}ignore`]: PresetContent;
+
+ /** defines JSON or YAML content */
+ [json: `${Path}.json` | `${Path}.yaml`]: PresetContent;
+
+ /** defines ES module content including JS, JSX, TS, and TSX formats */
+ [
+ esm:
+ | `${Path}.js`
+ | `${Path}.cjs`
+ | `${Path}.mjs`
+ | `${Path}.jsx`
+ | `${Path}.ts`
+ | `${Path}.cts`
+ | `${Path}.mts`
+ | `${Path}.tsx`
+ ]: PresetContent>;
+
+ /** defines other file contents, including file path, arrays, JSON objects, or arbitrary records */
+ [file: Path]:
+ | PresetContent
+ | PresetContent
+ | PresetContent
+ | PresetContent>;
}
-/** an auxiliary type for representing a dynamic ignore rules generator */
-export type IgnoreRulesGenerator = Generator;
-
-/** realized PresetAsset that doesn't need any further processing */
-export interface ResolvedPresetAsset extends Omit {
- /** mapping of files to be generated to its configuration template files (key: file path relative to the target project's root, value: content to be written to file) */
- template?: Record;
- /** list of templates that should not be created as symlinks */
- noSymlinks?: string[];
- /** path to the scripts template */
- scripts?: Record