Skip to content

Commit

Permalink
Merge pull request #19 from shgysk8zer0/feature/npm-utils
Browse files Browse the repository at this point in the history
Add and use `@shgysk8zer0/npm-utils`
  • Loading branch information
shgysk8zer0 authored Jun 7, 2023
2 parents 760f793 + 1810f95 commit b49de2e
Show file tree
Hide file tree
Showing 8 changed files with 103 additions and 139 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [v1.0.1] - 2023-06-06

### Added
- Add `@shgysk8zer0/npm-utils`

### Changed
- Use `@shgysk8zer0/npm-utils/importmap` instead of own resolver of specifiers

### Fixed
- Update GitHub Release Action with correct permissions

Expand Down
7 changes: 4 additions & 3 deletions importmap.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
"imports": {
"leaflet": "https://unpkg.com/leaflet@1.9.3/dist/leaflet-src.esm.js",
"firebase/": "https://www.gstatic.com/firebasejs/9.16.0/",
"@shgysk8zer0/kazoo/": "https://unpkg.com/@shgysk8zer0/kazoo@0.0.5/",
"@shgysk8zer0/polyfills": "https://unpkg.com/@shgysk8zer0/polyfills@0.0.5/all.js",
"@shgysk8zer0/polyfills/": "https://unpkg.com/@shgysk8zer0/polyfills@0.0.5/"
"@shgysk8zer0/kazoo/": "https://unpkg.com/@shgysk8zer0/kazoo@0.0.16/",
"@shgysk8zer0/polyfills": "https://unpkg.com/@shgysk8zer0/polyfills@0.0.8/all.js",
"@shgysk8zer0/polyfills/": "https://unpkg.com/@shgysk8zer0/polyfills@0.0.8/",
"@shgysk8zer0/components/": "https://unpkg.com/@shgysk8zer0/components@0.0.9/"
}
}
168 changes: 56 additions & 112 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,146 +1,90 @@
/* eslint-env node */
import path from 'node:path';
import fs from 'node:fs';
import { parse } from 'yaml';
import { fileExists, readFile } from '@shgysk8zer0/npm-utils/fs';
import { readYAMLFile, isYAMLFile } from '@shgysk8zer0/npm-utils/yaml';
import { readJSONFile, isJSONFile } from '@shgysk8zer0/npm-utils/json';
import { buildImportmap, getInvalidMapError, resolveImport } from '@shgysk8zer0/npm-utils/importmap';
import { isString, isBare } from '@shgysk8zer0/npm-utils/utils';
import { JS as JS_MIMES } from '@shgysk8zer0/npm-utils/mimes';
import { pathToURL } from '@shgysk8zer0/npm-utils/url';

const URL_PREFIXES = ['http:', 'https:'];
const PATH_PREFIXES = ['/', './', '../'];
const YAML_EXTS = ['.yaml', '.yml'];
const JSON_EXTS = ['.json'];
const JS_MIMES = ['application/javascript', 'text/javascript'];

const isYAML = path => YAML_EXTS.some(ext => path.toLowerCase().endsWith(ext));
const isJSON = path => JSON_EXTS.some(ext => path.toLowerCase().endsWith(ext));
const isJS = type => JS_MIMES.includes(type.toLowerCase());
const isString = str => typeof str === 'string';
const isURL = str => isString(str) && URL_PREFIXES.some(scheme => str.startsWith(scheme));
const isPath = str => isString(str) && PATH_PREFIXES.some(scheme => str.startsWith(scheme));
const isBare = str => ! isPath(str) && ! isURL(str);
const externalError = specifier => new TypeError(`Import specifier "${specifier}" is present in the Rollup external config, which is not allowed. Please remove it.`);

const cached = new Map();

function buildCache({ imports }, { external } = {}) {
return Object.entries(imports).map(([key, value]) => {
if (isBare(value)) {
throw TypeError(`Resolution of specifier “${key}” was blocked by a null entry. ${value} is a bare specifier and is not allowed.`);
} else if (external instanceof Function && external(key)) {
throw externalError(key);
} else if (Array.isArray(external) && external.includes(key)) {
throw externalError(key);
} else {
return { key, value };
}
});
}

function getFile(pathname = '', options = {}) {
return new Promise((resolve, reject) => {
const filepath = path.normalize(pathname);

fs.promises.readFile(filepath, { encoding: 'utf8' }).then(file => {
try {
if (isYAML(filepath)) {
const obj = parse(file);
resolve(buildCache(obj, options));
} else if (isJSON(filepath)) {
const obj = JSON.parse(file);
resolve(buildCache(obj, options));
} else {
throw new Error('Unsupported file type.');
}
} catch (error) {
reject(error);
}
}).catch(reject);
});
async function getFile(pathname) {
if (! await fileExists(pathname)) {
throw new Error(`${pathname} not found.`);
} else if (isYAMLFile(pathname)) {
return readYAMLFile(pathname);
} else if (isJSONFile(pathname)) {
return readJSONFile(pathname);
} else {
throw new TypeError(`Unsupported file type for ${pathname}.`);
}
}

export function rollupImport(importMaps = []) {
const cache = new Map();
const importmap = new Map();
const maps = Array.isArray(importMaps) ? importMaps : [importMaps];

function getMatch(id) {
// Return exact matches first
if (cache.has(id)) {
return cache.get(id);
} else {
let isFound = false;

// Find the longest value beginning with id
// @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap#mapping_path_prefixes
const [key, value] = [...cache.entries()]
.filter(([k]) => k.endsWith('/'))
.reduce(([key1, value1], [key2, value2]) => {
const matches = id.startsWith(key2);

if (! isFound && matches) {
isFound = true;
return [key2, value2];
} else if (matches && key2.length > key1.length) {
return [key2, value2];
} else {
return [key1, value1];
}
}, []);

if (typeof key === 'string') {
const match = id.replace(key, value);
// Update cache for future searches
cache.set(id, match);
return match;
} else {
return null;
}
}
}

return {
name: '@shgysk8zer0/rollup-import',
async load(path) {
if (cached.has(path)) {
return cached.get(path);
} else if (isPath(path)) {
const content = await fs.promises.readFile(path, { encoding: 'utf8' });
cached.set(path, content);
return content;
} else if (isURL(path)) {
const resp = await fetch(path);
if (resp.ok && isJS(resp.headers.get('Content-Type').split(';')[0].trim())) {
const content = resp.text();
cached.set(path, content);
return content;
} else {
return null;
} else {
switch(new URL(path).protocol) {
case 'file:':
return readFile(path.replace('file://', '')).then(content => {
cached.set(path, content);
return content;
});

case 'https:':
case 'http:':
return fetch(path).then(async resp => {
// console.log({ url: resp.url, status: resp.status, ok: resp.ok });
if (! resp.ok) {
throw new Error(`<${path}> [${resp.status} ${resp.statusText}]`);
} else if (! isJS(resp.headers.get('Content-Type').split(';')[0].trim())) {
throw new TypeError(`Expected 'application/javascript' but got ${resp.headers.get('Content-Type')}`);
} else {
const content = await resp.text();
cached.set(path, content);
return content;
}
});

default:
throw new TypeError(`Unsupported protocol "${path.protocol}."`);
}
}
},
async buildStart(options) {
if (typeof options !== 'undefined') {
const mappings = maps.map(entry => isString(entry)
? getFile(entry, options) // Load from file
: buildCache(entry, options) // Build from object
: entry // Use the Object
);

await Promise.all(mappings).then(entries => {
entries.forEach(entry => {
entry.forEach(({ key, value }) => cache.set(key, value));
});
});
await buildImportmap(importmap, mappings);
const err = getInvalidMapError(importmap);

if (err instanceof Error) {
throw err;
}
}
},
resolveId(id, src, /*{ assertions, custom, isEntry }*/) {
resolveId(id, src, { /*assertions, custom,*/ isEntry }) {
// @TODO: Store `options.external` and use for return value?
if (isURL(id)) {
return { id, external: false };
} else if (isURL(src) && isPath(id)) {
return { id: new URL(id, src).href, external: false };
if (isEntry) {
return { id: new URL(id, `file://${process.cwd()}/`).href, external: false };
} else if (isBare(id)) {
const match = getMatch(id);
const match = resolveImport(id, importmap, { base: src });

return isString(match) ? { id: match, external: false } : null;
return match instanceof URL ? { id: match.href, external: false } : null;
} else {
return null;
return { id: pathToURL(id, src).href, external: false };
}
},
};
Expand Down
46 changes: 24 additions & 22 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@shgysk8zer0/rollup-import",
"version": "1.0.0",
"version": "1.0.1",
"description": "A RollUp plugin for importing modules from URLs, paths, and bare specifiers using import maps.",
"type": "module",
"engines": {
Expand Down Expand Up @@ -46,6 +46,6 @@
"rollup": "*"
},
"dependencies": {
"yaml": "^2.2.2"
"@shgysk8zer0/npm-utils": "^1.0.4"
}
}
7 changes: 7 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import { rollupImport } from './index.js';

export default {
input: 'test/index.js',
onwarn: warning => {
if (warning.code === 'MISSING_GLOBAL_NAME' || warning.code === 'UNRESOLVED_IMPORT') {
throw new Error(warning.message);
} else if (warning.code !== 'CIRCULAR_DEPENDENCY') {
console.warn(`(!) ${warning.message}`);
}
},
plugins: [rollupImport(['importmap.json'])],
output: {
file: 'test/index.out.js',
Expand Down
1 change: 1 addition & 0 deletions test/components.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '@shgysk8zer0/components/button/share.js';
1 change: 1 addition & 0 deletions test/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import '@shgysk8zer0/polyfills';
import './components.js';
import { html, ready } from '@shgysk8zer0/kazoo/dom.js';

import {
Expand Down

0 comments on commit b49de2e

Please sign in to comment.