create-esm-loader
Advanced tools
Comparing version
// # create-loader.js | ||
import semver from 'semver'; | ||
export default function createLoader(...args) { | ||
@@ -49,39 +50,97 @@ return new Loader(...args).hooks(); | ||
// This function returns an object containing all Node.js loader hooks as | ||
// properties so that the loader entry file can re-export them. It's here | ||
// that we can do some checks of the Node version in the future if we want. | ||
// properties so that the loader entry file can re-export them. See #1. | ||
// Given that the api changed in v16.12.0, we'll inspect the current | ||
// process version and adapt accordingly. | ||
hooks() { | ||
// For backwards compatibility purposes, we will manually compose | ||
// `format()`, `fetch()` and `transform()` into a `load()` function. | ||
const hook = id => (...args) => this.handleStack(id, ...args); | ||
return { | ||
resolve: hook('resolve'), | ||
getFormat: hook('format'), | ||
getSource: hook('fetch'), | ||
const resolve = hook('resolve'); | ||
const getFormat = hook('format'); | ||
const getSource = hook('fetch'); | ||
// Handling the transform is fundamentally different as we need to | ||
// chain results here! | ||
transformSource: async (source, ctx, node) => { | ||
let stack = await this.stack; | ||
let fns = stack.transform || []; | ||
let baseOptions = { ...this.options }; | ||
let mem = source; | ||
let flag = true; | ||
for (let { fn, options } of fns) { | ||
let finalOptions = { | ||
...baseOptions, | ||
...options, | ||
...ctx, | ||
}; | ||
let result = await fn(mem, finalOptions); | ||
if (result) { | ||
flag = false; | ||
// Handling transformation is fundamentally different as we have to | ||
// chain results here. | ||
const transformSource = async (source, ctx, node) => { | ||
let stack = await this.stack; | ||
let fns = stack.transform || []; | ||
let baseOptions = { ...this.options }; | ||
let mem = source; | ||
let flag = true; | ||
for (let { fn, options } of fns) { | ||
let finalOptions = { | ||
...baseOptions, | ||
...options, | ||
...ctx, | ||
}; | ||
let result = await fn(mem, finalOptions); | ||
if (result || typeof result === 'string') { | ||
flag = false; | ||
if (typeof result === 'string') { | ||
mem = result; | ||
} else { | ||
mem = result.source; | ||
} | ||
} | ||
if (flag) { | ||
return node(source, ctx, node); | ||
} else { | ||
return { source: mem }; | ||
} | ||
if (flag) { | ||
return node(source, ctx, node); | ||
} else { | ||
return { source: mem }; | ||
} | ||
}; | ||
// Now compose the correct hooks based on the Node version we're | ||
// running. | ||
if (semver.satisfies(process.version, '<16.12.0')) { | ||
return { | ||
resolve, | ||
getFormat, | ||
getSource, | ||
transformSource, | ||
}; | ||
} | ||
// If we reach this point, it means we're running on Node v16.12.0 or | ||
// higher, which uses the new approach. We only have to export a | ||
// `resolve` and `load` function here, but the difficulty is that the | ||
// `load()` function has to be composed manually! | ||
const load = async function(url, ctx, defaultLoad) { | ||
// If the format was already specified by the resolve hook, we | ||
// won't try to fetch it again. Note that this functionality is | ||
// specific to v16.12. | ||
const grab = (obj = {}) => obj.format; | ||
let { | ||
format = await getFormat(url, ctx, noop).then(grab), | ||
} = ctx; | ||
// Mock the default `getSource` function. What's important here is | ||
// that if we the default getSource is used, we'll also set it as | ||
// default format! | ||
const defaultGetSource = async (url, ctx) => { | ||
let result = await defaultLoad(url, { format }); | ||
if (!format) { | ||
format = result.format; | ||
} | ||
}, | ||
return result; | ||
}; | ||
let { source } = await getSource(url, ctx, defaultGetSource); | ||
// At last transform. | ||
const defaultTransform = source => ({ source }); | ||
let transform = await transformSource( | ||
source, | ||
{ url, format }, | ||
defaultTransform, | ||
); | ||
return { | ||
format, | ||
source: transform.source, | ||
}; | ||
}; | ||
return { resolve, load }; | ||
} | ||
@@ -121,3 +180,6 @@ | ||
let module = await import(def.loader); | ||
let normalized = normalize(module.default); | ||
let normalized = normalize({ | ||
hooks: module.default, | ||
options: def.options, | ||
}); | ||
this.fill(normalized, dummy); | ||
@@ -214,1 +276,4 @@ })()); | ||
} | ||
// # noop() | ||
function noop() {} |
{ | ||
"name": "create-esm-loader", | ||
"version": "0.0.1-alpha.0", | ||
"version": "0.0.1", | ||
"description": "A utility library for creating Node loader hooks", | ||
@@ -32,6 +32,10 @@ "type": "module", | ||
"devDependencies": { | ||
"chai": "^4.2.0", | ||
"chai": "^4.3.4", | ||
"chai-spies": "^1.0.0", | ||
"mocha": "^8.2.1" | ||
"mocha": "^9.1.3", | ||
"typescript": "^4.4.4" | ||
}, | ||
"dependencies": { | ||
"semver": "^7.3.5" | ||
} | ||
} |
@@ -9,3 +9,3 @@ # create-esm-loader | ||
Node 14 provides full support for native ES Modules without the need for transpilation. | ||
While CommonJS is likely not to go anywhere soon, it is good practice to at least start thinking about migrating your codebase from CommonJS to ESM. | ||
While CommonJS is likely not to go anywhere soon, it is good practice to [at least start thinking about migrating your codebase from CommonJS to ESM](https://blog.sindresorhus.com/get-ready-for-esm-aa53530b3f77). | ||
In the `require`-world, we had [require.extensions](https://nodejs.org/api/modules.html#modules_require_extensions) if we wanted to load non-JS files into Node. | ||
@@ -29,17 +29,9 @@ You could use this, for example, to load TypeScript files and compile them just-in-time. | ||
ESM loaders must be written in ESM format. | ||
This means that Node needs to interpret it as an ES Module as well, which means you either need to use the `.mjs` extension, or make sure that the nearest `package.json` contains a `{ "type": "module" }` field. | ||
For more info, see https://nodejs.org/api/esm.html#esm_enabling. | ||
`create-esm-loader` is inspired by Webpack. | ||
You can start to create loaders like this: | ||
You can pass it a configuration object and it will return a set of [loader hooks](https://nodejs.org/api/esm.html#hooks) which you then have to export manually. | ||
This typically looks like | ||
```js | ||
// loader.mjs | ||
// loader.js | ||
import createLoader from 'create-esm-loader'; | ||
const loader = createLoader(config); | ||
// In order for a loader to work on Node, you must export the appropriate hooks: | ||
const { resolve, getFormat, getSource, transformSource } = loader; | ||
export { resolve, getFormat, getSource, transformSource }; | ||
export const { resolve, load } = createLoader(config); | ||
``` | ||
@@ -52,2 +44,22 @@ | ||
Note that in Node 16.12, the loader hooks [have changed](https://nodejs.org/docs/v16.12.0/api/esm.html#esm_loaders). | ||
In previous versions, **including `16.11`**, you had to export `resolve()`, `getFormat()`, `getSource()` and `transformSource()`. | ||
In Node `>=16.12.0`, you have to export `resolve()` and `load()` instead. | ||
`create-esm-loader` is backwards compatible and is able to handle both. | ||
This means that if you're writing a loader that needs to support `<16.12`, you have to export | ||
```js | ||
export const { | ||
resolve, | ||
getFormat, | ||
getSource, | ||
transformSource, | ||
load, | ||
} = createLoader(config); | ||
``` | ||
ESM loaders must be written in ESM format. | ||
This means that Node needs to interpret it as an ES Module as well, which means you either need to use the `.mjs` extension, or make sure that the nearest `package.json` contains a `{ "type": "module" }` field. | ||
For more info, see https://nodejs.org/api/esm.html#esm_enabling. | ||
### Basic configuration | ||
@@ -72,5 +84,4 @@ | ||
``` | ||
Those methods respectively correspond to the `resolve()`, `getFormat()`, `getSource()` and `transform()` [loader hooks](https://nodejs.org/api/esm.html#esm_hooks) from Node. | ||
The reason why the names don't match is to make abstraction of the underlying Node mechanism, which might still change in the future. | ||
The hope is that if this happens, only this module will need to be updated and not the way you've written your loader configurations. | ||
Those methods used to correspond respectively to the `resolve()`, `getFormat()`, `getSource()` and `transform()` [loader hooks](https://nodejs.org/docs/latest-v14.x/api/esm.html#esm_loaders) from Node, but as mentioned above the `getFormat()`, `getSource()` and `transform()` hooks have now been merged into a single `load()` hook. | ||
The api of this module has not changed as it's explicit goal is to hide how Node handles loaders internally. | ||
@@ -80,2 +91,20 @@ Every hook is optional and can be an async function, which is useful if you need to do some async logic within it. | ||
### Node `^16.12` | ||
If you only target node 16.12 and above, you can simplify your life a bit by specifying the format in the `resolve()` hook, omitting the need for a separate `format()` hook. | ||
```js | ||
// Will not work in Node < 16.12!! | ||
const loader = createLoader({ | ||
resolve(specifier, opts) { | ||
let url = new URL(specifier, opts.parentURL); | ||
if (url.pathname.endsWith('.vue')) { | ||
return { | ||
format: 'module', | ||
url: url.href, | ||
}; | ||
} | ||
}, | ||
}); | ||
``` | ||
### Advanced configurations | ||
@@ -168,4 +197,2 @@ | ||
```js | ||
import { URL } from 'url'; | ||
const tsLoader = { | ||
@@ -186,8 +213,11 @@ resolve(specifier, opts) { | ||
return { | ||
source: ts.compile(String(source)), | ||
source: ts.transpileModule(String(source), { | ||
compilerOptions: { | ||
module: ts.ModuleKind.ES2020, | ||
}, | ||
}), | ||
}; | ||
}, | ||
}; | ||
const { resolve, getFormat, getSource, transformSource } = createLoader(tsLoader); | ||
export { resolve, getFormat, getSource, transformSource }; | ||
export const { resolve, load } = createLoader(tsLoader); | ||
@@ -215,4 +245,3 @@ // Usage: | ||
}; | ||
const { resolve, getFormat, getSource, transformSource } = createLoader(directoryLoader); | ||
export { resolve, getFormat, getSource, transformSource }; | ||
export const { resolve, load } = createLoader(directoryLoader); | ||
@@ -219,0 +248,0 @@ // Usage: |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
17687
21.23%241
30.98%243
13.55%1
Infinity%4
33.33%+ Added
+ Added