@knighted/jsx 1.0.0 → 1.2.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -4
- package/dist/cjs/jsx.cjs +18 -172
- package/dist/cjs/loader/jsx.cjs +363 -0
- package/dist/cjs/loader/jsx.d.cts +19 -0
- package/dist/cjs/react/index.cjs +5 -0
- package/dist/cjs/react/index.d.cts +2 -0
- package/dist/cjs/react/react-jsx.cjs +142 -0
- package/dist/cjs/react/react-jsx.d.cts +5 -0
- package/dist/cjs/runtime/shared.cjs +169 -0
- package/dist/cjs/runtime/shared.d.cts +38 -0
- package/dist/jsx.js +11 -165
- package/dist/lite/index.js +3 -3
- package/dist/loader/jsx.d.ts +19 -0
- package/dist/loader/jsx.js +357 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.js +1 -0
- package/dist/react/react-jsx.d.ts +5 -0
- package/dist/react/react-jsx.js +138 -0
- package/dist/runtime/shared.d.ts +38 -0
- package/dist/runtime/shared.js +156 -0
- package/package.json +30 -2
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
const OPEN_TAG_RE = /<\s*$/;
|
|
2
|
+
const CLOSE_TAG_RE = /<\/\s*$/;
|
|
3
|
+
const PLACEHOLDER_PREFIX = '__KX_EXPR__';
|
|
4
|
+
let invocationCounter = 0;
|
|
5
|
+
export const parserOptions = {
|
|
6
|
+
lang: 'jsx',
|
|
7
|
+
sourceType: 'module',
|
|
8
|
+
range: true,
|
|
9
|
+
preserveParens: true,
|
|
10
|
+
};
|
|
11
|
+
export const formatParserError = (error) => {
|
|
12
|
+
let message = `[oxc-parser] ${error.message}`;
|
|
13
|
+
if (error.labels?.length) {
|
|
14
|
+
const label = error.labels[0];
|
|
15
|
+
if (label.message) {
|
|
16
|
+
message += `\n${label.message}`;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (error.codeframe) {
|
|
20
|
+
message += `\n${error.codeframe}`;
|
|
21
|
+
}
|
|
22
|
+
return message;
|
|
23
|
+
};
|
|
24
|
+
export const extractRootNode = (program) => {
|
|
25
|
+
for (const statement of program.body) {
|
|
26
|
+
if (statement.type === 'ExpressionStatement') {
|
|
27
|
+
const expression = statement.expression;
|
|
28
|
+
if (expression.type === 'JSXElement' || expression.type === 'JSXFragment') {
|
|
29
|
+
return expression;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
throw new Error('The jsx template must contain a single JSX element or fragment.');
|
|
34
|
+
};
|
|
35
|
+
export const getIdentifierName = (identifier) => {
|
|
36
|
+
switch (identifier.type) {
|
|
37
|
+
case 'JSXIdentifier':
|
|
38
|
+
return identifier.name;
|
|
39
|
+
case 'JSXNamespacedName':
|
|
40
|
+
return `${identifier.namespace.name}:${identifier.name.name}`;
|
|
41
|
+
case 'JSXMemberExpression':
|
|
42
|
+
return `${getIdentifierName(identifier.object)}.${identifier.property.name}`;
|
|
43
|
+
default:
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
export const walkAst = (node, visitor) => {
|
|
48
|
+
if (!node || typeof node !== 'object') {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const candidate = node;
|
|
52
|
+
if (typeof candidate.type !== 'string') {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
visitor(candidate);
|
|
56
|
+
Object.values(candidate).forEach(value => {
|
|
57
|
+
if (!value) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(value)) {
|
|
61
|
+
value.forEach(child => walkAst(child, visitor));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (typeof value === 'object') {
|
|
65
|
+
walkAst(value, visitor);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
export const normalizeJsxText = (value) => {
|
|
70
|
+
const collapsed = value.replace(/\r/g, '').replace(/\n\s+/g, ' ');
|
|
71
|
+
const trimmed = collapsed.trim();
|
|
72
|
+
return trimmed.length > 0 ? trimmed : '';
|
|
73
|
+
};
|
|
74
|
+
export const collectPlaceholderNames = (expression, ctx) => {
|
|
75
|
+
const placeholders = new Set();
|
|
76
|
+
walkAst(expression, node => {
|
|
77
|
+
if (node.type === 'Identifier' && ctx.placeholders.has(node.name)) {
|
|
78
|
+
placeholders.add(node.name);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
return Array.from(placeholders);
|
|
82
|
+
};
|
|
83
|
+
export const evaluateExpression = (expression, ctx, evaluateJsxNode) => {
|
|
84
|
+
if (expression.type === 'JSXElement' || expression.type === 'JSXFragment') {
|
|
85
|
+
return evaluateJsxNode(expression);
|
|
86
|
+
}
|
|
87
|
+
if (!('range' in expression) || !expression.range) {
|
|
88
|
+
throw new Error('Unable to evaluate expression: missing source range information.');
|
|
89
|
+
}
|
|
90
|
+
const [start, end] = expression.range;
|
|
91
|
+
const source = ctx.source.slice(start, end);
|
|
92
|
+
const placeholders = collectPlaceholderNames(expression, ctx);
|
|
93
|
+
try {
|
|
94
|
+
const evaluator = new Function(...placeholders, `"use strict"; return (${source});`);
|
|
95
|
+
const args = placeholders.map(name => ctx.placeholders.get(name));
|
|
96
|
+
return evaluator(...args);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
throw new Error(`Failed to evaluate expression ${source}: ${error.message}`);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
export const sanitizeIdentifier = (value) => {
|
|
103
|
+
const cleaned = value.replace(/[^a-zA-Z0-9_$]/g, '');
|
|
104
|
+
if (!cleaned) {
|
|
105
|
+
return 'Component';
|
|
106
|
+
}
|
|
107
|
+
if (!/[A-Za-z_$]/.test(cleaned[0])) {
|
|
108
|
+
return `Component${cleaned}`;
|
|
109
|
+
}
|
|
110
|
+
return cleaned;
|
|
111
|
+
};
|
|
112
|
+
export const ensureBinding = (value, bindings, bindingLookup) => {
|
|
113
|
+
const existing = bindingLookup.get(value);
|
|
114
|
+
if (existing) {
|
|
115
|
+
return existing;
|
|
116
|
+
}
|
|
117
|
+
const descriptor = value.displayName || value.name || `Component${bindings.length}`;
|
|
118
|
+
const baseName = sanitizeIdentifier(descriptor ?? '');
|
|
119
|
+
let candidate = baseName;
|
|
120
|
+
let suffix = 1;
|
|
121
|
+
while (bindings.some(binding => binding.name === candidate)) {
|
|
122
|
+
candidate = `${baseName}${suffix++}`;
|
|
123
|
+
}
|
|
124
|
+
const binding = { name: candidate, value };
|
|
125
|
+
bindings.push(binding);
|
|
126
|
+
bindingLookup.set(value, binding);
|
|
127
|
+
return binding;
|
|
128
|
+
};
|
|
129
|
+
export const buildTemplate = (strings, values) => {
|
|
130
|
+
const raw = strings.raw ?? strings;
|
|
131
|
+
const placeholders = new Map();
|
|
132
|
+
const bindings = [];
|
|
133
|
+
const bindingLookup = new Map();
|
|
134
|
+
let source = raw[0] ?? '';
|
|
135
|
+
const templateId = invocationCounter++;
|
|
136
|
+
let placeholderIndex = 0;
|
|
137
|
+
for (let idx = 0; idx < values.length; idx++) {
|
|
138
|
+
const chunk = raw[idx] ?? '';
|
|
139
|
+
const nextChunk = raw[idx + 1] ?? '';
|
|
140
|
+
const value = values[idx];
|
|
141
|
+
const isTagNamePosition = OPEN_TAG_RE.test(chunk) || CLOSE_TAG_RE.test(chunk);
|
|
142
|
+
if (isTagNamePosition && typeof value === 'function') {
|
|
143
|
+
const binding = ensureBinding(value, bindings, bindingLookup);
|
|
144
|
+
source += binding.name + nextChunk;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (isTagNamePosition && typeof value === 'string') {
|
|
148
|
+
source += value + nextChunk;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const placeholder = `${PLACEHOLDER_PREFIX}${templateId}_${placeholderIndex++}__`;
|
|
152
|
+
placeholders.set(placeholder, value);
|
|
153
|
+
source += placeholder + nextChunk;
|
|
154
|
+
}
|
|
155
|
+
return { source, placeholders, bindings };
|
|
156
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knighted/jsx",
|
|
3
|
-
"version": "1.0.0",
|
|
3
|
+
"version": "1.2.0-rc.0",
|
|
4
4
|
"description": "A simple JSX transpiler that runs in node.js or the browser.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jsx browser transform",
|
|
@@ -25,6 +25,15 @@
|
|
|
25
25
|
"import": "./dist/lite/index.js",
|
|
26
26
|
"default": "./dist/lite/index.js"
|
|
27
27
|
},
|
|
28
|
+
"./react": {
|
|
29
|
+
"types": "./dist/react/index.d.ts",
|
|
30
|
+
"import": "./dist/react/index.js",
|
|
31
|
+
"default": "./dist/react/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./loader": {
|
|
34
|
+
"import": "./dist/loader/jsx.js",
|
|
35
|
+
"default": "./dist/loader/jsx.js"
|
|
36
|
+
},
|
|
28
37
|
"./package.json": "./package.json"
|
|
29
38
|
},
|
|
30
39
|
"engines": {
|
|
@@ -41,29 +50,48 @@
|
|
|
41
50
|
"prettier": "prettier -w .",
|
|
42
51
|
"test": "vitest run --coverage",
|
|
43
52
|
"test:watch": "vitest",
|
|
53
|
+
"build:fixture": "node scripts/build-rspack-fixture.mjs",
|
|
44
54
|
"dev": "vite dev --config vite.config.ts",
|
|
45
55
|
"build:demo": "vite build --config vite.config.ts",
|
|
46
56
|
"preview": "vite preview --config vite.config.ts",
|
|
47
57
|
"build:lite": "tsup --config tsup.config.ts",
|
|
58
|
+
"setup:wasm": "node scripts/setup-wasm.mjs",
|
|
48
59
|
"prepack": "npm run build"
|
|
49
60
|
},
|
|
50
61
|
"devDependencies": {
|
|
51
62
|
"@eslint/js": "^9.39.1",
|
|
52
63
|
"@knighted/duel": "^2.1.6",
|
|
64
|
+
"@rspack/core": "^1.0.5",
|
|
53
65
|
"@types/jsdom": "^27.0.0",
|
|
66
|
+
"@types/react": "^19.2.7",
|
|
67
|
+
"@types/react-dom": "^19.2.3",
|
|
54
68
|
"@vitest/coverage-v8": "^4.0.14",
|
|
55
69
|
"eslint": "^9.39.1",
|
|
70
|
+
"http-server": "^14.1.1",
|
|
56
71
|
"jsdom": "^27.2.0",
|
|
72
|
+
"lit": "^3.2.1",
|
|
57
73
|
"prettier": "^3.7.3",
|
|
58
|
-
"
|
|
74
|
+
"react": "^19.0.0",
|
|
75
|
+
"react-dom": "^19.0.0",
|
|
76
|
+
"tar": "^7.4.3",
|
|
77
|
+
"tsup": "^8.5.1",
|
|
59
78
|
"typescript": "^5.9.3",
|
|
60
79
|
"typescript-eslint": "^8.48.0",
|
|
61
80
|
"vite": "^7.2.4",
|
|
62
81
|
"vitest": "^4.0.14"
|
|
63
82
|
},
|
|
64
83
|
"dependencies": {
|
|
84
|
+
"magic-string": "^0.30.21",
|
|
65
85
|
"oxc-parser": "^0.99.0"
|
|
66
86
|
},
|
|
87
|
+
"peerDependencies": {
|
|
88
|
+
"react": ">=18"
|
|
89
|
+
},
|
|
90
|
+
"peerDependenciesMeta": {
|
|
91
|
+
"react": {
|
|
92
|
+
"optional": true
|
|
93
|
+
}
|
|
94
|
+
},
|
|
67
95
|
"optionalDependencies": {
|
|
68
96
|
"@oxc-parser/binding-darwin-arm64": "^0.99.0",
|
|
69
97
|
"@oxc-parser/binding-linux-x64-gnu": "^0.99.0",
|