Skip to content

Commit

Permalink
Merge pull request #24 from shgysk8zer0/feature/import-meta
Browse files Browse the repository at this point in the history
Add support for `import.meta.url` & `import.meta.resolve()`
  • Loading branch information
shgysk8zer0 committed Jun 10, 2023
2 parents 6c67b1e + 42c4640 commit 490a7ad
Show file tree
Hide file tree
Showing 13 changed files with 626 additions and 171 deletions.
4 changes: 3 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,7 @@
"no-async-promise-executor": 0,
"no-prototype-builtins": 0
},
"globals": {}
"globals": {
"globalThis": "readonly"
}
}
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,5 @@ dist
*.out.js
*.min.js
*.cjs
*.map.*
*.map
*bak
5 changes: 4 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
node_modules/
test/
.env
.github/
.editorconfig
.eslintignore
Expand All @@ -14,4 +15,6 @@ rollup.config.js
test.config.js
*.tgz
*.log
*.out.js
*.bak
*.min.js
*.map
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [v1.1.0] - 2023-06-10

### Added
- Add handling of `import.meta.url` and `import.meta.resolve()`
- Added `magic-string` and `dotenv` as dependencies
- Add `@shgysk8zer0/js-utils` as dev dependency

### Removed
- Uninstall `rollup` and `eslint`

### Changed
- Update README with `import.meta.url` and `import.meta.resolve()` notes & instructions

## [v1.0.3] - 2023-06-07

### Fixed
Expand Down
75 changes: 45 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,23 +36,51 @@ npm i @shgysk8zer0/rollup-import
```

## Supports
- External [impormap](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap)s
- External [impormap](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap)
- JSON
- YAML
- Object `{ imports, scope }` for importmap
- Map `new Map([[specifier, value]])` for importmap
- Importing modules from URL and paths and "bare specifiers"
- Resolving `import.meta.url` and `import.meta.resolve('path.ext')`

## Not yet supported
- `import html from 'template.html' with { type: 'html' }` - No spec yet and will have issues with TrustedTypes
- `import style from 'styles.css' with { type: 'style' }` - Would require `new CSSStyleSheet().replace()` or `style-src 'unsafe-inline'`
- `import json from 'data.json' with { type: 'json' }`
- Parsing from `<script type="importmap">` in an HTML file
- Use of `scope`

## Example

### `rollup.config.js`

```js
import { rollupImport } from '@shgysk8zer0/rollup-import';
import {
rollupImport, // Handles `import '@scope/package' resolving and fetching`
rollupImportMeta // Handles `import.meta.url` and `import.meta.resolve()`,
} from '@shgysk8zer0/rollup-import';

import terser from '@rollup/plugin-terser';

// To load environment variables from `.env`
import { config } from 'dotenv';
config();

export default {
input: 'src/index.js',
plugins: [rollupImport(['importmap.json'])],
input: 'src/index.mjs',
plugins: [
rollupImport(['path/to/importmap.json']),
rollupImportMeta({
// MUST be a valid URL
baseURL: 'https://example.com', // Defaults to `process.env.URL` if set
// MUST be a `file:` URL
projectRoot: 'file:///home/user/Projects/my-project/', // Dfaults to `file:///${process.cwd()}/`
}),
terser(),
],
output: {
file: 'dest/index.out.js',
file: 'dest/index.js',
format: 'iife'
}
};
Expand All @@ -65,7 +93,7 @@ export default {
"imports": {
"leaflet": "https://unpkg.com/leaflet@1.9.3/dist/leaflet-src.esm.js",
"firebase/": "https://www.gstatic.com/firebasejs/9.16.0/",
"@scope/package": "./node_modules/@scope/package/index.js"
"@scope/package": "./node_modules/@scope/package/index.js",
"@shgysk8zer0/polyfills": "https://unpkg.com/@shgysk8zer0/polyfills@0.0.5/all.min.js",
"@shgysk8zer0/polyfills/": "https://unpkg.com/@shgysk8zer0@0.0.5/polyfills/"
}
Expand All @@ -78,36 +106,23 @@ export default {
import '@scope/package';
import '@shgysk8zer0/polyfills';
import '@shgysk8zer0/polyfills/legacy/object.js'; // -> "https://unpkg.com/@shgysk8zer0@0.0.5/polyfills/legacy/ojbect.js"
import { initializeApp } from 'firebase/firebase-app.js';
import { name } from './consts.js';

import {
map as LeafletMap,
marker as LeafletMarker,
icon as LeafletIcon,
tileLayer as LeafletTileLayer,
point as Point,
latLng as LatLng,
version,
} from 'leaflet';
const stylesheet = document.createElement('link');
stylesheet.rel = 'stylesheet';
stylesheet.href = import.meta.resolve('styles.css');

import { initializeApp } from 'firebase/firebase-app.js';

import {
getFirestore, collection, getDocs, getDoc, doc, addDoc, setDoc,
enableIndexedDbPersistence,
} from 'firebase/firebase-firestore.js';
document.head.append(stylesheet);
```

import {
getAuth, createUserWithEmailAndPassword, signInWithEmailAndPassword, signOut,
onAuthStateChanged, updateProfile, sendPasswordResetEmail,
} from 'firebase/firebase-auth.js';
## Notes

import { getStorage, ref, getDownloadURL, uploadBytes } from 'firebase/firebase-storage.js';
```
Using `import`s only, you may use only `rollupImport` or `rollupImportMeta`
via `@shgysk8zer0/rollup-import/import` and `@shgysk8zer0/rollup-import/meta`
respectively. To use with `require()`, you MUST import either/both using
`const {rollupImport, rollupImportMeta } = require('@shgysk8zer0/rollup-import')`.

## Note
This plugin works well if importing modules without bundling in the dev environment.
In order to do this, however, you **must** include a `<script type="importmap">`
in your HTML - `<script type="importmap" src="...">` will not work.

Eventually, this may also replace `import.meta.url` with the current URL if possible.
90 changes: 90 additions & 0 deletions import.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/* eslint-env node */
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 isJS = type => JS_MIMES.includes(type.toLowerCase());

const cached = new Map();

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 importmap = new Map();
const maps = Array.isArray(importMaps) ? importMaps : [importMaps];

return {
name: '@shgysk8zer0/rollup-import',
async load(path) {
if (cached.has(path)) {
return cached.get(path);
} 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 => {
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}."`);
}
}
},
async buildStart(options) {
if (typeof options !== 'undefined') {
const mappings = maps.map(entry => isString(entry)
? getFile(entry, options) // Load from file
: entry // Use the Object
);

await buildImportmap(importmap, mappings);
const err = getInvalidMapError(importmap);

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

return match instanceof URL ? { id: match.href, external: false } : null;
} else {
return { id: pathToURL(id, src).href, external: false };
}
},
};
}
93 changes: 2 additions & 91 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,91 +1,2 @@
/* eslint-env node */
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 isJS = type => JS_MIMES.includes(type.toLowerCase());

const cached = new Map();

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 importmap = new Map();
const maps = Array.isArray(importMaps) ? importMaps : [importMaps];

return {
name: '@shgysk8zer0/rollup-import',
async load(path) {
if (cached.has(path)) {
return cached.get(path);
} 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
: entry // Use the Object
);

await buildImportmap(importmap, mappings);
const err = getInvalidMapError(importmap);

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

return match instanceof URL ? { id: match.href, external: false } : null;
} else {
return { id: pathToURL(id, src).href, external: false };
}
},
};
}
export { rollupImport } from './import.js';
export { rollupImportMeta } from './meta.js';
48 changes: 48 additions & 0 deletions meta.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import MagicString from 'magic-string';
import { validateURL } from '@shgysk8zer0/npm-utils/url';

function transformSource(source, id, mapping) {
const ms = new MagicString(source);

Object.entries(mapping).forEach(([search, replace]) => ms.replaceAll(search, replace));

if (ms.hasChanged()) {
const code = ms.toString();
const map = ms.generateMap({ source: id });

return { code, map };
}
}

export function rollupImportMeta({
baseURL = process?.env?.URL,
projectRoot = new URL(`${process.cwd()}/`, 'file://').href,
} = {}) {
if (! validateURL(baseURL)) {
throw new TypeError(`baseURL: "${baseURL}"" is not a valid URL.`);
} else if (! validateURL(projectRoot)) {
throw new TypeError(`projectRoot: "${projectRoot} is not a valid URL."`);
} else {
return {
transform(source, id) {
if (! source.includes('import.meta')) {
return;
} else if (id.startsWith('https:')) {
return transformSource(source, id, {
'${import.meta.url}': id,
'import.meta.url': `'${id}'`,
'import.meta.resolve(': `(path => new URL(path, '${id}').href)(`,
});
} else if (id.startsWith('file:')) {
const url = new URL(id.replace(projectRoot, baseURL)).href;

return transformSource(source, id, {
'${import.meta.url}': url,
'import.meta.url': `'${url}'`,
'import.meta.resolve(': `(path => new URL(path, '${url}').href)(`,
});
}
}
};
}
}
Loading

0 comments on commit 490a7ad

Please sign in to comment.