Skip to content

Commit 5e86f61

Browse files
add: rspack, with babel Programming API (without babel-loader)
1 parent c08ac44 commit 5e86f61

File tree

4 files changed

+3518
-111
lines changed

4 files changed

+3518
-111
lines changed

html-tag-jsx-loader.js

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
/**
2+
* Custom loader that transforms JSX to html-tag-js tag() calls
3+
* This uses Babel's parser/transformer but is lighter than full babel-loader
4+
*/
5+
const { parse } = require('@babel/parser');
6+
const traverse = require('@babel/traverse').default;
7+
const generate = require('@babel/generator').default;
8+
const t = require('@babel/types');
9+
10+
module.exports = function htmlTagJsxLoader(source) {
11+
const callback = this.async();
12+
13+
// Enable caching for this loader
14+
this.cacheable && this.cacheable();
15+
16+
try {
17+
// Debug logging - verify loader is running
18+
console.log('🔧 Custom JSX loader processing:', this.resourcePath);
19+
20+
// Determine file type from extension
21+
const isTypeScript = /\.tsx?$/.test(this.resourcePath);
22+
const isJavaScript = /\.(jsx?|mjs)$/.test(this.resourcePath);
23+
24+
// Quick check: if no JSX syntax at all, pass through unchanged
25+
// Look for JSX opening tags: < followed by a letter or uppercase
26+
const hasJSXLike = /<[A-Za-z]/.test(source);
27+
if (!hasJSXLike) {
28+
return callback(null, source);
29+
}
30+
31+
// Parse with appropriate plugins
32+
const parserPlugins = ['jsx', '@babel/plugin-transform-runtime', '@babel/plugin-transform-block-scoping'];
33+
if (isTypeScript) {
34+
parserPlugins.push('typescript');
35+
}
36+
37+
const ast = parse(source, {
38+
sourceType: 'module',
39+
plugins: parserPlugins,
40+
});
41+
42+
// Track if we need to add the import
43+
let needsTagImport = false;
44+
let hasJSX = false;
45+
const hasExistingImport = source.includes('import tag from \'html-tag-js\'') ||
46+
source.includes('import tag from "html-tag-js"');
47+
48+
// Transform JSX elements
49+
traverse(ast, {
50+
JSXFragment(path) {
51+
hasJSX = true;
52+
needsTagImport = true;
53+
const { node } = path;
54+
const { children: childrenNode } = node;
55+
56+
const children = [];
57+
populateChildren(childrenNode, children, t);
58+
const arrayExpression = t.arrayExpression(children);
59+
path.replaceWith(arrayExpression);
60+
},
61+
62+
JSXElement(path) {
63+
hasJSX = true;
64+
needsTagImport = true;
65+
const { node } = path;
66+
const {
67+
openingElement: el,
68+
children: childrenNode,
69+
} = node;
70+
71+
let { name: tagName } = el.name;
72+
const { attributes } = el;
73+
74+
let id;
75+
let className;
76+
const on = [];
77+
const args = [];
78+
const attrs = [];
79+
const children = [];
80+
const options = [];
81+
const events = {};
82+
let isComponent = /^(?:[A-Z][a-zA-Z0-9_$]*|(?:[a-zA-Z_$][a-zA-Z0-9_$]*\.)+[a-zA-Z_$][a-zA-Z0-9_$]*)$/.test(tagName);
83+
84+
if (el.name.type === 'JSXMemberExpression') {
85+
const { object, property } = el.name;
86+
tagName = `${object.name}.${property.name}`;
87+
isComponent = true;
88+
}
89+
90+
populateChildren(childrenNode, children, t);
91+
92+
for (const attr of attributes) {
93+
if (attr.type === 'JSXSpreadAttribute') {
94+
if (isComponent) {
95+
attrs.push(t.spreadElement(attr.argument));
96+
} else {
97+
options.push(t.spreadElement(attr.argument));
98+
}
99+
continue;
100+
}
101+
102+
let { name, namespace } = attr.name;
103+
104+
if (!isComponent) {
105+
if (name === 'id') {
106+
if (attr.value && attr.value.type === 'StringLiteral') {
107+
id = attr.value;
108+
} else if (attr.value && attr.value.type === 'JSXExpressionContainer') {
109+
id = attr.value.expression;
110+
}
111+
continue;
112+
}
113+
114+
if (['class', 'className'].includes(name)) {
115+
if (attr.value && attr.value.type === 'StringLiteral') {
116+
className = attr.value;
117+
} else if (attr.value && attr.value.type === 'JSXExpressionContainer') {
118+
className = attr.value.expression;
119+
}
120+
continue;
121+
}
122+
}
123+
124+
if (namespace) {
125+
namespace = namespace.name;
126+
name = name.name;
127+
}
128+
129+
if (!attr.value) {
130+
attrs.push(t.objectProperty(
131+
t.stringLiteral(name),
132+
t.stringLiteral(''),
133+
));
134+
continue;
135+
}
136+
137+
const { type } = attr.value;
138+
const isAttr = /-/.test(name);
139+
let value;
140+
141+
if (type === 'StringLiteral') {
142+
value = attr.value;
143+
} else {
144+
value = attr.value.expression;
145+
}
146+
147+
if (namespace) {
148+
if (!['on', 'once', 'off'].includes(namespace)) {
149+
attrs.push(t.objectProperty(
150+
t.stringLiteral(namespace === 'attr' ? name : `${namespace}:${name}`),
151+
value,
152+
));
153+
continue;
154+
}
155+
156+
if (namespace === 'off') continue;
157+
158+
if (!events[name]) {
159+
events[name] = [];
160+
on.push(t.objectProperty(
161+
t.stringLiteral(name),
162+
t.arrayExpression(events[name]),
163+
));
164+
}
165+
166+
events[name].push(value);
167+
continue;
168+
}
169+
170+
if (isAttr) {
171+
const attrRegex = /^attr-(.+)/;
172+
if (attrRegex.test(name)) {
173+
[, name] = attrRegex.exec(name);
174+
}
175+
176+
attrs.push(t.objectProperty(
177+
t.stringLiteral(name),
178+
value,
179+
));
180+
continue;
181+
}
182+
183+
(isComponent ? attrs : options)
184+
.unshift(t.objectProperty(
185+
t.identifier(name),
186+
value,
187+
));
188+
}
189+
190+
if (isComponent) {
191+
args.push(t.identifier(tagName));
192+
193+
if (on.length > 0) {
194+
attrs.push(
195+
t.objectProperty(
196+
t.identifier('on'),
197+
t.objectExpression(on),
198+
)
199+
);
200+
}
201+
202+
if (attrs.length > 0) {
203+
args.push(t.objectExpression(attrs));
204+
}
205+
206+
if (children.length > 0) {
207+
args.push(t.arrayExpression(children));
208+
}
209+
} else {
210+
args.push(t.stringLiteral(tagName));
211+
212+
if (on.length > 0) {
213+
options.push(
214+
t.objectProperty(
215+
t.identifier('on'),
216+
t.objectExpression(on),
217+
)
218+
);
219+
}
220+
221+
if (attrs.length > 0) {
222+
options.push(
223+
t.objectProperty(
224+
t.identifier('attr'),
225+
t.objectExpression(attrs),
226+
)
227+
);
228+
}
229+
230+
if (id || className) {
231+
if (className) {
232+
args.push(className);
233+
} else if (id) {
234+
args.push(t.nullLiteral());
235+
}
236+
237+
if (id) {
238+
args.push(id);
239+
}
240+
}
241+
242+
if (children.length) {
243+
args.push(t.arrayExpression(children));
244+
}
245+
246+
if (options.length) {
247+
args.push(t.objectExpression(options));
248+
}
249+
}
250+
251+
const identifier = t.identifier('tag');
252+
const callExpression = t.callExpression(identifier, args);
253+
path.replaceWith(callExpression);
254+
},
255+
});
256+
257+
// If no JSX was found, return original source
258+
if (!hasJSX) {
259+
return callback(null, source);
260+
}
261+
262+
// Generate the transformed code
263+
const output = generate(ast, {
264+
sourceMaps: true,
265+
sourceFileName: this.resourcePath,
266+
retainLines: false,
267+
compact: false
268+
}, source);
269+
270+
// Add import if needed
271+
if (needsTagImport && !hasExistingImport) {
272+
output.code = `import tag from 'html-tag-js';\n${output.code}`;
273+
}
274+
275+
callback(null, output.code, output.map);
276+
} catch (error) {
277+
callback(error);
278+
}
279+
};
280+
281+
/**
282+
* Parse node to expression
283+
*/
284+
function parseNode(types, node) {
285+
const { type } = node;
286+
287+
if (type === 'JSXText') {
288+
return types.stringLiteral(node.value);
289+
}
290+
291+
if (type === 'JSXElement') {
292+
return node;
293+
}
294+
295+
const { expression } = node;
296+
const invalidExpressions = ['JSXEmptyExpression'];
297+
298+
if (invalidExpressions.includes(expression.type)) {
299+
return null;
300+
}
301+
302+
return expression;
303+
}
304+
305+
/**
306+
* Populate children
307+
*/
308+
function populateChildren(childrenNode, children, t) {
309+
for (let node of childrenNode) {
310+
node = parseNode(t, node);
311+
if (!node) continue;
312+
children.push(node);
313+
}
314+
}

0 commit comments

Comments
 (0)