Inside san-loader: How San Files Are Split, Compiled, and Integrated in Webpack
This article provides a deep technical walkthrough of san-loader, explaining how the Baidu‑developed San framework’s .san files are parsed, split into template, script and style sections, processed by san-loader‑plugin, and finally compiled into browser‑ready code with support for options like compileTemplate, esModule, and CSS Modules.
Overview
San is Baidu’s MVVM front‑end framework, similar to Vue or React. Files with a .san extension contain template, script and style sections. san-loader is a webpack pre‑processor that parses a .san file, splits it into those three parts and forwards each part to the appropriate downstream loaders, ultimately producing browser‑executable code.
san-loader and san-loader‑plugin
san-loader consists of two components:
san-loader – only identifies .san files and extracts the three code blocks.
san-loader‑plugin – inserts the loader into the webpack compilation lifecycle so that it runs before other loaders, guaranteeing the required execution order.
Earlier versions bundled all downstream loaders inside san-loader, which caused maintenance problems when any loader was upgraded. The redesign separates responsibilities: the loader only splits files, while the plugin ensures correct ordering.
SanLoaderPlugin
Key webpack concepts used by the plugin:
compiler – the global compilation object created during webpack initialization, containing the configuration from webpack.config.js.
compilation – a new object generated for each build, holding information about changed files.
apply – the method webpack calls on a plugin instance; custom logic is placed inside this method.
class SanLoaderPlugin {
apply(compiler) {
// plugin logic
}
}The plugin clones the existing module.rules, inserts a rule for san-loader before the other rules, and then replaces compiler.options.module.rules with the new ordered list.
apply(compiler) {
const rules = compiler.options.module.rules;
const clonedRules = rules.filter(r => r !== rawSanRules)
.map(rawRule => cloneRule(rawRule, new Map()));
compiler.options.module.rules = [...clonedRules, ...rules];
}After the plugin guarantees the order, the split blocks are processed by downstream loaders such as html-loader, css-loader, etc., using query strings like lang=html&san=&type=template.
Execution Flow
The loader uses htmlparser2 to parse the source and produce a descriptor object that groups AST nodes for template, script and style. The flow includes parsing, path resolution, import generation and final export assembly.
File Splitting
let descriptor = {};
for (let node of ast) {
if (ELEMENT_TYPES.includes(node.type) && tagNames.includes(node.name)) {
descriptor[node.name] = descriptor[node.name] || [];
descriptor[node.name].push(node);
}
}Source‑map Generation
const MagicString = require('magic-string');
function stringManager(code, ast) {
const s = new MagicString(code);
traverse(ast, node => {
if (node && node.type !== 'comment') {
s.addSourcemapLocation(node.startIndex);
if (node.children && node.children[0] && node.children[0].startIndex != null) {
s.addSourcemapLocation(node.children[0].startIndex - 1);
}
}
});
return s;
}
const s = stringManager(source, ast);
const map = s.generateMap({
file: path.basename(resourcePath),
source: resourcePath,
includeContent: true
});Import Code Generation
For each script or template block the loader creates an import (ES module) or require (CommonJS) statement. For each style block it generates an import and records the variable name when CSS Modules are used.
code += `import script from '${resource}';
`;
code += `import template from '${resource}';
`;
code += `import style${i} from '${resource}';
`;
injectStyles.push(`style${i}`);Template‑Script Combination
if (template) {
if (typeof template === 'string') {
dfns[i].template = template;
} else if (Array.isArray(template)) {
dfns[i].aPack = template;
} else {
dfns[i].aNode = template;
}
}Options
Two main options can be configured in webpack.config.js for san-loader:
compileTemplate – controls compilation of string‑based templates. Accepted values are 'none' (default), 'aPack' and 'aNode'. 'aPack' produces a compressed one‑dimensional array representation of the aNode tree, reducing bundle size.
esModule – when true, the loader emits ES‑module syntax; otherwise it uses CommonJS.
{
test: /\.san$/,
use: [{
loader: require.resolve('san-hot-loader'),
options: {
compileTemplate: 'aPack',
esModule: true
}
}]
}CSS Modules Support
If a <style module> block is present, the loader treats it as a CSS Module. The generated import relies on css-loader configured with modules enabled.
{
loader: 'css-loader',
options: {
modules: { localIdentName: '[local]_[hash:base64:5]' },
localsConvention: 'camelCase',
sourceMap: true
}
} if (isCSSModule) {
code += `var style${i} = require('${resource}');
`;
injectStyles.push(`style${i}`);
}Reference Repositories
San: https://github.com/baidu/san san-loader: https://github.com/ecomfe/san-loader
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
