aliaset
Version:
twind monorepo
595 lines (517 loc) • 21.5 kB
text/typescript
import fs from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import process from 'node:process'
import { createRequire } from 'node:module'
import { execSync } from 'node:child_process'
import fg from 'fast-glob'
import { resolve } from 'resolve.exports'
import { rollup } from '@rollup/browser'
import { Generator } from '@jspm/generator'
import { composeTwoImportMaps } from '@jsenv/importmap'
import { dataToEsm } from '@rollup/pluginutils'
import { sveltekit } from '@sveltejs/kit/vite'
import { searchForWorkspaceRoot, defineConfig, transformWithEsbuild, send } from 'vite'
import commonjs from '@rollup/plugin-commonjs'
import nodePolyfills from 'rollup-plugin-polyfill-node'
import format from 'date-fns/format'
import { version as VERSION } from './package.json'
export default defineConfig((env) => {
const cdn = new Map<string, string | Buffer>()
const root = `_app/`
return {
plugins: [
sveltekit(),
{
name: 'generate-cdn',
async buildStart() {
cdn.clear()
console.time('generate-cdn')
const isProd = env.mode === 'production'
const emitFile = (fileName: string, source: string | Buffer | Uint8Array) => {
cdn.set(
fileName.slice(root.length - 1),
typeof source === 'string' || Buffer.isBuffer(source) ? source : Buffer.from(source),
)
if (env.command === 'build') {
this.emitFile({ type: 'asset', fileName, source })
}
}
// bundle current packages and generate resolutions fro @jsbpm/generator
// TODO: refactor to cdn.twind.style
// 1. find all packages
// - in dev use packages/*/package.json
// - in prod use packages/*/dist/package.json
const files = await fg(
[`../../packages/*/package.json`, '!../../packages/{cdn,twind,with-*}/**'],
{
absolute: true,
},
)
const conditions = [
'development',
'modern',
'esmodules',
'es0215',
'module',
'import',
'default',
]
console.time('generate-cdn:bundle')
const packages = await Promise.all(
files.map(async (manifestFile) => {
// try to re-use already build dist files
if (process.env.CI) {
const distManifestFile = path.resolve(manifestFile, '../dist/package.json')
try {
fs.access(distManifestFile)
manifestFile = distManifestFile
} catch {
// ignore
}
}
// 2. generate systemjs bundle with rollup for all entry points
// - https://www.npmjs.com/package/resolve.exports
const manifest = JSON.parse(await fs.readFile(manifestFile, { encoding: 'utf8' }))
manifest.exports = {
'./package.json': './package.json',
...(manifest.exports || {
'.': manifest.source || manifest.esnext || manifest.module || manifest.main,
}),
}
const exports = Object.keys(manifest.exports)
.map((entry) => {
const resolved = resolve(manifest, entry, { conditions })
if (!resolved) {
// ignore
return
}
const file = path.resolve(path.dirname(manifestFile), resolved)
return { entry, resolved, file }
})
.filter(<T>(value: T): value is Exclude<T, undefined | null> => value != undefined)
const external = new Set<string>()
const bundle = await rollup({
input: Object.fromEntries(
exports
.filter((input) => /\.[mc]?[jt]sx?$/.test(input.resolved))
.map((input) => [
input.entry === '.' ? manifest.name.split('/').pop() : input.entry.slice(2),
input.resolved,
]),
),
onwarn(warning, handler) {
if (warning.code !== 'CIRCULAR_DEPENDENCY') {
handler(warning)
}
},
plugins: [
{
name: 'virtual',
resolveId(source, importer, { isEntry }) {
if (isEntry) {
const input = exports.find((input) => input.resolved === source)
return input?.file
}
if (importer && source.startsWith('.')) {
const base = path.resolve(path.dirname(importer), source)
const extensions = [
'',
'.tsx',
'.jsx',
'.mts',
'.ts',
'.mjs',
'.js',
'.cts',
'.cjs',
'.json',
]
return Promise.resolve(0).then(async function find(
index,
): Promise<string | { id: string; external: boolean }> {
const ext = extensions[index]
if (ext == null) {
external.add(source)
return { id: source, external: true }
}
try {
await fs.access(base + ext)
return base + ext
} catch {
return find(index + 1)
}
})
}
if (source === 'distilt/env') {
const manifestFile = createRequire(importer || import.meta.url).resolve(
'distilt/package.json',
)
return fs
.readFile(manifestFile, { encoding: 'utf8' })
.then((raw) => JSON.parse(raw))
.then((manifest) => {
const resolved = resolve(manifest, './env', { conditions })
if (!resolved) {
external.add(source)
return { id: source, external: true }
}
return path.resolve(path.dirname(manifestFile), resolved)
})
}
external.add(source)
return { id: source, external: true }
},
async load(id) {
const content = await fs.readFile(id, { encoding: 'utf8' })
if (id.endsWith('.json')) {
return dataToEsm(JSON.parse(content), {
compact: isProd,
indent: isProd ? '' : '\t',
preferConst: true,
objectShorthand: true,
namedExports: true,
})
}
return content
},
},
{
name: 'esbuild',
transform(code, id) {
return transformWithEsbuild(code, id, {
format: 'esm',
minify: isProd,
sourcemap: 'external',
platform: 'browser',
// https://github.com/vitejs/vite/blob/main/packages/vite/src/node/constants.ts#L14
target: [
'es2020', // support import.meta.url
'edge88',
'firefox78',
'chrome87',
'safari13', // transpile nullish coalescing
],
})
},
},
],
})
// bundle.watchFiles
// 3. emit assets (add immutable cache-header in dev)
// - file: `this.emitFile({ type: 'asset', fileName: '-/cdn/<package>@<version>/<resolvedname>', source: code })})`
// - map: `this.emitFile({ type: 'asset', fileName: '-/cdn/<package>@<version>/<resolvedname>.map', source: map.toString() })})`
const { output } = await bundle.generate({
format: 'systemjs',
generatedCode: 'es2015',
sourcemap: true,
compact: isProd,
entryFileNames: process.env.CI ? '[name].js' : '[name]-[hash].js',
chunkFileNames: '_[hash].js',
})
await bundle.close()
const prefix = `immutable/cdn/${manifest.name}@${manifest.version}/`
const mapping: Record<string, string> = {}
for (const chunk of output) {
if (chunk.type === 'asset') {
emitFile(root + prefix + chunk.fileName, chunk.source)
} else {
const input =
chunk.isEntry && exports.find((input) => input.file === chunk.facadeModuleId)
if (input) {
// name/
mapping[manifest.name + input.entry.slice(1)] = './' + prefix + chunk.fileName
manifest.exports[input.entry] = './' + chunk.fileName
}
emitFile(root + prefix + chunk.fileName, chunk.code)
}
}
await Promise.all(
exports
.filter((input) => !/\.[mc]?[jt]sx?$/.test(input.resolved))
.map(async (input) => {
// name/
mapping[manifest.name + '/' + input.entry.slice(2)] =
'./' + prefix + input.entry.slice(2)
emitFile(
root + prefix + input.entry.slice(2),
input.entry === './package.json'
? JSON.stringify(
{
...manifest,
type: 'systemjs',
engines: undefined,
peerDependencies: undefined,
peerDependenciesMeta: undefined,
publishConfig: undefined,
main: undefined,
module: undefined,
esnext: undefined,
unpkg: undefined,
jsdelivr: undefined,
browser: undefined,
types: undefined,
},
null,
2,
)
: await fs.readFile(input.file, { encoding: 'utf8' }),
)
}),
)
return {
url: pathToFileURL(manifestFile),
manifest,
prefix,
exports,
mapping,
external,
}
}),
)
console.timeEnd('generate-cdn:bundle')
// 3. emit inputMap (application/importmap+json) with relative paths
// - file: `this.emitFile({ type: 'asset', fileName: '-/cdn/importmap.json', source: code })})`
console.time('generate-cdn:importMap')
// console.debug(packages)
const scoped = await Promise.all(
packages.map(async ({ url, prefix, external, manifest }) => {
const cdnUrl = 'https://cdn.jsdelivr.net/'
const exactPkgRegEx =
/^([^\/]+)\/((?:@[^/\\%@]+\/)?[^./\\%@][^/\\%@]*)@([^\/]+)(\/.*)?$/
const generator: Generator = new Generator({
// Set the map URL for relative normalization when installing local packages
mapUrl: url,
env: conditions,
defaultProvider: 'jspm.system',
// jspm may not have the previously (a few seconds ago) published packages -> use
providers: {
// unpkg or skypack may work as well
'@twind': 'local',
twind: 'local',
},
customProviders: {
// Based on https://github.com/jspm/generator/blob/main/src/providers/jsdelivr.ts
// but resolve latest to current build
local: {
// https://cdn.jsdelivr.net/npm/twind@1.0.0-next-20221115000153/package.json
parseUrlPkg(url) {
const pkg = packages.find((pkg) => new URL('.', pkg.url).href === url)
if (pkg) {
return {
registry: 'npm',
name: pkg.manifest.name,
version: pkg.manifest.version,
}
}
const match = url.slice(cdnUrl.length).match(exactPkgRegEx)
if (match) {
const [, registry, name, version] = match
return { registry, name, version }
}
return undefined
},
pkgToUrl(target) {
const pkg = packages.find((pkg) => pkg.manifest.name === target.name)
if (pkg) {
return `${cdnUrl}${target.registry}/${pkg.manifest.name}@${pkg.manifest.version}/`
}
return `${cdnUrl}${target.registry}/${target.name}@${target.version}/`
},
async resolveLatestTarget(target, layer, parentUrl) {
const pkg = packages.find((pkg) => pkg.manifest.name === target.name)
if (pkg && target.range.has(pkg.manifest.version)) {
return {
registry: 'npm',
name: pkg.manifest.name,
version: pkg.manifest.version,
}
}
return null
},
},
},
})
const versions: Record<string, string> = {
...manifest.devDependencies,
...manifest.peerDependencies,
...manifest.dependencies,
}
const result = await generator.install(
await Promise.all(
[...external].map(async (specifier) => {
const match = specifier.match(/^((?:@[^/]+\/)?[^/@]+)(?:@([^/]+))?(\/.+)?$/)
if (!match) return specifier
const { 1: id, 3: path = '' } = match
try {
const { version } = JSON.parse(
await fs.readFile(
new URL(`./node_modules/${id}/package.json`, url),
'utf8',
),
)
return version ? `${id}@${version}${path}` : id + path
} catch {
const version = versions[id]
return version ? `${id}@${version}${path}` : id + path
}
}),
),
)
return {
prefix,
importMap: generator.importMap,
...(result || { staticDeps: [], dynamicDeps: [] }),
}
}),
)
let importMap: import('@jsenv/importmap').ImportMap = {
imports: Object.fromEntries(packages.flatMap(({ mapping }) => Object.entries(mapping))),
}
for (const scope of scoped) {
const imports = Object.entries(scope.importMap.imports).filter(
([key]) => !importMap.imports?.hasOwnProperty(key),
)
if (imports.length) {
importMap = composeTwoImportMaps(importMap, {
scopes: {
['./' + scope.prefix]: Object.fromEntries(imports) as Record<string, string>,
},
})
}
const scopes = scope.importMap.scopes
if (scopes) {
importMap = composeTwoImportMaps(importMap, { scopes })
}
}
console.timeEnd('generate-cdn:importMap')
// console.debug(importMap)
const version = process.env.CI
? VERSION
: VERSION.replace(/^([.\d]+)(-.+)?$/, `$1-dev-${format(Date.now(), 'yyyyMMddHHmmss')}`)
const cdnManifest = {
version,
// from version: 1.0.0 -> latest, 1.0.0-(canary|next|dev)-*
'dist-tag': version.replace(/^[.\d]+(?:-([^.-]+).+)?$/, '$1') || 'latest',
'git-sha': process.env.GITHUB_SHA || execSync('git rev-parse HEAD').toString().trim(),
pr:
process.env.GITHUB_EVENT_NAME === 'pull_request'
? execSync('jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH"')
.toString()
.trim()
: undefined,
...importMap,
packages: Object.fromEntries(
packages.map(({ manifest }) => [manifest.name, manifest.version]),
),
}
// console.debug({
// ...manifestData,
// // cdn: [...cdn.keys()],
// })
emitFile(root + 'cdn.json', JSON.stringify(cdnManifest, null, isProd ? undefined : 2))
console.timeEnd('generate-cdn')
},
configureServer(server) {
server.middlewares.use('/' + root, (req, res, next) => {
const content = req.url && cdn.get(req.url)
if (content != null) {
let type = path.extname(req.url as string)
if (type.startsWith('.')) {
type = type.slice(1)
}
if (type === 'map') {
type = 'json'
}
if (/^[mc]?[jt]sx?$/.test(type)) {
type = 'js'
}
return send(req, res, content, type, {
headers: server.config.server.headers,
})
}
console.debug('not found', req.url, cdn.keys())
next()
})
},
},
],
resolve: {
// no browser condition because this is sometimes used for umd builds
exportConditions: [
env.mode,
'esnext',
'modern',
'esmodules',
'es2015',
'module',
'import',
'require',
'default',
env.ssrBuild && 'node',
].filter(Boolean),
},
worker: {
format: env.mode === 'development' ? 'es' : 'iife',
plugins: [
{
name: 'fix-umd-imports',
enforce: 'pre',
resolveId(source, importer, options) {
return this.resolve(source, importer, { ...options, skipSelf: true }).then(
(resolved) => {
if (resolved.id?.includes('/@jridgewell/') && resolved.id.endsWith('.umd.js')) {
resolved.id = resolved.id.replace(/\.umd\.js$/, '.mjs')
}
return resolved
},
)
},
},
env.mode !== 'development' &&
commonjs({
sourceMap: false,
}),
env.mode !== 'development' &&
nodePolyfills({
// transform all files, including all files including any source files
include: null,
}),
{
name: 'resolve-to-location',
resolveFileUrl({ fileName }) {
return `new URL('${fileName}',location.href).href`
},
},
{
name: 'fix-invalid-code',
generateBundle(options, bundle) {
for (const chunk of Object.values(bundle)) {
if (chunk.type === 'chunk') {
chunk.code = chunk.code.replaceAll('(1.toString)', '(1 .toString)')
}
}
},
},
].filter(Boolean),
rollupOptions: {
output: {
inlineDynamicImports: env.mode !== 'development',
},
},
},
clearScreen: false,
build: {
sourcemap: !process.env.CI,
},
server: {
fs: {
// search up for workspace root
allow: [searchForWorkspaceRoot(process.cwd())],
},
watch: {
ignored: [fileURLToPath(new URL('.mf/**', import.meta.url))],
},
},
}
})