diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5e5f9bf --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,165 @@ +# Changelog + +All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + +## [4.0.1](https://github.com/alvis/presetter/compare/v4.0.0...v4.0.1) (2023-06-21) + + + +# [4.0.0](https://github.com/alvis/presetter/compare/v3.5.5...v4.0.0) (2023-06-18) + + +### ✨ Features + +* convert the whole project into ESM ([381c84f](https://github.com/alvis/presetter/commit/381c84f59605faf9ce6c2b3f81c2eea81bfebc61)) +* **types:** separate types from presetter ([4ff3626](https://github.com/alvis/presetter/commit/4ff3626d915649208067b2235645e8534508f341)) + + +### 🐛 Bug Fixes + +* **preset-rollup:** inject NODE_ENV for build and develop stages ([b09ade5](https://github.com/alvis/presetter/commit/b09ade516250b8b5256d7f3d52ab1a30102607b5)) +* **preset-rollup:** make the rollup preset compatible with ESM ([86154b8](https://github.com/alvis/presetter/commit/86154b8daaf7e3f97c1a93753416263fe7b086dc)) +* update outdated packages ([bbaf33f](https://github.com/alvis/presetter/commit/bbaf33f0eaeaeca2c296cfc68207b4ff4632674f)) + + +### 📦 Code Refactoring + +* **preset-rollup:** normalize config in functional way ([f6b3815](https://github.com/alvis/presetter/commit/f6b3815a13309791852ba2442e0a48e6531f8d45)) + + +### ♻️ Chores + +* allow tests can be done individually ([b86f6ee](https://github.com/alvis/presetter/commit/b86f6eef689f7783a2a19841eaadf6e581dbb649)) +* use pnpm for workspace management ([358d0d7](https://github.com/alvis/presetter/commit/358d0d77cdc2b37affb36931f13568464f1a46ab)) + + +### 💎 Styles + +* make any node build-in modules explicitly imported first ([90730f2](https://github.com/alvis/presetter/commit/90730f2dfd27a6a6f1fbceb26ac5fb974e11508e)) + + +### Breaking changes + +* - by default, client project emit ESM and for clarity preset-essentials has been renamed to preset-esm +- ts-node is removed in favor of a more powerful and less buggy tsx + + + +## [3.5.5](https://github.com/alvis/presetter/compare/v3.5.4...v3.5.5) (2023-03-31) + + + +## [3.5.4](https://github.com/alvis/presetter/compare/v3.5.3...v3.5.4) (2023-03-30) + + + +## [3.5.3](https://github.com/alvis/presetter/compare/v3.5.2...v3.5.3) (2023-03-26) + + + +## [3.5.2](https://github.com/alvis/presetter/compare/v3.5.1...v3.5.2) (2023-03-26) + + + +## [3.5.1](https://github.com/alvis/presetter/compare/v3.5.0...v3.5.1) (2023-03-18) + + + +# [3.5.0](https://github.com/alvis/presetter/compare/v3.4.1...v3.5.0) (2022-09-23) + + +### 🛠 Builds + +* move .presetterrc to the root ([0eb17a0](https://github.com/alvis/presetter/commit/0eb17a058fa245faae96dcb017184460dae08b46)) + + +### 📦 Code Refactoring + +* **presetter:** remove dependency on fs-extra ([8810c56](https://github.com/alvis/presetter/commit/8810c568d1cf1de275b055916b6aeac450b43403)) +* **rollup:** remove unnecessary async waits ([9d9c4c3](https://github.com/alvis/presetter/commit/9d9c4c3b5131548512574634414214380cb501f2)) + + + +## [3.4.1](https://github.com/alvis/presetter/compare/v3.4.0...v3.4.1) (2022-08-10) + + +### 🐛 Bug Fixes + +* **presetter:** merge arrays only if the values are all primitive ([961f30c](https://github.com/alvis/presetter/commit/961f30c8fcbf9ba6f6fbf9e4d90cf0cb8c5a1069)) +* update packages ([ec8307d](https://github.com/alvis/presetter/commit/ec8307d79468f8529b29a0f7b356b4368c03302c)) + + + +# [3.4.0](https://github.com/alvis/presetter/compare/v3.3.0...v3.4.0) (2022-08-02) + + +### 🐛 Bug Fixes + +* **presetter:** resolve presets as a tree not a list ([0db5ff3](https://github.com/alvis/presetter/commit/0db5ff3b0020735591b3986c216578857dde3039)) + + + +# [3.3.0](https://github.com/alvis/presetter/compare/v3.2.0...v3.3.0) (2022-07-12) + + +### 📚 Documentation + +* update dependencies badges ([94c084d](https://github.com/alvis/presetter/commit/94c084ddc39821106c9077bd4c12d1a7da9529eb)) + + + +# [3.2.0](https://github.com/alvis/presetter/compare/v3.1.1...v3.2.0) (2022-05-18) + + +### 📦 Code Refactoring + +* use American English ([17315b7](https://github.com/alvis/presetter/commit/17315b78a9d65fb6193a609dd27b4d34454df1bc)) + + +### ♻️ Chores + +* update package dependencies ([4e50e0d](https://github.com/alvis/presetter/commit/4e50e0d2fce1bea470fbea62fc1eba52dccb3aed)) + + + +## [3.1.1](https://github.com/alvis/presetter/compare/v3.1.0...v3.1.1) (2021-12-29) + + + +# [3.1.0](https://github.com/alvis/presetter/compare/v3.0.3...v3.1.0) (2021-12-28) + + + +## [3.0.3](https://github.com/alvis/presetter/compare/v3.0.2...v3.0.3) (2021-11-03) + + + +## [3.0.2](https://github.com/alvis/presetter/compare/v3.0.1...v3.0.2) (2021-11-03) + + + +## [3.0.1](https://github.com/alvis/presetter/compare/v3.0.0...v3.0.1) (2021-10-14) + + + +# [3.0.0](https://github.com/alvis/presetter/compare/v2.1.0...v3.0.0) (2021-10-04) + + +### ✨ Features + +* **preset-rollup:** provide a preset for code bundling via rollup ([b0704b0](https://github.com/alvis/presetter/commit/b0704b0fe727c2724d7057d3803ecee49c230eff)) + + +### 🐛 Bug Fixes + +* **presetter:** narrow types in directive's argument ([c6de804](https://github.com/alvis/presetter/commit/c6de8044d4d9f584e3ef4ef688f81c08db425d1c)) + + +### 🛠 Builds + +* use presetter-preset-strict wherever possible ([4ff72a1](https://github.com/alvis/presetter/commit/4ff72a1a9730dfd4ad99d0a63dd4b005041ce759)) + + +### 📦 Code Refactoring + +* rename presetter-preset to presetter-preset-essentials ([b799587](https://github.com/alvis/presetter/commit/b7995871d85a1ccb8d2e43ba9b3a7e305de7a99b)) diff --git a/configs/rollup.yaml b/configs/rollup.yaml new file mode 100644 index 0000000..a812de1 --- /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[visualizer]' diff --git a/package.json b/package.json new file mode 100644 index 0000000..6eaa7ab --- /dev/null +++ b/package.json @@ -0,0 +1,55 @@ +{ + "name": "presetter-preset-rollup", + "version": "4.0.1", + "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", + "type": "module", + "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", + "test": "run test --", + "watch": "run watch --" + }, + "peerDependencies": { + "@rollup/plugin-commonjs": "^25.0.0", + "@rollup/plugin-graphql": "^2.0.0", + "@rollup/plugin-image": "^3.0.0", + "@rollup/plugin-json": "^6.0.0", + "@rollup/plugin-node-resolve": "^15.0.0", + "@rollup/plugin-yaml": "^4.0.0", + "presetter": "workspace:*", + "rollup": "^3.0.0", + "rollup-plugin-postcss": "^4.0.0", + "rollup-plugin-ts": "^3.0.0", + "rollup-plugin-tsconfig-paths": "^1.0.0", + "rollup-plugin-visualizer": "^5.0.0" + }, + "devDependencies": { + "presetter": "workspace:*" + }, + "dependencies": { + "presetter-types": "workspace:*", + "type-fest": "^3.12.0" + } +} diff --git a/source/index.ts b/source/index.ts new file mode 100644 index 0000000..2e691bd --- /dev/null +++ b/source/index.ts @@ -0,0 +1,78 @@ +/* + * *** 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 { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { loadFile, template } from 'presetter'; + +import { getRollupParameter } from './rollup'; + +import type { PresetAsset } from 'presetter-types'; + +import type { RollupConfig } from './rollup'; + +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 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 function (): PresetAsset { + return { + template: { + /* eslint-disable @typescript-eslint/naming-convention */ + 'rollup.config.ts': (context) => { + const content = loadFile( + resolve(TEMPLATES, 'rollup.config.ts'), + 'text', + ); + const variable = getRollupParameter(context); + + return template(content, variable); + /* eslint-enable @typescript-eslint/naming-convention */ + }, + }, + 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..9613232 --- /dev/null +++ b/source/plugin.ts @@ -0,0 +1,115 @@ +/* + * *** 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'; +import type { JsonObject } 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 = string | ApplyDirective; + +/** 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/source/rollup.ts b/source/rollup.ts new file mode 100644 index 0000000..403f683 --- /dev/null +++ b/source/rollup.ts @@ -0,0 +1,216 @@ +/* + * *** 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 { 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 | 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 function getRollupParameter( + context: ResolvedPresetContext, +): Record<'rollupImport' | 'rollupExport', string> { + const { config, variable } = context.custom; + + const normalizedConfig = template( + 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 isJSON(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 isJSON(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/spec/index.spec.ts b/spec/index.spec.ts new file mode 100644 index 0000000..1e57354 --- /dev/null +++ b/spec/index.spec.ts @@ -0,0 +1,62 @@ +/* + * *** 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 { existsSync, readdirSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import * as pathNode from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { jest } from '@jest/globals'; + +const __dir = fileURLToPath(dirname(import.meta.url)); + +jest.unstable_mockModule('node:path', () => ({ + ...pathNode, + // spy on resolve to check if a template is referenced + resolve: jest.fn(resolve), +})); + +const { resolve: resolveSpyed } = await import('node:path'); +const { loadDynamicMap, resolveContext } = await import('presetter'); + +const { default: getPresetAsset } = await import('#index'); +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(__dir, '..', 'configs'); + const configs = (existsSync(CONFIGS) && readdirSync(CONFIGS)) || []; + const TEMPLATES = resolve(__dir, '..', 'templates'); + const templates = (existsSync(TEMPLATES) && readdirSync(TEMPLATES)) || []; + + for (const path of configs) { + expect(resolveSpyed).toBeCalledWith(CONFIGS, path); + } + for (const path of templates) { + expect(resolveSpyed).toBeCalledWith(TEMPLATES, 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..453df85 --- /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-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( + 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"}] as const))]}', + }); + }); + + 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(...([] as const))]}', + }); + }); + + 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(...([] as const))]}', + }); + }); + + 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"}] as const))]}', + }); + }); + + 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}] as const))]}', + }); + }); + + 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] as const)), import2(...([import0] as const))]}`, + }); + + 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] as const)), import2(...([import0] as const))]}`, + }); + }); + + 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] as const)), import2(...([import0.export1] as const))]}`, + }); + }); + + 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]]}] as const))]}`, + }); + }); +}); 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..865950f --- /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: cross-env NODE_ENV=production run-s clean build:rollup +build:rollup: rollup --config rollup.config.ts --configPlugin rollup-plugin-ts +develop: cross-env NODE_ENV=development run-s "build:rollup -- --watch {@}" --