script-linker
CJS/MJS source loader that can preresolve imports/requires so linking them on runtime runs much faster.
Features include:
- Simple transforms (ie all contained on the same line)
- Source Maps for all transforms so debugging code is easy
- IO agnostic, bring your own IO
- Similar CJS/ESM interop like Node.js
- Cross platform
- Very fast. More than 100x faster than detective used in browserify.
Usage
const ScriptLinker = require('@holepunchto/script-linker')
const Localdrive = require('localdrive')
const drive = new Localdrive('./root-folder')
const s = new ScriptLinker(drive)
const mod = await s.load('/some/module.js')
console.log(mod.toESM())
In the process executing the module, you need to include the ScriptLinker runtime dep.
Currently that's done by running
const r = ScriptLinker.runtime({
getSync (url) {
},
resolveSync (req, dirname, { isImport }) {
}
})
Links
Per default the map function in ScriptLinker produces URLs per the following spec
app://[raw|esm|cjs|map|app]/(filename)|(dirname~module)
The links can be parsed (and generated) with the the links submodule
const { links } = ScriptLinker
const l = links.parse('app://esm/~module')
Runtime
For ScriptLinker to resolve dynamic imports or commonjs modules it needs a small runtime defined, with some helper functions.
In your execution context (ie the frontend), load this using the runtime submodule
const { runtime } = ScriptLinker
const r = runtime({
map,
mapImport,
builtins,
getSync (url) {
},
resolveSync (req, dirname, { isImport }) {
}
})
API
s = new ScriptLinker(drive, options)
Make a new ScriptLinker instance. Accepts a Localdrive or Hyperdrive.
Options include:
{
sourceOverwrites: null,
imports,
builtins: {
has (name) { },
async get (name) { },
keys () { }
},
bare: false,
linkSourceMaps: true,
symbol: 'scriptlinker',
protocol: 'app',
defaultType: 'commonjs',
map (id, { protocol, isImport, isConsole, isSourceMap, isBuiltin }) {
return
},
mapImport (id, dirname) { }
}
filename = await s.resolve(request, dirname, [options])
Resolve a request (ie ./foo.js or module) into an absolute filename from the context of a directory.
Options include:
{
isImport: true,
transform: 'esm'
}
module = await s.load(filename)
Load a module. filename should be an absolute path.
string = module.source
The raw source of the module
string = module.toESM()
Transform this module to be ESM.
string = module.toCJS()
Transform this module to be CJS.
string = module.generateSourceMap()
Generate a source map for this module.
string = module.filename
The filename (and id) for this module.
cache = module.cache()
The data to cache if you want to make reloading the script linker faster.
module.resolutions
An array of the imports/requires this module has, and what they resolve to.
Note that the requires might be wrong (very likely not!), but is merely there as a caching optimisation.
The main work of ScriptLinker is to produce this array. When produced, you can cache it and pass it using the stat
function so transforms run faster on reboots.
module.type
Is this an esm module or commonjs?
Similarly to resolutions, you can cache this and pass it using stat.
string = await s.transform(options)
Helper for easily transforming a module based on a set of options.
Options include:
{
filename: '/path/to/file.js',
resolve: './module',
dirname: '/',
transform: 'esm' || 'cjs' || 'map',
}
Optionally instead of the transform you can pass the following flags instead for convenience
{
isSourceMap: true
isImport: true
}
Note that the options to transform match what is returned from the url parser meaning the following works
const l = ScriptLinker.links.parse(defaultUrl)
const source = await s.transform(l)
string = await s.bundle(filename, { builtins: 'builtinsObjectName' })
A simple static bundler that compiles the module specified by filename and it's dependencies into a single script without dependencies.
Builtins should be the string name of the global variable containing the builtins provided.
for await (const { isImport, module } of s.dependencies(filename))
Walk the dependencies of a module. Each pair of isImport, module is only yielded once.
const modMap = await s.warmup(entryPoints)
Warmup a single or multiple entrypoints. Doing this will help the CJS export parser find more exports.
Returns a Map of modules that were visited.
You can iterate this map and send to the runtime the filename and cjs of commonjs modules
const cjs = []
for (const [filename, mod] of modMap) {
if (mod.type !== 'commonjs') continue
cjs.push({ filename, source: mod.toCJS() })
}
In the runtime, use this info to populate runtime.sources with the cjs source.
const runtime = ScriptLinker.runtime(...)
for (const { filename, source } of batch) {
runtime.sources.set(filename, source)
}
This will result in close to no runtime requests for cjs when running your code.
License
Apache-2.0