aliaset
Version:
twind monorepo
254 lines (217 loc) • 7.52 kB
text/typescript
import type { ImportMap } from './system'
import type { Transpile } from './transpile'
import { rollup } from '@rollup/browser'
import { transform } from 'sucrase'
import { parse } from 'es-module-lexer/js'
import { Generator } from '@jspm/generator'
import { SemverRange } from 'sver'
import { toBase64, toBase64url } from './base64'
import { withVersion } from './versions'
const api: Transpile = {
async transform(input, { manifest, modules = {}, preload = [] }) {
const inputMap: ImportMap = { scopes: manifest.scopes }
// console.debug({ manifest, normalizedImportMap })
const dependencies = new Set<string>(preload)
const staticDependencies = new Set<string>()
const prefix = '~/'
const inputFiles = Object.keys(input).map((name) => prefix + name)
modules = {
...modules,
...Object.fromEntries(Object.entries(input).map(([name, value]) => [prefix + name, value])),
}
const buildId = Date.now().toString(36)
const bundle = await rollup({
input: inputFiles,
plugins: [
{
name: 'virtual',
resolveId(source) {
if (modules.hasOwnProperty(source)) {
return source
}
},
load(id) {
if (modules.hasOwnProperty(id)) {
return modules[id]
}
},
},
{
name: 'sucrase',
transform(code, id) {
const result = transform(code, {
transforms: ['typescript'],
production: false,
jsxRuntime: 'automatic',
filePath: id,
sourceMapOptions: {
compiledFilename: id,
},
})
return {
code: result.code,
map: result.sourceMap,
}
},
},
{
name: 'import-map-generator',
resolveId(source, _importer, options) {
if (options.isEntry) return null
// only support url-like imports
if (/^https?:\/\//.test(source)) {
staticDependencies.add(source)
return false
}
if (/^data:/.test(source)) {
return false
}
// or bare imports
if (/^[^./]/.test(source)) {
const { found, input, output } = withVersion(source, manifest)
if (found) {
// check if versions satifies importMap version and use importMap resolution
const resolved = manifest.imports?.[output.id + output.path]
if (
resolved &&
(!input.version || SemverRange.match(input.version, output.version))
) {
source = output.id + output.path
}
}
for (const [key, value] of Object.entries(manifest.imports || {})) {
// add all sub-path imports
if (
key === source ||
(key.startsWith(source + '/') && key !== source + '/package.json')
) {
;(inputMap.imports ||= {})[key] = value
}
}
dependencies.add(source)
// treat every import as external
return { id: source, external: true }
}
return this.error(
`Invalid import ${JSON.stringify(
source,
)}! Only 'https://', 'http://', 'data:', and bare module imports are allowed.`,
)
},
},
],
})
const generator = new Generator({
baseUrl: 'memory://',
inputMap,
defaultProvider: 'jspm.system',
env: [
'development',
'modern',
'esmodules',
'es0215',
'browser',
'module',
'import',
'default',
],
})
// TODO: trace generator.logStream
const [{ dynamicDeps = [], staticDeps = [] } = {}, { output }] = await Promise.all([
generator.install([...dependencies]),
bundle
.generate({
// cache busting
footer: `\n//# ${buildId}`,
format: 'systemjs',
generatedCode: 'es2015',
// manual inlining to prevent "Unsupported environment: `window.btoa` or `Buffer` should be supported."
sourcemap: 'hidden',
// sourcemap: false,
compact: true,
})
.finally(() => bundle.close()),
])
// scope all external import to the current manifest
// this allows to have distinct dependency tree within one System registry
const hash = toBase64url(
await crypto.subtle.digest(
'sha-256',
await new Blob([JSON.stringify(manifest)]).arrayBuffer(),
),
).slice(0, 10)
const scopeToManifest = (input: string) => {
// we only need to do this for external url
if (input.startsWith('https://ga.system.jspm.io/npm:')) {
const url = new URL(input)
url.searchParams.set('_', hash)
input = url.href
}
return input
}
const scopeImports = (map: Record<string, string>) =>
Object.fromEntries(Object.entries(map).map(([key, url]) => [key, scopeToManifest(url)]))
const map = generator.getMap()
if (map.imports) {
map.imports = scopeImports(map.imports)
}
if (map.scopes) {
map.scopes = Object.fromEntries(
Object.entries(map.scopes).map(([key, scope]) => [key, scopeImports(scope)]),
)
}
const importMap = {
...map,
preload: [...new Set([...staticDependencies, ...staticDeps])].map(scopeToManifest),
prefetch: dynamicDeps.map(scopeToManifest),
}
console.debug('importMap', importMap)
return {
...Object.fromEntries(
output
.filter(
// ignore all sourcemap chunks
(chunk) =>
!(
chunk.type === 'asset' &&
chunk.name === undefined &&
chunk.fileName.endsWith('.map')
),
)
.map((chunk) => {
if (chunk.type === 'chunk' && chunk.isEntry && input.hasOwnProperty(chunk.name)) {
const map = chunk.map
? '\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,' +
toBase64(chunk.map.toString())
: ''
return [chunk.name, `data:text/javascript;base64,${toBase64(chunk.code + map)}`]
}
console.warn(
{ input, chunk },
{
"chunk.type === 'chunk'": chunk.type === 'chunk',
'chunk.isEntry': chunk.type === 'chunk' && chunk.isEntry,
'input.hasOwnProperty(chunk.name)': chunk.name && input.hasOwnProperty(chunk.name),
},
)
throw new Error(`Invalid ${chunk.type} ${chunk.name} generated`)
}),
),
importMap,
} as any
},
async findImports(source) {
const { code } = transform(source, {
transforms: ['typescript'],
production: false,
jsxRuntime: 'automatic',
})
// TODO: same as transform but return only dependencies
const [imports] = await parse(code)
return imports.map(({ s: start, e: end }) => code.slice(start, end)).filter(isBareSpecifier)
},
}
export default api
function isBareSpecifier(id: string): boolean {
return !/^https?:\/\/|^\./.test(id)
}