module transform api design (Analysis) #315
Replies: 4 comments 8 replies
-
Plugin CoordinateConvert module typetransform some file type to internal translator, such as convert an svg to jsx esbuild: return loader in transform hooktransform(code,id){
if(!id.endsWith('.svg')){
return;
}
const jsxCode = svg2jsx(code);
return {
code: jsxCode,
loader: 'jsx'
}
} rollup | vite mock query to file extensionmethod 1
transform(code,id){
const { filepath, query} = parseRequest(id);
if(!id.endsWith('.svg')){
return;
}
const jsxCode = svg2jsx(code);
const jsCode = babel.transform(jsxCode); // we may not know what other transform doing to jsxCode, such as babel polyfill
return {
code: jsCode,
}
} method 2
{
name: 'svg',
async resolveId(id, importer) {
// convert .svg to .svg&lang.jsx so jsx plugin can handle this
if (id.endsWith('.svg')) {
const { id: resolvedId } = await this.resolve(id, importer, { skipSelf: true });
return {
id: appendQuery(resolvedId, { loader: 'jsx' }),
};
} else {
return;
}
},
async transform(code, id) {
const { filePath, query } = parseRequest(id);
if (filePath.endsWith('.svg')) {
const jsxCode = await transform(code);
return {
code: jsxCode,
};
} else {
return undefined;
}
},
async load(id) {
const { filePath, query } = parseRequest(id);
return {
code: fs.readFileSync(filePath, 'utf-8'),
};
},
},
{
name: 'jsx',
async transform(code, id) {
const loader = defaultLoader(id);
if (loader === 'jsx') {
const jsCode = esbuild.transformSync(code, { loader: 'jsx' }).code;
return {
code: jsCode,
};
}
},
},
webpack loader{
test: /\.svg/,
use: ['svgr-loader', 'babel-loader']
}
nodejs loader
/**
*
* @param {string} url
* @param {any} context
* @param {Function} next
* @returns
*/
async function load(url, context, next) {
if (url.endsWith('.svg')) {
let result = await next(
url,
{
...context,
format: 'module',
},
next
);
const jsxCode = await transform(result.source.toString(), {}, {});
const code = (
await esbuild.transformSync(jsxCode, {
loader: 'jsx',
})
).code;
return {
format: 'module'
source: code,
shortCircuit: true,
};
} else {
return next(url, context, next);
}
}
/**
*
* @param {string} id
* @param {*} context
* @param {*} next
*/
async function resolve(id, context, next) {
// handle unknown file extension problem
if (id.endsWith('.svg')) {
let url = new URL(id, context.parentURL).href;
return {
url,
format: 'module',
shortCircuit: true,
};
}
return next(id, context, next);
} async function load(url, context, next) {
if (context.format === 'jsx') {
const code = await next(url, { ...context, format: 'module' }, next);
const jsCode = esbuild.transformSync(code, { format: 'esm' });
return {
source: jsCode.code,
format: 'module',
short,
};
} else {
return {
source: code,
format: context.format,
};
}
} |
Beta Was this translation helpful? Give feedback.
-
Vue Loader ImplementationVue loader Implementation is a much more complex use case than svgr case, we need to deal with
Parcel Implementation
{
"*.vue": ["@parcel/transformer-vue"],
"template:*.vue": ["@parcel/transformer-vue"],
"script:*.vue": ["@parcel/transformer-vue"],
"style:*.vue": ["@parcel/transformer-vue"],
"custom:*.vue": ["@parcel/transformer-vue"],
}
const { Transformer } = require('@parcel/plugin');
const compiler = require('@vue/compiler-sfc');
const semver = require('semver');
const { hashObject } = require('@parcel/utils');
const path = require('path');
module.exports = new Transformer({
canReuseAST({ ast }) {
return ast.type === 'vue' && semver.satisfies(ast.version, '^3.0.0');
},
async parse({ asset }) {
let code = await asset.getCode();
let parsed = compiler.parse(code, {
sourceMap: true,
filename: asset.filePath,
});
let descriptor = parsed.descriptor;
let id = hashObject({
filePath: asset.filePath,
source: code,
}).slice(-6);
return {
type: 'vue',
version: '3.0.0',
program: {
...descriptor,
script: compiler.compileScript(descriptor, {
id,
isProd: false,
}),
},
};
},
async transform({ asset, options, resolve, config }) {
const ast = (await asset.getAST()).program;
console.log('ast:', ast);
const { template, script, style, customBlocks } = ast;
const code = await asset.getCode();
let basePath = path.basename(asset.filePath);
console.log('pipeline:', asset.pipeline);
if (asset.pipeline) {
// script:xxx.vue, template:xxx.vue, style:xxx.vue, custom:xxx.vue goes here
switch (asset.pipeline) {
case 'script':
console.log('xxx:', script);
const lang = script.lang;
// pass script to 'ts'|'js'|'jsx'|'tsx' handler
return [
{
type: lang || 'js',
content: script.content,
},
];
case 'style':
return [
{
type: style.lang || 'css',
content: style.content,
},
];
case 'template':
const template_code = compiler.compileTemplate({
filename: asset.filePath,
source: template.content,
isProd: false,
isFunctional: false,
});
return [
{
type: 'js',
content: template_code,
},
];
}
} else {
// xxx.vue goes here
return [
{
type: 'js',
content: `
require('script:./${basePath}');
require('template:./${basePath}');
require('style:./${basePath}');
require('custom:./${basePath}');
`,
},
];
}
},
}); vite implementationhttps://github.com/vitejs/vite/blob/main/packages/plugin-vue/src/index.ts#L171 Webpack implementationusing matchResource to deal with change module type in loader suggested by @sodatea const compiler = require('@vue/compiler-sfc');
const loader_utils = require('loader-utils');
const qs = require('qs');
const descCache = new Map();
const util = require('util');
/**
* @this {import('webpack').LoaderContext<any>}
*/
module.exports = function vueLoader(content) {
const callback = this.async();
const inner = async () => {
const parsed = compiler.parse(content, {
sourceMap: false,
filename: this.resourcePath,
});
const descriptor = parsed.descriptor;
const { script, scriptSetup, styles, template, customBlocks } = descriptor;
descCache.set(this.resource, {
...parsed.descriptor,
script: compiler.compileScript(descriptor, {
id: Math.random().toString(),
isProd: false,
}),
});
const query = qs.parse(this.resourceQuery, { ignoreQueryPrefix: true });
if (query.vue === 'true') {
let cache = descCache.get(this.resource);
const { script, style } = cache;
if (query.type === 'script') {
return script.content;
} else if (query.type === 'style') {
const lang = styles[0].lang;
return styles[0].content;
}
} else {
const jsPath = this.resourcePath + '.js' + '!=!' + this.resourcePath + '?vue=true&type=script';
// const jscode = await util.promisify(this.loadModule)(jsPath);
const cssPath = this.resourcePath + '.less' + '!=!' + this.resourcePath + '?vue=true&type=style';
// const csscode = await util.promisify(this.loadModule)(cssPath);
// console.log('csscode:', csscode);
return `require(${JSON.stringify(jsPath)});require(${JSON.stringify(cssPath)})`;
}
};
return util.callbackify(inner)((err, data) => {
callback(err, data);
});
}; |
Beta Was this translation helpful? Give feedback.
-
virtual modulesometime we need to use virtual module to generate module on the fly, but it often cause some trouble for plugin. special idsee https://github.com/vitejs/vite/blob/main/packages/plugin-vue/src/index.ts#L162
const EXPORT_HELPER_ID = 'plugin-vue:export-helper'
const helperCode = `
export default (sfc, props) => {
const target = sfc.__vccOpts || sfc;
for (const [key, val] of props) {
target[key] = val;
}
return target;
}`;
export default function vuePlugin(){
return {
async resolveId(id){
if(id === EXPORT_HELPER_ID) {
return id
}
},
load(id){
if( id === EXPORT_HELPER_ID){
return helperCode;
}
}
}
}
` id with querywe may add special query to annotate an module as virtual module,
export default function vuePlugin(){
return {
async resolveId(id){
if(parseRequest(id).query.vue){
return id;
}
},
load(id){
if(parseRequest(id).query.vue){
// do something
}
}
}
} rollup's id is easy to be broken when deal with polluted id, every plugin should do the parseRequest trick to avoid being broken, use
|
Beta Was this translation helpful? Give feedback.
-
I found a problem of In this model, |
Beta Was this translation helpful? Give feedback.
-
Purpose
There are five mainstream kinds of module transform api frontend tooling is using
all of these ways have some pros and cons and we need to discuss which api fit us well
Core Concept
core concept for further discussion( I just borrow it from nodejs loader
Use Cases
We need to handle the following scenario very well by loader plugin
Let's discuss these in separate sections
Beta Was this translation helpful? Give feedback.
All reactions