commit dfebcbe9a59b03255ae16362701d4a5a2a5b593e Author: Alvis HT Tang Date: Thu Sep 30 11:13:13 2021 +0100 feat(preset-rollup): provide a preset for code bundling via rollup diff --git a/.presetterrc.json b/.presetterrc.json new file mode 100644 index 0000000..e33a3fe --- /dev/null +++ b/.presetterrc.json @@ -0,0 +1,9 @@ +{ + "preset": "presetter-preset", + "config": { + "npmignore": ["!/configs/**", "!/templates/**"] + }, + "variable": { + "root": "../.." + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..98c731d --- /dev/null +++ b/README.md @@ -0,0 +1,192 @@ +
+ +![Logo](https://github.com/alvis/presetter/raw/master/assets/logo.svg) + +🏄🏻 _A collection of opinionated configurations for a building a code bundle via rollup for presetter_ + +•   [Quick Start](#quick-start)   •   [Project Structure](#project-structure)   •   [Customisation](#customisation)   •   [Scripts](#script-template-summary)   • + +[![npm](https://img.shields.io/npm/v/presetter-preset-rollup?style=flat-square)](https://github.com/alvis/presetter/releases) +[![build](https://img.shields.io/github/workflow/status/alvis/presetter/code%20test?style=flat-square)](https://github.com/alvis/presetter/actions) +[![maintainability](https://img.shields.io/codeclimate/maintainability/alvis/presetter?style=flat-square)](https://codeclimate.com/github/alvis/presetter/maintainability) +[![coverage](https://img.shields.io/codeclimate/coverage/alvis/presetter?style=flat-square)](https://codeclimate.com/github/alvis/presetter/test_coverage) +[![security](https://img.shields.io/snyk/vulnerabilities/github/alvis/presetter/packages/preset-rollup/package.json.svg?style=flat-square)](https://snyk.io/test/github/alvis/presetter?targetFile=packages/preset-rollup/package.json&style=flat-square) +[![dependencies](https://img.shields.io/david/alvis/presetter?path=packages/preset-rollup&style=flat-square)](https://david-dm.org/alvis/presetter?path=packages/preset-rollup) +[![license](https://img.shields.io/github/license/alvis/presetter.svg?style=flat-square)](https://github.com/alvis/presetter/blob/master/LICENSE) + +
+ +## Features + +**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 +- 2️⃣ Dual CJS and ESM modules export by default +- 🍄 Common rollup packages included as one single bundle + - `@rollup/plugin-commonjs` + - `@rollup/plugin-graphql` + - `@rollup/plugin-image` + - `@rollup/plugin-json` + - `@rollup/plugin-yaml` + - `rollup` <~ of course including rollup itself + - `rollup-plugin-postcss` + - `rollup-plugin-ts` + - `rollup-plugin-tsconfig-paths` + - `rollup-plugin-visualizer` + +## Quick Start + +[**FULL DOCUMENTATION IS AVAILABLE HERE**](https://github.com/alvis/presetter/blob/master/README.md) + +1. Bootstrap your project with `presetter-preset` & `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! + +```json +{ + "main": "lib/index.js", + "module": "lib/index.mjs", + "types": "lib/index.d.ts", + "exports": { + "require": "./lib/index.js", + "import": "./lib/index.mjs" + } +} +``` + +## Project Structure + +After installing `presetter-preset` & `presetter-preset-rollup`, your project file structure should look like the following or with more configuration file if you also installed `presetter-preset`. + +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 [customisation](https://github.com/alvis/presetter/blob/master/packages/preset-rollup#customisation) section below for more details. + +``` +(root) + ├─ .git + ├─ .preseterrc.json + ├─ node_modules + ├─ source + │ ├─ + │ ├─ index.ts + │ ├─ (auxiliary).ts + ├─ spec + │ ├─ *.spec.ts + ├─ package.json + └─ rollup.config.ts +``` + +## Customisation + +By default, this preset exports a handy configuration for rollup for a typescript project. +But you can further customise (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 */ + 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) + // ... + + /** 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?: + | NormalisedRollupConfig['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}]]` + +## Script Template Summary + +- **`run build`**: Bundle your code via rollup +- **`run develop`**: Continuous code build and watch diff --git a/configs/rollup.yaml b/configs/rollup.yaml new file mode 100644 index 0000000..035d5b5 --- /dev/null +++ b/configs/rollup.yaml @@ -0,0 +1,26 @@ +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-ts[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[default]' diff --git a/package.json b/package.json new file mode 100644 index 0000000..2bfafed --- /dev/null +++ b/package.json @@ -0,0 +1,53 @@ +{ + "name": "presetter-preset-rollup", + "version": "0.0.0", + "description": "An opinionated presetter preset for using rollup as a bundler", + "keywords": [ + "presetter", + "preset" + ], + "homepage": "https://github.com/alvis/presetter#readme", + "bugs": { + "url": "https://github.com/alvis/presetter/issues" + }, + "license": "MIT", + "author": { + "name": "Alvis HT Tang", + "email": "alvis@hilbert.space" + }, + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "git+https://github.com/alvis/presetter.git" + }, + "scripts": { + "bootstrap": "presetter bootstrap && run prepare", + "build": "run build", + "coverage": "run coverage", + "lint": "run lint", + "prepublishOnly": "run prepare && run prepublishOnly", + "release": "run-s release:peer:*", + "release:peer:presetter": "npm pkg set peerDependencies.presetter=^$(npx -c 'echo $npm_package_version')", + "test": "run test", + "watch": "run watch" + }, + "peerDependencies": { + "@rollup/plugin-commonjs": "^20.0.0", + "@rollup/plugin-graphql": "^1.0.0", + "@rollup/plugin-image": "^2.0.0", + "@rollup/plugin-json": "^4.0.0", + "@rollup/plugin-node-resolve": "^13.0.0", + "@rollup/plugin-yaml": "^3.0.0", + "presetter": "^3.0.0", + "rollup": "^2.0.0", + "rollup-plugin-postcss": "^4.0.0", + "rollup-plugin-ts": "^1.0.0", + "rollup-plugin-tsconfig-paths": "^1.0.0", + "rollup-plugin-visualizer": "^5.0.0" + }, + "devDependencies": { + "presetter": "file:../presetter", + "presetter-preset": "file:../preset" + } +} diff --git a/source/index.ts b/source/index.ts new file mode 100644 index 0000000..65723b7 --- /dev/null +++ b/source/index.ts @@ -0,0 +1,72 @@ +/* + * *** MIT LICENSE *** + * ------------------------------------------------------------------------- + * This code may be modified and distributed under the MIT license. + * See the LICENSE file for details. + * ------------------------------------------------------------------------- + * + * @summary Collection of preset assets for bundling a project with rollup + * + * @author Alvis HT Tang + * @license MIT + * @copyright Copyright (c) 2021 - All Rights Reserved. + * ------------------------------------------------------------------------- + */ + +import { resolve } from 'path'; +import { loadFile, template } from 'presetter'; + +import { getRollupParameter } from './rollup'; + +import type { PresetAsset } from 'presetter'; + +import type { RollupConfig } from './rollup'; + +// paths to the template directory +const TEMPLATES = resolve(__dirname, '..', 'templates'); +const CONFIGS = resolve(__dirname, '..', 'configs'); + +/** config for this preset */ +export type PresetConfig = { + rollup?: RollupConfig; +}; + +/** List of configurable variables */ +export type Variable = { + /** 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: Variable = { + source: 'source', + output: 'lib', +}; + +/** + * get the list of templates provided by this preset + * @returns list of preset templates + */ +export default async function (): Promise { + return { + template: { + 'rollup.config.ts': async (context) => { + const content = await loadFile( + resolve(TEMPLATES, 'rollup.config.ts'), + 'text', + ); + const variable = await getRollupParameter(context); + + return template(content, variable); + }, + }, + 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/source/plugin.ts b/source/plugin.ts new file mode 100644 index 0000000..a4b5bf2 --- /dev/null +++ b/source/plugin.ts @@ -0,0 +1,117 @@ +/* + * *** MIT LICENSE *** + * ------------------------------------------------------------------------- + * This code may be modified and distributed under the MIT license. + * See the LICENSE file for details. + * ------------------------------------------------------------------------- + * + * @summary Collection of plugin related helpers + * + * @author Alvis HT Tang + * @license MIT + * @copyright Copyright (c) 2021 - All Rights Reserved. + * ------------------------------------------------------------------------- + */ + +import { isDirective } from 'presetter'; + +import type { ApplyDirective, ImportDirective } from 'presetter'; + +/** 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 = string | ApplyDirective; + +/** options for a plugin */ +export type PluginOptions = + | Record + | 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/source/rollup.ts b/source/rollup.ts new file mode 100644 index 0000000..dcbaf05 --- /dev/null +++ b/source/rollup.ts @@ -0,0 +1,224 @@ +/* + * *** MIT LICENSE *** + * ------------------------------------------------------------------------- + * This code may be modified and distributed under the MIT license. + * See the LICENSE file for details. + * ------------------------------------------------------------------------- + * + * @summary Collection of helpers for rollup + * + * @author Alvis HT Tang + * @license MIT + * @copyright Copyright (c) 2021 - All Rights Reserved. + * ------------------------------------------------------------------------- + */ + +import { + isDirective, + isJSON, + merge, + resolveDirective, + template, +} from 'presetter'; + +import { assertPluginManifest } from './plugin'; + +import type { + PluginConfiguration, + PluginList, + PluginManifest, + PluginObject, +} from './plugin'; +import type { + ApplyDirective, + ImportDirective, + ResolvedPresetContext, +} from 'presetter'; + + +/** preset configuration for rollup */ +export interface RollupConfig { + [index: string]: unknown | RollupConfig; + /** 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 | TrueRollupConfig; + /** list of plugin and its options */ + plugins?: PluginConfiguration[]; +} + +/** transformed configuration for rollup, with all plugins represented by an object */ +interface IntermediateRollupConfig { + [index: string]: unknown | IntermediateRollupConfig; + /** 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 async function getRollupParameter( + context: ResolvedPresetContext, +): Promise> { + const { config, variable } = context.custom; + + const normalisedConfig = template( + normaliseConfig(transformConfig({ ...config.rollup })), + variable, + ); + + return generateRollupParameter(normalisedConfig, context); +} + +/** + * generate template parameters for rollup + * @param config normalised 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 }; +} + +/** + * normalise rollup config with all plugins represented as a list + * @param config transformed config + * @returns config that rollup would take + */ +function normaliseConfig(config: IntermediateRollupConfig): TrueRollupConfig { + return Object.fromEntries( + Object.entries(config).map(([key, value]): [string, unknown] => { + return [ + key, + isDirective(value) ? value : normaliseConfigValue(key, value), + ]; + }), + ); +} + +/** + * try to normalise any nested configuration + * @param key field name + * @param value value of a field + * @returns normalised value + */ +function normaliseConfigValue(key: string, value: unknown): unknown { + switch (key) { + case 'plugins': + return [ + ...Object.entries(value as PluginObject) + .filter(([_, options]) => options !== null) + .map(([plugin, options]) => + [plugin, normaliseConfigValue(plugin, options)].filter( + (element) => element !== undefined, + ), + ), + ]; + default: + return isJSON(value) + ? normaliseConfig(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 isJSON(value) ? transformConfig(value) : value; + } +} + +/** + * objectify rollup plugins + * @param plugins rollup plugin config + * @returns normalised plugin config + */ +function objectifyPlugins( + plugins: PluginManifest, +): IntermediateRollupConfig['plugins'] { + const normalisedPlugin: PluginObject = {}; + + const pluginList: PluginConfiguration[] = Array.isArray(plugins) + ? arrayToPluginConfiguration(plugins) + : objectToPluginConfiguration(plugins); + + for (const [name, options] of pluginList) { + Object.assign( + normalisedPlugin, + merge(normalisedPlugin, { [name]: options }), + ); + } + + return normalisedPlugin; +} + +/** + * normalise rollup plugin config in array form + * @param plugins rollup plugin config in array form + * @returns normalised plugin config + */ +function arrayToPluginConfiguration( + plugins: PluginList, +): PluginConfiguration[] { + return plugins.map((plugin) => + typeof plugin === 'string' ? [plugin] : plugin, + ); +} + +/** + * normalise rollup plugin config in object form + * @param plugins rollup plugin config in object form + * @returns normalised plugin config + */ +function objectToPluginConfiguration( + plugins: PluginObject, +): PluginConfiguration[] { + return [...Object.entries(plugins)]; +} diff --git a/spec/index.spec.ts b/spec/index.spec.ts new file mode 100644 index 0000000..e216991 --- /dev/null +++ b/spec/index.spec.ts @@ -0,0 +1,52 @@ +/* + * *** MIT LICENSE *** + * ------------------------------------------------------------------------- + * This code may be modified and distributed under the MIT license. + * See the LICENSE file for details. + * ------------------------------------------------------------------------- + * + * @summary Tests on config generation + * + * @author Alvis HT Tang + * @license MIT + * @copyright Copyright (c) 2020 - All Rights Reserved. + * ------------------------------------------------------------------------- + */ + +import { readdirSync } from 'fs'; +import { resolve } from 'path'; +import { resolveContext, resolveDynamicMap } from 'presetter'; + +import getPresetAsset from '#index'; + +jest.mock('path', () => ({ + __esModule: true, + ...jest.requireActual('path'), + resolve: jest.fn(jest.requireActual('path').resolve), +})); + +describe('fn:getPresetAsset', () => { + it('use all templates', async () => { + const assets = [await getPresetAsset()]; + const context = await resolveContext(assets, { + target: { name: 'preset', root: '/', package: {} }, + custom: { preset: 'preset' }, + }); + + // load all potential dynamic content + await resolveDynamicMap(assets, context, 'supplementaryConfig'); + await resolveDynamicMap(assets, context, 'template'); + + const TEMPLATES = resolve(__dirname, '..', 'templates'); + const allTemplates = await readdirSync(TEMPLATES); + const CONFIGS = resolve(__dirname, '..', 'configs'); + const supplementaryConfig = await readdirSync(CONFIGS); + + for (const path of allTemplates) { + expect(resolve).toBeCalledWith(TEMPLATES, path); + } + for (const path of supplementaryConfig) { + expect(resolve).toBeCalledWith(CONFIGS, path); + } + }); +}); diff --git a/spec/plugin.spec.ts b/spec/plugin.spec.ts new file mode 100644 index 0000000..b95d3e3 --- /dev/null +++ b/spec/plugin.spec.ts @@ -0,0 +1,101 @@ +/* + * *** MIT LICENSE *** + * ------------------------------------------------------------------------- + * This code may be modified and distributed under the MIT license. + * See the LICENSE file for details. + * ------------------------------------------------------------------------- + * + * @summary Tests on plugin related helpers + * + * @author Alvis HT Tang + * @license MIT + * @copyright Copyright (c) 2021 - All Rights Reserved. + * ------------------------------------------------------------------------- + */ + +import { + assertPluginList, + assertPluginListItem, + assertPluginObject, + assertPluginManifest, +} 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/spec/rollup.spec.ts b/spec/rollup.spec.ts new file mode 100644 index 0000000..fbddf67 --- /dev/null +++ b/spec/rollup.spec.ts @@ -0,0 +1,220 @@ +/* + * *** MIT LICENSE *** + * ------------------------------------------------------------------------- + * This code may be modified and distributed under the MIT license. + * See the LICENSE file for details. + * ------------------------------------------------------------------------- + * + * @summary Tests on the helpers for rollup + * + * @author Alvis HT Tang + * @license MIT + * @copyright Copyright (c) 2021 - All Rights Reserved. + * ------------------------------------------------------------------------- + */ + +import { getRollupParameter } from '#rollup'; + +import type { Config, ResolvedPresetContext } from 'presetter'; + +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( + await 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( + await getRollupParameter( + generateContext({ + plugins: { '@apply newPlugin': { name: 'newPlugin' } }, + }), + ), + ).toEqual({ + rollupImport: `import * as import0 from 'newPlugin';`, + rollupExport: + 'export default {"plugins": [import0(...[{"name": "newPlugin"}])]}', + }); + }); + + it('add a plugin by just the plugin name, using everything default', async () => { + expect( + await getRollupParameter( + generateContext({ + plugins: ['@apply newPlugin'], + }), + ), + ).toEqual({ + rollupImport: `import * as import0 from 'newPlugin';`, + rollupExport: 'export default {"plugins": [import0(...[])]}', + }); + }); + + it('add a plugin by adding the plugin in the array form, using everything default', async () => { + expect( + await getRollupParameter( + generateContext({ + plugins: [['@apply newPlugin']], + }), + ), + ).toEqual({ + rollupImport: `import * as import0 from 'newPlugin';`, + rollupExport: 'export default {"plugins": [import0(...[])]}', + }); + }); + + it('add a plugin by adding the plugin in the array form, using the supplied options', async () => { + expect( + await getRollupParameter( + generateContext({ + plugins: [['@apply newPlugin', { name: 'newPlugin' }]], + }), + ), + ).toEqual({ + rollupImport: `import * as import0 from 'newPlugin';`, + rollupExport: + 'export default {"plugins": [import0(...[{"name": "newPlugin"}])]}', + }); + }); + + it('remove a plugin by setting the plugin config as null', async () => { + expect( + await getRollupParameter( + generateContext({ + plugins: { + '@apply pluginWithOptions': null, + }, + }), + ), + ).toEqual({ + rollupImport: ``, + rollupExport: 'export default {"plugins": []}', + }); + }); + + it('add a plugin from a named import', async () => { + expect( + await 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}])]}', + }); + }); + + it('generate default parameters if no further config is given', async () => { + expect(await getRollupParameter(generateContext())).toEqual({ + rollupImport: ``, + rollupExport: 'export default {}', + }); + }); + + it('generate config with extra options other than plugins', async () => { + expect( + await 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( + await 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]), import2(...[import0])]}`, + }); + + expect( + await 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]), import2(...[import0])]}`, + }); + }); + + it('generate only one import statement per unique import', async () => { + expect( + await 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]), import2(...[import0.export1])]}`, + }); + }); + + it('support nested plugin declaration', async () => { + expect( + await 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]]}])]}`, + }); + }); +}); diff --git a/templates/rollup.config.ts b/templates/rollup.config.ts new file mode 100644 index 0000000..dc2365f --- /dev/null +++ b/templates/rollup.config.ts @@ -0,0 +1,3 @@ +{rollupImport} + +{rollupExport} \ No newline at end of file diff --git a/templates/scripts.yaml b/templates/scripts.yaml new file mode 100644 index 0000000..8f5b630 --- /dev/null +++ b/templates/scripts.yaml @@ -0,0 +1,5 @@ +# replace the `prepare` template from presetter-preset +# so that the build procedure will not be triggered upon package installation +build: run-s clean build:rollup +build:rollup: cross-env NODE_ENV=production rollup --config rollup.config.ts --configPlugin rollup-plugin-ts +develop: run-s "build:rollup -- --watch {@}" --