Skip to content

Commit

Permalink
feat: Full import map support
Browse files Browse the repository at this point in the history
BREAKING CHANGE: The plugin now require a base URL as a string or URL object as the first argument.
  • Loading branch information
Trygve Lie committed Oct 30, 2022
1 parent 2fb9301 commit 6b79d64
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 80 deletions.
69 changes: 37 additions & 32 deletions lib/plugin.js
Original file line number Diff line number Diff line change
@@ -1,74 +1,79 @@
import path from 'path';
import fs from 'fs';

const isBare = (str) => {
if (str.startsWith('/') || str.startsWith('./') || str.startsWith('../') || str.substr(0, 7) === 'http://' || str.substr(0, 8) === 'https://') {
return false;
}
return true;
};
import { parse, resolve } from '@import-maps/resolve';
import { URL } from 'node:url';
import merge from 'deepmerge';
import path from 'node:path';
import fs from 'node:fs/promises';

const isString = (str) => typeof str === 'string';

const validate = (map, options) => Object.keys(map.imports).map((key) => {
const value = map.imports[key];
const validateBaseArgument = (base) => {
if (isString(base)) {
return false;
}

if (isBare(value)) {
throw Error(`Import specifier can NOT be mapped to a bare import statement. Import specifier "${key}" is being wrongly mapped to "${value}"`);
if (base instanceof URL) {
return false;
}

return true;
};

const checkMapAgainstExternals = (map, options) => Object.keys(map.imports).forEach((key) => {
if (typeof options.external === 'function') {
if (options.external(key)) throw Error('Import specifier must NOT be present in the Rollup external config. Please remove specifier from the Rollup external config.');
}

if (Array.isArray(options.external)) {
if (options.external.includes(key)) throw Error('Import specifier must NOT be present in the Rollup external config. Please remove specifier from the Rollup external config.');
}

return { key, value };
});

const fileReader = (pathname = '', options = {}) => new Promise((resolve, reject) => {
const fileReader = (pathname = '') => new Promise((success, reject) => {
const filepath = path.normalize(pathname);
fs.promises.readFile(filepath).then((file) => {
fs.readFile(filepath).then((file) => {
try {
const obj = JSON.parse(file);
resolve(validate(obj, options));
success(obj);
} catch (error) {
reject(error);
}
}).catch(reject);
});

export function rollupImportMapPlugin(importMaps = []) {
const cache = new Map();
export function rollupImportMapPlugin(baseURL, importMaps = []) {
if (validateBaseArgument(baseURL)) {
throw new TypeError('First argument must be a URL object or a valid absolute URL as a string');
}

const base = isString(baseURL) ? new URL(baseURL) : baseURL;
const maps = Array.isArray(importMaps) ? importMaps : [importMaps];
let cache = {};

return {
name: 'rollup-plugin-import-map',

async buildStart(options) {
const mappings = maps.map((item) => {
if (isString(item)) {
return fileReader(item, options);
return fileReader(item);
}
return validate(item, options);
});

await Promise.all(mappings).then((items) => {
items.forEach((item) => {
item.forEach((obj) => {
cache.set(obj.key, obj.value);
});
});
return item;
});

const loadedMaps = await Promise.all(mappings);

const mapObject = merge.all(loadedMaps);
checkMapAgainstExternals(mapObject, options);

cache = parse(mapObject, base);
},

resolveId(importee) {
const url = cache.get(importee);
if (url) {
const { resolvedImport, matched } = resolve(importee, cache, base);
if (matched) {
return {
id: url,
id: resolvedImport.href,
external: true,
};
}
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,9 @@
"rollup": "3.2.3",
"semantic-release": "19.0.5",
"tap": "16.03.0"
},
"dependencies": {
"@import-maps/resolve": "1.0.1",
"deepmerge": "4.2.2"
}
}
23 changes: 4 additions & 19 deletions tap-snapshots/test/plugin.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,9 @@
*/
'use strict'
exports[`test/plugin.js TAP plugin() - array of import map maps - should replace import statements with CDN URLs > non bare imports 1`] = `
import { firstElement } from 'https://cdn.eik.dev/something/v666';
import { replaceElement, firstElement } from 'https://cdn.eik.dev/something/v666';
import { html } from 'https://cdn.eik.dev/lit-element/v2';
function replaceElement(target, element) {
target.replaceWith(element);
return element;
}
function view(items) {
return html\`<p>Hello \${items[0]}!</p>\`;
}
Expand Down Expand Up @@ -80,7 +75,7 @@ render();
`

exports[`test/plugin.js TAP plugin() - import map maps address to a relative path - should replace import statement with relative path > non bare imports 1`] = `
import { html } from './lit-element/v2';
import { html } from 'http://localhost/lit-element/v2';
function replaceElement(target, element) {
target.replaceWith(element);
Expand Down Expand Up @@ -147,14 +142,9 @@ start();
`

exports[`test/plugin.js TAP plugin() - import map maps non bare imports - should replace import statement with CDN URL > non bare imports 1`] = `
import { firstElement } from 'https://cdn.eik.dev/something/v666';
import { replaceElement, firstElement } from 'https://cdn.eik.dev/something/v666';
import { html } from 'https://cdn.eik.dev/lit-element/v2';
function replaceElement(target, element) {
target.replaceWith(element);
return element;
}
function view(items) {
return html\`<p>Hello \${items[0]}!</p>\`;
}
Expand Down Expand Up @@ -297,14 +287,9 @@ start();
`

exports[`test/plugin.js TAP plugin() - input is a filepath to a map file and an inline map - should load map and replace import statements with CDN URLs > non bare imports 1`] = `
import { firstElement } from 'https://cdn.eik.dev/something/v666';
import { replaceElement, firstElement } from 'https://cdn.eik.dev/something/v666';
import { html } from 'https://cdn.eik.dev/lit-element/v2';
function replaceElement(target, element) {
target.replaceWith(element);
return element;
}
function view(items) {
return html\`<p>Hello \${items[0]}!</p>\`;
}
Expand Down
66 changes: 37 additions & 29 deletions test/plugin.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { rollupImportMapPlugin } from '../lib/plugin.js';
import { rollup } from 'rollup';
import { URL } from 'node:url';
import tap from 'tap';

const simple = new URL('../fixtures/modules/simple/main.js', import.meta.url).pathname;
Expand All @@ -18,7 +19,7 @@ tap.test('plugin() - target is refered to in external - should reject process',
const options = {
input: simple,
external: ['foo'],
plugins: [rollupImportMapPlugin({
plugins: [rollupImportMapPlugin('http://localhost/', {
imports: {
'foo': 'http://not.a.host.com'
}
Expand All @@ -28,14 +29,13 @@ tap.test('plugin() - target is refered to in external - should reject process',
t.end();
});


tap.test('plugin() - basic module - should replace lit-element with CDN URL', async (t) => {
const options = {
input: basic,
onwarn: (warning, warn) => {
// Supress logging
},
plugins: [rollupImportMapPlugin({
plugins: [rollupImportMapPlugin('http://localhost/', {
imports: {
'lit-element': 'https://cdn.eik.dev/lit-element/v2'
}
Expand All @@ -49,13 +49,14 @@ tap.test('plugin() - basic module - should replace lit-element with CDN URL', as
t.end();
});


tap.test('plugin() - simple module - should replace lit-element with CDN URL', async (t) => {
const options = {
input: simple,
onwarn: (warning, warn) => {
// Supress logging
},
plugins: [rollupImportMapPlugin({
plugins: [rollupImportMapPlugin('http://localhost/', {
imports: {
'lit-element': 'https://cdn.eik.dev/lit-element/v2'
}
Expand All @@ -75,7 +76,7 @@ tap.test('plugin() - import map maps non bare imports - should replace import st
onwarn: (warning, warn) => {
// Supress logging
},
plugins: [rollupImportMapPlugin({
plugins: [rollupImportMapPlugin('http://localhost/', {
imports: {
'lit-element': 'https://cdn.eik.dev/lit-element/v2',
'./utils/dom.js': 'https://cdn.eik.dev/something/v666'
Expand All @@ -96,7 +97,7 @@ tap.test('plugin() - import map maps address to a relative path - should replace
onwarn: (warning, warn) => {
// Supress logging
},
plugins: [rollupImportMapPlugin({
plugins: [rollupImportMapPlugin('http://localhost/', {
imports: {
'lit-element': './lit-element/v2',
}
Expand All @@ -116,7 +117,7 @@ tap.test('plugin() - import specifier is a interior package path - should replac
onwarn: (warning, warn) => {
// Supress logging
},
plugins: [rollupImportMapPlugin({
plugins: [rollupImportMapPlugin('http://localhost/', {
imports: {
'lit-element': 'https://cdn.eik.dev/lit-element/v2',
'lit-html/lit-html': 'https://cdn.eik.dev/lit-html/v2',
Expand All @@ -132,30 +133,13 @@ tap.test('plugin() - import specifier is a interior package path - should replac
t.end();
});

tap.test('plugin() - import map maps address to a bare importer - should throw', async (t) => {
const options = {
input: simple,
onwarn: (warning, warn) => {
// Supress logging
},
plugins: [rollupImportMapPlugin({
imports: {
'lit-element': 'lit-element/v2',
}
})],
}

t.rejects(rollup(options), new Error('Import specifier can NOT be mapped to a bare import statement. Import specifier "lit-element" is being wrongly mapped to "lit-element/v2"'));
t.end();
});

tap.test('plugin() - array of import map maps - should replace import statements with CDN URLs', async (t) => {
const options = {
input: simple,
onwarn: (warning, warn) => {
// Supress logging
},
plugins: [rollupImportMapPlugin([{
plugins: [rollupImportMapPlugin('http://localhost/', [{
imports: {
'lit-element': 'https://cdn.eik.dev/lit-element/v2'
}
Expand All @@ -180,7 +164,7 @@ tap.test('plugin() - input is a filepath to a map file - should load map and rep
onwarn: (warning, warn) => {
// Supress logging
},
plugins: [rollupImportMapPlugin(map)],
plugins: [rollupImportMapPlugin('http://localhost/', map)],
}

const bundle = await rollup(options);
Expand All @@ -196,7 +180,7 @@ tap.test('plugin() - input is a filepath to a map file and an inline map - shoul
onwarn: (warning, warn) => {
// Supress logging
},
plugins: [rollupImportMapPlugin([
plugins: [rollupImportMapPlugin('http://localhost/', [
map,
{
imports: {
Expand All @@ -218,7 +202,7 @@ tap.test('plugin() - input is a filepath to a non existing map file - should thr
onwarn: (warning, warn) => {
// Supress logging
},
plugins: [rollupImportMapPlugin('./foo.map.json')],
plugins: [rollupImportMapPlugin('http://localhost/', './foo.map.json')],
}

t.rejects(rollup(options), /ENOENT: no such file or directory, open 'foo.map.json'/);
Expand All @@ -231,9 +215,33 @@ tap.test('plugin() - input is a filepath to a faulty map file - should throw', a
onwarn: (warning, warn) => {
// Supress logging
},
plugins: [rollupImportMapPlugin(err)],
plugins: [rollupImportMapPlugin('http://localhost/', err)],
}

t.rejects(rollup(options), /Unexpected end of JSON input/);
t.end();
});

tap.test('plugin() - first argument is not a string or a URL object', async (t) => {
t.throws(() => {
rollupImportMapPlugin(777);
}, new TypeError('First argument must be a URL object or a valid absolute URL as a string'), 'should throw a TypeError');

t.end();
});

tap.test('plugin() - first argument is a legal URL as a String', async (t) => {
t.doesNotThrow(() => {
rollupImportMapPlugin('http://localhost');
}, 'should not throw');

t.end();
});

tap.test('plugin() - first argument is a URL object', async (t) => {
t.doesNotThrow(() => {
rollupImportMapPlugin(new URL('http://localhost'), []);
}, 'should not throw');

t.end();
});

0 comments on commit 6b79d64

Please sign in to comment.