@chialab/esbuild-plugin-transform
Advanced tools
Comparing version 0.8.4 to 0.9.0
245
lib/index.js
@@ -6,4 +6,15 @@ import { promises } from 'fs'; | ||
const { readFile } = promises; | ||
const { default: SourceMap } = sourcemap; | ||
const { default: SourceMapNode } = sourcemap; | ||
/** | ||
* @typedef {Object} SourceMap | ||
* @property {number} version | ||
* @property {string[]} sources | ||
* @property {string[]} names | ||
* @property {string} [sourceRoot] | ||
* @property {string[]} [sourcesContent] | ||
* @property {string} mappings | ||
* @property {string} file | ||
*/ | ||
export const SCRIPT_LOADERS = ['tsx', 'ts', 'jsx', 'js']; | ||
@@ -28,6 +39,10 @@ | ||
/** | ||
* @typedef {{ filter: RegExp, store: Map<string, Entry>, getEntry(filePath: string): Promise<Entry>, buildEntry(filePath: string, extra?: Partial<import('esbuild').OnLoadResult>): Promise<import('esbuild').OnLoadResult|undefined>, finishEntry(filePath: string, extra?: Partial<import('esbuild').OnLoadResult>): Promise<import('esbuild').OnLoadResult|undefined> }} TransformOptions | ||
* @typedef {(filePath: string, result: { code: string, map?: SourceMap|SourceMap[], loader?: import('esbuild').Loader }, extra?: Partial<import('esbuild').OnLoadResult>) => Promise<import('esbuild').OnLoadResult|undefined>} BuildFactory | ||
*/ | ||
/** | ||
* @typedef {{ entry?: Entry, filter: RegExp, store: Map<string, Entry>, getEntry(filePath: string): Promise<Entry>, buildEntry: BuildFactory }} TransformOptions | ||
*/ | ||
/** | ||
* @typedef {import('esbuild').BuildOptions & { transform?: TransformOptions }} BuildTransformOptions | ||
@@ -61,3 +76,2 @@ */ | ||
buildEntry: buildEntryFactory(build, options), | ||
finishEntry: buildEntryFactory(build, options), | ||
}; | ||
@@ -68,6 +82,7 @@ } | ||
* @param {string} filePath | ||
* @param {string} [contents] | ||
* @return {Promise<Entry>} | ||
*/ | ||
export async function createEntry(filePath) { | ||
const code = await readFile(filePath, 'utf-8'); | ||
export async function createEntry(filePath, contents) { | ||
const code = contents || await readFile(filePath, 'utf-8'); | ||
return { | ||
@@ -83,2 +98,33 @@ filePath, | ||
/** | ||
* Transpile entry to standard js. | ||
* @param {Entry} entry | ||
* @param {typeof import('esbuild')} esbuild | ||
* @param {import('esbuild').BuildOptions} [options] | ||
*/ | ||
export async function transpileEntry(entry, esbuild, options = {}) { | ||
if (entry.target !== TARGETS.typescript) { | ||
return { | ||
code: entry.code, | ||
loader: entry.loader, | ||
}; | ||
} | ||
const loaders = options.loader || {}; | ||
const { code, map } = await esbuild.transform(entry.code, { | ||
sourcefile: entry.filePath, | ||
sourcemap: true, | ||
loader: loaders[path.extname(entry.filePath)] === 'ts' ? 'ts' : 'tsx', | ||
format: 'esm', | ||
target: TARGETS.es2020, | ||
jsxFactory: options.jsxFactory, | ||
jsxFragment: options.jsxFragment, | ||
}); | ||
return { | ||
code, | ||
map, | ||
loader: /** @type {import('esbuild').Loader} */ ('js'), | ||
}; | ||
} | ||
/** | ||
* @param {import('esbuild').PluginBuild} build | ||
@@ -89,11 +135,12 @@ */ | ||
* @param {string} filePath | ||
* @param {string} [initialContents] | ||
*/ | ||
async function getEntry(filePath) { | ||
async function getEntry(filePath, initialContents) { | ||
const options = /** @type {BuildTransformOptions} */ (build.initialOptions); | ||
if (!options.transform) { | ||
return await createEntry(filePath); | ||
return await createEntry(filePath, initialContents); | ||
} | ||
const store = options.transform.store; | ||
const entry = store.get(filePath) || await createEntry(filePath); | ||
const entry = store.get(filePath) || await createEntry(filePath, initialContents); | ||
store.set(filePath, entry); | ||
@@ -107,2 +154,37 @@ return entry; | ||
/** | ||
* @param {string} basename | ||
* @param {string} original | ||
*/ | ||
function createInitialSourceMap(basename, original) { | ||
const initialSourceMap = new SourceMapNode(); | ||
initialSourceMap.setSourceContent(basename, original); | ||
return initialSourceMap; | ||
} | ||
/** | ||
* @param {string} basename | ||
* @param {string} original | ||
* @param {SourceMap[]} mappings | ||
*/ | ||
function mergeMappings(basename, original, mappings) { | ||
const initial = createInitialSourceMap(basename, original); | ||
const sourceMap = mappings.reduce((sourceMap, mapping) => { | ||
mapping.file = basename; | ||
mapping.sources = [basename]; | ||
mapping.sourcesContent = [original]; | ||
try { | ||
const map = new SourceMapNode(); | ||
map.addVLQMap(mapping); | ||
map.extends(sourceMap.toBuffer()); | ||
return map; | ||
} catch (err) { | ||
// | ||
} | ||
return sourceMap; | ||
}, initial); | ||
return sourceMap.toVLQ(); | ||
} | ||
/** | ||
* @param {import('esbuild').PluginBuild} build | ||
@@ -113,11 +195,5 @@ * @param {import('esbuild').BuildOptions} options | ||
/** | ||
* @param {string} filePath | ||
* @param {Partial<import('esbuild').OnLoadResult>} extra | ||
* @return {Promise<import('esbuild').OnLoadResult|undefined>} | ||
* @type {BuildFactory} | ||
*/ | ||
async function buildEntry(filePath, extra = {}) { | ||
if (!shouldReturn) { | ||
return; | ||
} | ||
async function buildEntry(filePath, { code, map, loader }, extra = {}) { | ||
const { store } = getTransformOptions(build); | ||
@@ -129,5 +205,19 @@ const entry = store.get(filePath); | ||
if (!shouldReturn) { | ||
entry.code = code; | ||
if (Array.isArray(map)) { | ||
entry.mappings.push(...map); | ||
} else if (map) { | ||
entry.mappings.push(map); | ||
} | ||
if (loader) { | ||
entry.loader = loader; | ||
} | ||
return; | ||
} | ||
const loaders = options.loader || {}; | ||
const defaultLoader = (loaders[path.extname(filePath)] === 'ts' ? 'ts' : 'tsx'); | ||
const { original, mappings, code } = entry; | ||
const { original } = entry; | ||
const mappings = Array.isArray(map) ? map : (map ? [map] : entry.mappings); | ||
if (!mappings.length) { | ||
@@ -142,29 +232,12 @@ return { | ||
const basename = path.basename(entry.filePath); | ||
const initialSourceMap = new SourceMap(); | ||
initialSourceMap.setSourceContent(basename, original); | ||
const sourceMap = mappings.reduce((sourceMap, mapping) => { | ||
mapping.file = basename; | ||
mapping.sources = [basename]; | ||
mapping.sourcesContent = [original]; | ||
try { | ||
const map = new SourceMap(); | ||
map.addVLQMap(mapping); | ||
map.extends(sourceMap.toBuffer()); | ||
return map; | ||
} catch(err) { | ||
// | ||
} | ||
return sourceMap; | ||
}, initialSourceMap); | ||
const finalMap = mappings.length > 1 ? mergeMappings(basename, original, mappings) : mappings[0]; | ||
finalMap.version = 3; | ||
finalMap.file = basename; | ||
finalMap.sources = [basename]; | ||
finalMap.sourcesContent = [original]; | ||
const map = sourceMap.toVLQ(); | ||
map.version = 3; | ||
map.file = basename; | ||
map.sources = [basename]; | ||
map.sourcesContent = [original]; | ||
return { | ||
...extra, | ||
loader: entry.loader || extra.loader || defaultLoader, | ||
contents: `${code}\n//# sourceMappingURL=data:application/json;base64,${Buffer.from(JSON.stringify(map)).toString('base64')}`, | ||
contents: `${code}\n//# sourceMappingURL=data:application/json;base64,${Buffer.from(JSON.stringify(finalMap)).toString('base64')}`, | ||
}; | ||
@@ -177,5 +250,10 @@ } | ||
/** | ||
* @typedef {(args: import('esbuild').OnLoadArgs) => import('esbuild').OnLoadResult} LoadCallback | ||
*/ | ||
/** | ||
* @param {import('esbuild').Plugin[]} plugins | ||
* @return An esbuild plugin. | ||
*/ | ||
export function start() { | ||
export default function(plugins = []) { | ||
/** | ||
@@ -185,3 +263,3 @@ * @type {import('esbuild').Plugin} | ||
const plugin = { | ||
name: 'transform-start', | ||
name: 'transform', | ||
setup(build) { | ||
@@ -195,2 +273,15 @@ /** | ||
const getEntry = getEntryFactory(build); | ||
const buildEntry = buildEntryFactory(build, options, false); | ||
const finishEntry = buildEntryFactory(build, options); | ||
const { stdin, loader: loaders = {} } = options; | ||
const input = stdin ? (stdin.sourcefile || 'stdin.js') : undefined; | ||
if (stdin && input) { | ||
const regex = new RegExp(input.replace(/([()[\]{}\\\-+.*?^$])/g, '\\$1')); | ||
build.onResolve({ filter: regex }, () => ({ path: input, namespace: 'file' })); | ||
delete options.stdin; | ||
options.entryPoints = [input]; | ||
} | ||
Object.defineProperty(options, 'transform', { | ||
@@ -202,30 +293,53 @@ enumerable: false, | ||
store, | ||
getEntry: getEntryFactory(build), | ||
buildEntry: buildEntryFactory(build, options, false), | ||
finishEntry: buildEntryFactory(build, options), | ||
getEntry, | ||
buildEntry, | ||
}, | ||
}); | ||
}, | ||
}; | ||
return plugin; | ||
} | ||
/** | ||
* @type {Array<[import('esbuild').OnLoadOptions, LoadCallback]>} | ||
*/ | ||
const onLoad = []; | ||
for (let i = 0; i < plugins.length; i++) { | ||
plugins[i].setup({ | ||
initialOptions: build.initialOptions, | ||
onStart: build.onStart.bind(build), | ||
onEnd: build.onEnd.bind(build), | ||
onResolve: build.onResolve.bind(build), | ||
/** | ||
* @param {import('esbuild').OnLoadOptions} options | ||
* @param {LoadCallback} callback | ||
*/ | ||
onLoad(options, callback) { | ||
if (options.namespace === 'file') { | ||
onLoad.push([options, callback]); | ||
} else { | ||
build.onLoad(options, callback); | ||
} | ||
}, | ||
}); | ||
} | ||
/** | ||
* @return An esbuild plugin. | ||
*/ | ||
export function end() { | ||
/** | ||
* @type {import('esbuild').Plugin} | ||
*/ | ||
const plugin = { | ||
name: 'transform-end', | ||
setup(build) { | ||
const options = build.initialOptions; | ||
const loaders = options.loader || {}; | ||
const { filter, finishEntry } = getTransformOptions(build); | ||
build.onLoad({ filter, namespace: 'file' }, async (args) => { | ||
if (args.path === input && stdin) { | ||
await getEntry(args.path, stdin.contents); | ||
} | ||
build.onLoad({ filter, namespace: 'file' }, async (args) => finishEntry(args.path, { | ||
loader: loaders[path.extname(args.path)] === 'ts' ? 'ts' : 'tsx', | ||
})); | ||
for (let i = 0; i < onLoad.length; i++) { | ||
const [{ filter, namespace }, callback] = onLoad[i]; | ||
if (!filter.test(args.path) || (namespace && namespace !== args.namespace)) { | ||
continue; | ||
} | ||
await callback(args); | ||
} | ||
const { code, mappings, loader } = await getEntry(args.path); | ||
return finishEntry(args.path, { | ||
code, | ||
map: mappings, | ||
loader, | ||
}, { | ||
loader: loaders[path.extname(args.path)] === 'ts' ? 'ts' : 'tsx', | ||
}); | ||
}); | ||
}, | ||
@@ -236,1 +350,2 @@ }; | ||
} | ||
{ | ||
"name": "@chialab/esbuild-plugin-transform", | ||
"type": "module", | ||
"version": "0.8.4", | ||
"version": "0.9.0", | ||
"description": "Pipe transformation plugin for esbuild.", | ||
@@ -13,3 +13,3 @@ "main": "lib/index.js", | ||
"url": "https://github.com/chialab/rna", | ||
"directory": "packages/esbuild-plugin-commonjs" | ||
"directory": "packages/esbuild-plugin-transform" | ||
}, | ||
@@ -43,3 +43,3 @@ "keywords": [ | ||
}, | ||
"gitHead": "dfc6716438e20433c8f424ec1f0689a2c80f167f" | ||
"gitHead": "530ad171be0786bc9e5fdc044276467db3a2697a" | ||
} |
@@ -22,8 +22,9 @@ <p align="center"> | ||
import esbuild from 'esbuild'; | ||
import { start, end } from '@chialab/esbuild-plugin-transform'; | ||
import transform from '@chialab/esbuild-plugin-transform'; | ||
await esbuild.build({ | ||
plugins: [ | ||
start(), | ||
end(), | ||
transform([ | ||
// plugins | ||
]), | ||
], | ||
@@ -55,9 +56,9 @@ }); | ||
build.onLoad({ filter: /\./, namespace: 'file' }, async (args) => { | ||
build.onLoad({ filter, namespace: 'file' }, async (args) => { | ||
const entry = await getEntry(args.path); | ||
const { code, map } = await transform(entry.code); | ||
entry.code = code; | ||
entry.mappings.push(map); | ||
return buildEntry(entry, { | ||
code, | ||
map, | ||
loader: 'js', | ||
@@ -64,0 +65,0 @@ }); |
@@ -5,5 +5,8 @@ /** | ||
/** | ||
* @typedef {{ filter: RegExp, store: Map<string, Entry>, getEntry(filePath: string): Promise<Entry>, buildEntry(filePath: string, extra?: Partial<import('esbuild').OnLoadResult>): Promise<import('esbuild').OnLoadResult|undefined>, finishEntry(filePath: string, extra?: Partial<import('esbuild').OnLoadResult>): Promise<import('esbuild').OnLoadResult|undefined> }} TransformOptions | ||
* @typedef {(filePath: string, result: { code: string, map?: SourceMap|SourceMap[], loader?: import('esbuild').Loader }, extra?: Partial<import('esbuild').OnLoadResult>) => Promise<import('esbuild').OnLoadResult|undefined>} BuildFactory | ||
*/ | ||
/** | ||
* @typedef {{ entry?: Entry, filter: RegExp, store: Map<string, Entry>, getEntry(filePath: string): Promise<Entry>, buildEntry: BuildFactory }} TransformOptions | ||
*/ | ||
/** | ||
* @typedef {import('esbuild').BuildOptions & { transform?: TransformOptions }} BuildTransformOptions | ||
@@ -22,13 +25,39 @@ */ | ||
* @param {string} filePath | ||
* @param {string} [contents] | ||
* @return {Promise<Entry>} | ||
*/ | ||
export function createEntry(filePath: string): Promise<Entry>; | ||
export function createEntry(filePath: string, contents?: string | undefined): Promise<Entry>; | ||
/** | ||
* @return An esbuild plugin. | ||
* Transpile entry to standard js. | ||
* @param {Entry} entry | ||
* @param {typeof import('esbuild')} esbuild | ||
* @param {import('esbuild').BuildOptions} [options] | ||
*/ | ||
export function start(): import("esbuild").Plugin; | ||
export function transpileEntry(entry: Entry, esbuild: typeof import('esbuild'), options?: import("esbuild").BuildOptions | undefined): Promise<{ | ||
code: string; | ||
loader: import("esbuild").Loader | undefined; | ||
map?: undefined; | ||
} | { | ||
code: string; | ||
map: string; | ||
loader: import('esbuild').Loader; | ||
}>; | ||
/** | ||
* @typedef {(args: import('esbuild').OnLoadArgs) => import('esbuild').OnLoadResult} LoadCallback | ||
*/ | ||
/** | ||
* @param {import('esbuild').Plugin[]} plugins | ||
* @return An esbuild plugin. | ||
*/ | ||
export function end(): import("esbuild").Plugin; | ||
export default function _default(plugins?: import('esbuild').Plugin[]): import("esbuild").Plugin; | ||
/** | ||
* @typedef {Object} SourceMap | ||
* @property {number} version | ||
* @property {string[]} sources | ||
* @property {string[]} names | ||
* @property {string} [sourceRoot] | ||
* @property {string[]} [sourcesContent] | ||
* @property {string} mappings | ||
* @property {string} file | ||
*/ | ||
export const SCRIPT_LOADERS: string[]; | ||
@@ -51,11 +80,16 @@ export namespace TARGETS { | ||
target: string; | ||
loader?: import("esbuild").Loader | undefined; | ||
loader?: import('esbuild').Loader; | ||
mappings: SourceMap[]; | ||
}; | ||
export type BuildFactory = (filePath: string, result: { | ||
code: string; | ||
map?: SourceMap | SourceMap[]; | ||
loader?: import('esbuild').Loader; | ||
}, extra?: Partial<import("esbuild").OnLoadResult> | undefined) => Promise<import('esbuild').OnLoadResult | undefined>; | ||
export type TransformOptions = { | ||
entry?: Entry | undefined; | ||
filter: RegExp; | ||
store: Map<string, Entry>; | ||
getEntry(filePath: string): Promise<Entry>; | ||
buildEntry(filePath: string, extra?: Partial<import("esbuild").OnLoadResult> | undefined): Promise<import('esbuild').OnLoadResult | undefined>; | ||
finishEntry(filePath: string, extra?: Partial<import("esbuild").OnLoadResult> | undefined): Promise<import('esbuild').OnLoadResult | undefined>; | ||
buildEntry: BuildFactory; | ||
}; | ||
@@ -65,1 +99,11 @@ export type BuildTransformOptions = import('esbuild').BuildOptions & { | ||
}; | ||
export type LoadCallback = (args: import('esbuild').OnLoadArgs) => import('esbuild').OnLoadResult; | ||
export type SourceMap = { | ||
version: number; | ||
sources: string[]; | ||
names: string[]; | ||
sourceRoot?: string | undefined; | ||
sourcesContent?: string[] | undefined; | ||
mappings: string; | ||
file: string; | ||
}; |
17734
404
75