diff --git a/packages/data-uri/CHANGELOG.md b/packages/data-uri/CHANGELOG.md new file mode 100644 index 000000000..b96742d0e --- /dev/null +++ b/packages/data-uri/CHANGELOG.md @@ -0,0 +1,5 @@ +# @rollup/plugin-data-uri ChangeLog + +## 0.1.0 + +- First Release diff --git a/packages/data-uri/README.md b/packages/data-uri/README.md new file mode 100644 index 000000000..75e4bd883 --- /dev/null +++ b/packages/data-uri/README.md @@ -0,0 +1,74 @@ +[npm]: https://img.shields.io/npm/v/@rollup/plugin-data-uri +[npm-url]: https://www.npmjs.com/package/@rollup/plugin-data-uri +[size]: https://packagephobia.now.sh/badge?p=@rollup/plugin-data-uri +[size-url]: https://packagephobia.now.sh/result?p=@rollup/plugin-data-uri + +[![npm][npm]][npm-url] +[![size][size]][size-url] +[![libera manifesto](https://img.shields.io/badge/libera-manifesto-lightgrey.svg)](https://liberamanifesto.com) + +# @rollup/plugin-data-uri + +🍣 A Rollup plugin which imports modules from Data URIs. + +## Requirements + +This plugin requires an [LTS](https://github.com/nodejs/Release) Node version (v8.0.0+) and Rollup v1.20.0+. + +## Install + +Using npm: + +```console +npm install @rollup/plugin-data-uri --save-dev +``` + +## Usage + +Create a `rollup.config.js` [configuration file](https://www.rollupjs.org/guide/en/#configuration-files) and import the plugin: + +```js +import dataUri from '@rollup/plugin-data-uri'; + +module.exports = { + input: 'src/index.js', + output: { + dir: 'output', + format: 'cjs' + }, + plugins: [dataUri()] +}; +``` + +Then call `rollup` either via the [CLI](https://www.rollupjs.org/guide/en/#command-line-reference) or the [API](https://www.rollupjs.org/guide/en/#javascript-api). If the build produces any errors, the plugin will write a "data-uri" character to stderr, which should be audible on most systems. + +## Options + +This plugin currently has no available options. + +## Supported MIME Types + +The following MIME types are supported by this plugin: + +- `text/javascript` +- `application/json` + +This mirrors support in the [latest version of Node.js](https://nodejs.org/api/esm.html#esm_data_imports), with the exception of WebAssembly support. + +## Base64 Encoding + +Base64 encoding is supported for well-formed `data:` URIs. For example: + +```js +import batman from 'data:application/json;base64, eyAiYmF0bWFuIjogInRydWUiIH0='; +``` + +## Dynamic Imports + +Dynamic imports, such as `import('data:application/json, { "batman": "true" }')`, aren't supported by this plugin. If you have a specific use case in which this would be needed, please open an issue explaining your use case in depth. + +## Meta + +[CONTRIBUTING](/.github/CONTRIBUTING.md) + +[LICENSE (MIT)](/LICENSE) diff --git a/packages/data-uri/package.json b/packages/data-uri/package.json new file mode 100644 index 000000000..d59efaf86 --- /dev/null +++ b/packages/data-uri/package.json @@ -0,0 +1,74 @@ +{ + "name": "@rollup/plugin-data-uri", + "version": "0.1.0", + "publishConfig": { + "access": "public" + }, + "description": "Import modules from Data URIs", + "license": "MIT", + "repository": "rollup/plugins", + "author": "shellscape", + "homepage": "https://github.com/rollup/plugins/tree/master/packages/data-uri", + "bugs": "https://github.com/rollup/plugins/issues", + "main": "dist/index.js", + "engines": { + "node": ">= 8.0.0" + }, + "scripts": { + "build": "rollup -c", + "ci:coverage": "nyc pnpm run test && nyc report --reporter=text-lcov > coverage.lcov", + "ci:lint": "pnpm run build && pnpm run lint", + "ci:lint:commits": "commitlint --from=${CIRCLE_BRANCH} --to=${CIRCLE_SHA1}", + "ci:test": "pnpm run test -- --verbose", + "lint": "pnpm run lint:js && pnpm run lint:docs && pnpm run lint:package", + "lint:docs": "prettier --single-quote --write README.md", + "lint:js": "eslint --fix --cache src test --ext .js,.ts", + "lint:package": "prettier --write package.json --plugin=prettier-plugin-package", + "prebuild": "del-cli dist", + "prepublishOnly": "pnpm run lint && pnpm run build", + "pretest": "pnpm run build -- --sourcemap", + "test": "ava" + }, + "files": [ + "dist", + "types", + "README.md", + "LICENSE" + ], + "keywords": [ + "data", + "data-uri", + "data-url", + "plugin", + "rollup", + "uri", + "url" + ], + "peerDependencies": { + "rollup": "^1.20.0" + }, + "devDependencies": { + "@rollup/plugin-typescript": "^2.1.0", + "@rollup/pluginutils": "^3.0.1", + "rollup": "^1.27.14", + "typescript": "^3.7.4" + }, + "ava": { + "compileEnhancements": false, + "extensions": [ + "ts" + ], + "require": [ + "ts-node/register" + ], + "files": [ + "!**/fixtures/**", + "!**/output/**", + "!**/helpers/**", + "!**/recipes/**", + "!**/types.ts" + ] + }, + "module": "dist/index.es.js", + "types": "types/index.d.ts" +} diff --git a/packages/data-uri/rollup.config.js b/packages/data-uri/rollup.config.js new file mode 100644 index 000000000..9157309fc --- /dev/null +++ b/packages/data-uri/rollup.config.js @@ -0,0 +1,13 @@ +import typescript from '@rollup/plugin-typescript'; + +import pkg from './package.json'; + +export default { + input: 'src/index.ts', + plugins: [typescript()], + external: [...Object.keys(pkg.devDependencies), 'url'], + output: [ + { format: 'cjs', file: pkg.main, sourcemap: true }, + { format: 'esm', file: pkg.module, sourcemap: true } + ] +}; diff --git a/packages/data-uri/src/index.ts b/packages/data-uri/src/index.ts new file mode 100644 index 000000000..a4d34bdf6 --- /dev/null +++ b/packages/data-uri/src/index.ts @@ -0,0 +1,86 @@ +import { URL } from 'url'; + +import { Plugin, RollupError } from 'rollup'; + +import { dataToEsm } from '@rollup/pluginutils'; + +const reDataUri = /^([^/]+\/[^;,]+)(;base64)?,([\s\S]*)$/; +const mimeTypes = { + js: 'text/javascript', + json: 'application/json' +}; + +export default function dataUri(): Plugin { + const resolved: { [key: string]: { mime: string | null; content: string | null } } = {}; + + return { + name: 'dataUri', + + resolveId(id) { + if (resolved[id]) { + return id; + } + + if (!reDataUri.test(id)) { + return null; + } + + const uri = new URL(id); + + if (uri.protocol !== 'data:') { + return null; + } + + const empty = [null, null, null, null, null]; + const [, mime, format, data] = reDataUri.exec(uri.pathname) || empty; + + if (Object.values(mimeTypes).includes(mime as string) && data) { + const base64 = format && /base64/i.test(format.substring(1)); + const content = base64 ? Buffer.from(data, 'base64').toString('utf-8') : data; + + resolved[id] = { mime, content }; + + return id; + } + + return null; + }, + + load(id) { + if (!resolved[id]) { + return null; + } + + const { mime, content } = resolved[id]; + + if (!content) { + return null; + } + + if (mime === 'text/javascript') { + return content; + } else if (mime === 'application/json') { + let json = ''; + try { + json = JSON.parse(content); + } catch (e) { + const error: RollupError = { + message: e.toString(), + parserError: e, + plugin: '@rollup/plugin-data-uri', + pluginCode: 'DU$JSON' + }; + this.error(error); + } + + return dataToEsm(json, { + preferConst: true, + compact: false, + namedExports: true, + indent: ' ' + }); + } + return null; + } + }; +} diff --git a/packages/data-uri/test/fixtures/.eslintrc b/packages/data-uri/test/fixtures/.eslintrc new file mode 100644 index 000000000..2203f57ac --- /dev/null +++ b/packages/data-uri/test/fixtures/.eslintrc @@ -0,0 +1,9 @@ +{ + "globals": { + "t": true + }, + "rules": { + "import/extensions": "off", + "import/no-unresolved": "off" + } +} diff --git a/packages/data-uri/test/fixtures/bad-json.js b/packages/data-uri/test/fixtures/bad-json.js new file mode 100644 index 000000000..e738f274f --- /dev/null +++ b/packages/data-uri/test/fixtures/bad-json.js @@ -0,0 +1 @@ +import 'data:application/json, { "batman": }'; diff --git a/packages/data-uri/test/fixtures/base64.js b/packages/data-uri/test/fixtures/base64.js new file mode 100644 index 000000000..c09849e4a --- /dev/null +++ b/packages/data-uri/test/fixtures/base64.js @@ -0,0 +1,4 @@ +import batman from 'data:application/json;base64, eyAiYmF0bWFuIjogInRydWUiIH0='; + +t.truthy(batman.batman); +t.snapshot(batman); diff --git a/packages/data-uri/test/fixtures/import.js b/packages/data-uri/test/fixtures/import.js new file mode 100644 index 000000000..9df924148 --- /dev/null +++ b/packages/data-uri/test/fixtures/import.js @@ -0,0 +1,4 @@ +import 'data:text/javascript, t.truthy(true);'; +import { batman } from 'data:text/javascript, export const batman = true;\nconst joker = false;\nexport default joker;'; + +t.snapshot(batman); diff --git a/packages/data-uri/test/fixtures/json.js b/packages/data-uri/test/fixtures/json.js new file mode 100644 index 000000000..ebda7b114 --- /dev/null +++ b/packages/data-uri/test/fixtures/json.js @@ -0,0 +1,4 @@ +import batman from 'data:application/json, { "batman": "true" }'; + +t.truthy(batman.batman); +t.snapshot(batman); diff --git a/packages/data-uri/test/snapshots/test.js.md b/packages/data-uri/test/snapshots/test.js.md new file mode 100644 index 000000000..f33eda5d1 --- /dev/null +++ b/packages/data-uri/test/snapshots/test.js.md @@ -0,0 +1,74 @@ +# Snapshot report for `test/test.js` + +The actual snapshot is saved in `test.js.snap`. + +Generated by [AVA](https://ava.li). + +## bad json + +> Snapshot 1 + + { + code: 'PLUGIN_ERROR', + plugin: 'dataUri', + pluginCode: 'DU$JSON', + } + +## import + +> Snapshot 1 + + true + +> Snapshot 2 + + `'use strict';␊ + ␊ + t.truthy(true);␊ + ␊ + const batman = true;␊ + ␊ + t.snapshot(batman);␊ + ` + +## json + +> Snapshot 1 + + { + batman: 'true', + } + +> Snapshot 2 + + `'use strict';␊ + ␊ + const batman = "true";␊ + var batman$1 = {␊ + batman: batman␊ + };␊ + ␊ + t.truthy(batman$1.batman);␊ + t.snapshot(batman$1);␊ + ` + +## base64 + +> Snapshot 1 + + { + batman: 'true', + } + +> Snapshot 2 + + `'use strict';␊ + ␊ + const batman = "true";␊ + var batman$1 = {␊ + batman: batman␊ + };␊ + ␊ + t.truthy(batman$1.batman);␊ + t.snapshot(batman$1);␊ + ` diff --git a/packages/data-uri/test/snapshots/test.js.snap b/packages/data-uri/test/snapshots/test.js.snap new file mode 100644 index 000000000..556e4cf3b Binary files /dev/null and b/packages/data-uri/test/snapshots/test.js.snap differ diff --git a/packages/data-uri/test/snapshots/test.ts.md b/packages/data-uri/test/snapshots/test.ts.md new file mode 100644 index 000000000..447bba4a8 --- /dev/null +++ b/packages/data-uri/test/snapshots/test.ts.md @@ -0,0 +1,53 @@ +# Snapshot report for `test/test.ts` + +The actual snapshot is saved in `test.ts.snap`. + +Generated by [AVA](https://ava.li). + +## bad json + +> Snapshot 1 + + { + code: 'PLUGIN_ERROR', + plugin: 'dataUri', + pluginCode: 'DU$JSON', + } + +## import + +> Snapshot 1 + + true + +> Snapshot 2 + + `'use strict';␊ + ␊ + t.truthy(true);␊ + ␊ + const batman = true;␊ + ␊ + t.snapshot(batman);␊ + ` + +## json + +> Snapshot 1 + + { + batman: 'true', + } + +> Snapshot 2 + + `'use strict';␊ + ␊ + const batman = "true";␊ + var batman$1 = {␊ + batman: batman␊ + };␊ + ␊ + t.truthy(batman$1.batman);␊ + t.snapshot(batman$1);␊ + ` diff --git a/packages/data-uri/test/snapshots/test.ts.snap b/packages/data-uri/test/snapshots/test.ts.snap new file mode 100644 index 000000000..25b1d32d7 Binary files /dev/null and b/packages/data-uri/test/snapshots/test.ts.snap differ diff --git a/packages/data-uri/test/test.js b/packages/data-uri/test/test.js new file mode 100644 index 000000000..07cbe846c --- /dev/null +++ b/packages/data-uri/test/test.js @@ -0,0 +1,50 @@ +import { join } from 'path'; + +import test from 'ava'; +import { rollup } from 'rollup'; + +import { testBundle } from '../../../util/test'; + +import dataUri from '..'; + +process.chdir(join(__dirname, 'fixtures')); + +test('json', async (t) => { + t.plan(3); + const bundle = await rollup({ + input: 'json.js', + plugins: [dataUri()] + }); + const { code } = await testBundle(t, bundle); + t.snapshot(code); +}); + +test('import', async (t) => { + t.plan(3); + const bundle = await rollup({ + input: 'import.js', + plugins: [dataUri()] + }); + const { code } = await testBundle(t, bundle); + t.snapshot(code); +}); + +test('bad json', async (t) => { + const fn = () => + rollup({ + input: 'bad-json.js', + plugins: [dataUri()] + }); + const { code, plugin, pluginCode } = await t.throwsAsync(fn); + t.snapshot({ code, plugin, pluginCode }); +}); + +test('base64', async (t) => { + t.plan(3); + const bundle = await rollup({ + input: 'base64.js', + plugins: [dataUri()] + }); + const { code } = await testBundle(t, bundle); + t.snapshot(code); +}); diff --git a/packages/data-uri/tsconfig.json b/packages/data-uri/tsconfig.json new file mode 100644 index 000000000..f00b3bc9b --- /dev/null +++ b/packages/data-uri/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["es6", "dom"], + "esModuleInterop": true + }, + "exclude": ["test/fixtures/*.*"] +} diff --git a/packages/data-uri/types/index.d.ts b/packages/data-uri/types/index.d.ts new file mode 100644 index 000000000..8dc7336cc --- /dev/null +++ b/packages/data-uri/types/index.d.ts @@ -0,0 +1,6 @@ +import { Plugin } from 'rollup'; + +/** + * A Rollup plugin which imports modules from Data URIs. + */ +export default function dataUri(): Plugin; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 34bbad079..d75856a49 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -154,6 +154,17 @@ importers: source-map: ^0.6.1 source-map-support: ^0.5.12 typescript: ^3.5.2 + packages/data-uri: + devDependencies: + '@rollup/plugin-typescript': 2.1.0_rollup@1.27.14+typescript@3.7.4 + '@rollup/pluginutils': 3.0.1_rollup@1.27.14 + rollup: 1.27.14 + typescript: 3.7.4 + specifiers: + '@rollup/plugin-typescript': ^2.1.0 + '@rollup/pluginutils': ^3.0.1 + rollup: ^1.27.14 + typescript: ^3.7.4 packages/dsv: dependencies: d3-dsv: 0.1.14 @@ -1299,6 +1310,21 @@ packages: typescript: '>=2.1.0' resolution: integrity: sha512-UA/bN/DlHN19xdOllXmp7G7pM2ac9dQMg0q2T1rg4Bogzb7oHXj2WGafpiNpEm54PivcJdzGRJvRnI6zCISW3w== + /@rollup/plugin-typescript/2.1.0_rollup@1.27.14+typescript@3.7.4: + dependencies: + '@rollup/pluginutils': 3.0.3_rollup@1.27.14 + resolve: 1.14.2 + rollup: 1.27.14 + typescript: 3.7.4 + dev: true + engines: + node: '>=8.0.0' + peerDependencies: + rollup: ^1.20.0 + tslib: '*' + typescript: '>=2.1.0' + resolution: + integrity: sha512-7lXKGY06aofrceVez/YnN2axttFdHSqlUBpCJ6ebzDfxwLDKMgSV5lD4ykBcdgE7aK3egxuLkD/HKyRB5L8Log== /@rollup/pluginutils/3.0.1_rollup@1.27.14: dependencies: estree-walker: 0.6.1 @@ -1309,6 +1335,17 @@ packages: rollup: ^1.20.0 resolution: integrity: sha512-PmNurkecagFimv7ZdKCVOfQuqKDPkrcpLFxRBcQ00LYr4HAjJwhCFxBiY2Xoletll2htTIiXBg6g0Yg21h2M3w== + /@rollup/pluginutils/3.0.3_rollup@1.27.14: + dependencies: + estree-walker: 0.6.1 + rollup: 1.27.14 + dev: true + engines: + node: '>= 8.0.0' + peerDependencies: + rollup: ^1.20.0 + resolution: + integrity: sha512-lxys3KlfYU8LOGu7+UL2upa3kM9q8ISo4EK+vSqqJCLk6N74HsUZbUFiElolh4BUsv0jeMGtYDzpNrtrShE6Gg== /@samverschueren/stream-to-observable/0.3.0: dependencies: any-observable: 0.3.0 @@ -1388,7 +1425,7 @@ packages: dependencies: '@types/events': 3.0.0 '@types/minimatch': 3.0.3 - '@types/node': 13.1.2 + '@types/node': 13.1.4 dev: true resolution: integrity: sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w== @@ -1440,6 +1477,10 @@ packages: /@types/node/13.1.2: resolution: integrity: sha512-B8emQA1qeKerqd1dmIsQYnXi+mmAzTB7flExjmy5X1aVAKFNNNDubkavwR13kR6JnpeLp3aLoJhwn9trWPAyFQ== + /@types/node/13.1.4: + dev: true + resolution: + integrity: sha512-Lue/mlp2egZJoHXZr4LndxDAd7i/7SQYhV0EjWfb/a4/OZ6tuVwMCVPiwkU5nsEipxEf7hmkSU7Em5VQ8P5NGA== /@types/normalize-package-data/2.4.0: dev: true resolution: @@ -6937,6 +6978,12 @@ packages: path-parse: 1.0.6 resolution: integrity: sha512-fn5Wobh4cxbLzuHaE+nphztHy43/b++4M6SsGFC2gB8uYwf0C8LcarfCz1un7UTW8OFQg9iNjZ4xpcFVGebDPg== + /resolve/1.14.2: + dependencies: + path-parse: 1.0.6 + dev: true + resolution: + integrity: sha512-EjlOBLBO1kxsUxsKjLt7TAECyKW6fOh1VRkykQkKGzcBbjjPIxBqGh0jf7GJ3k/f5mxMqW3htMD3WdTUVtW8HQ== /responselike/1.0.2: dependencies: lowercase-keys: 1.0.1