@knighted/jsx 1.7.9 → 1.9.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.
@@ -0,0 +1,249 @@
1
+ import MagicString from 'magic-string';
2
+ import { parseSync } from 'oxc-parser';
3
+ import { normalizeJsxText } from './shared/normalize-text.js';
4
+ const createModuleParserOptions = (sourceType) => ({
5
+ lang: 'tsx',
6
+ sourceType,
7
+ range: true,
8
+ preserveParens: true,
9
+ });
10
+ const formatParserError = (error) => {
11
+ let message = `[jsx] ${error.message}`;
12
+ if (error.labels?.length) {
13
+ const label = error.labels[0];
14
+ if (label.message) {
15
+ message += `\n${label.message}`;
16
+ }
17
+ }
18
+ if (error.codeframe) {
19
+ message += `\n${error.codeframe}`;
20
+ }
21
+ if (error.helpMessage) {
22
+ message += `\n${error.helpMessage}`;
23
+ }
24
+ return message;
25
+ };
26
+ const isObjectRecord = (value) => typeof value === 'object' && value !== null;
27
+ const isSourceRange = (value) => Array.isArray(value) &&
28
+ value.length === 2 &&
29
+ typeof value[0] === 'number' &&
30
+ typeof value[1] === 'number';
31
+ const hasSourceRange = (value) => isObjectRecord(value) && isSourceRange(value.range);
32
+ const compareByRangeStartDesc = (first, second) => second.range[0] - first.range[0];
33
+ class SourceJsxReactBuilder {
34
+ source;
35
+ createElementRef;
36
+ fragmentRef;
37
+ constructor(source, createElementRef, fragmentRef) {
38
+ this.source = source;
39
+ this.createElementRef = createElementRef;
40
+ this.fragmentRef = fragmentRef;
41
+ }
42
+ compile(node) {
43
+ return this.compileNode(node);
44
+ }
45
+ compileNode(node) {
46
+ if (node.type === 'JSXFragment') {
47
+ const children = this.compileChildren(node.children);
48
+ return this.buildCreateElement(this.fragmentRef, 'null', children);
49
+ }
50
+ const opening = node.openingElement;
51
+ const tagExpr = this.compileTagName(opening.name);
52
+ const propsExpr = this.compileProps(opening.attributes);
53
+ const children = this.compileChildren(node.children);
54
+ return this.buildCreateElement(tagExpr, propsExpr, children);
55
+ }
56
+ compileChildren(children) {
57
+ const compiled = [];
58
+ children.forEach(child => {
59
+ switch (child.type) {
60
+ case 'JSXText': {
61
+ const normalized = normalizeJsxText(child.value);
62
+ if (normalized) {
63
+ compiled.push(JSON.stringify(normalized));
64
+ }
65
+ break;
66
+ }
67
+ case 'JSXExpressionContainer': {
68
+ if (child.expression.type === 'JSXEmptyExpression') {
69
+ break;
70
+ }
71
+ compiled.push(this.compileExpression(child.expression));
72
+ break;
73
+ }
74
+ case 'JSXSpreadChild': {
75
+ compiled.push(this.compileExpression(child.expression));
76
+ break;
77
+ }
78
+ case 'JSXElement':
79
+ case 'JSXFragment': {
80
+ compiled.push(this.compileNode(child));
81
+ break;
82
+ }
83
+ }
84
+ });
85
+ return compiled;
86
+ }
87
+ compileProps(attributes) {
88
+ const segments = [];
89
+ let staticEntries = [];
90
+ const flushStatics = () => {
91
+ if (!staticEntries.length) {
92
+ return;
93
+ }
94
+ segments.push(`{ ${staticEntries.join(', ')} }`);
95
+ staticEntries = [];
96
+ };
97
+ attributes.forEach(attribute => {
98
+ if (attribute.type === 'JSXSpreadAttribute') {
99
+ flushStatics();
100
+ const spreadValue = this.compileExpression(attribute.argument);
101
+ segments.push(`(${spreadValue} ?? {})`);
102
+ return;
103
+ }
104
+ const name = this.compileAttributeName(attribute.name);
105
+ let value;
106
+ if (!attribute.value) {
107
+ value = 'true';
108
+ }
109
+ else if (attribute.value.type === 'Literal') {
110
+ value = JSON.stringify(attribute.value.value);
111
+ }
112
+ else if (attribute.value.type === 'JSXExpressionContainer') {
113
+ if (attribute.value.expression.type === 'JSXEmptyExpression') {
114
+ return;
115
+ }
116
+ value = this.compileExpression(attribute.value.expression);
117
+ }
118
+ else {
119
+ value = 'undefined';
120
+ }
121
+ staticEntries.push(`${JSON.stringify(name)}: ${value}`);
122
+ });
123
+ flushStatics();
124
+ if (!segments.length) {
125
+ return 'null';
126
+ }
127
+ if (segments.length === 1) {
128
+ return segments[0] ?? 'null';
129
+ }
130
+ return `Object.assign({}, ${segments.join(', ')})`;
131
+ }
132
+ compileAttributeName(name) {
133
+ switch (name.type) {
134
+ case 'JSXIdentifier':
135
+ return name.name;
136
+ case 'JSXNamespacedName':
137
+ return `${name.namespace.name}:${name.name.name}`;
138
+ case 'JSXMemberExpression':
139
+ return `${this.compileAttributeName(name.object)}.${name.property.name}`;
140
+ default:
141
+ return '';
142
+ }
143
+ }
144
+ compileMemberExpressionTagName(name) {
145
+ const parts = [];
146
+ let current = name;
147
+ while (current.type === 'JSXMemberExpression') {
148
+ parts.unshift(current.property.name);
149
+ current = current.object;
150
+ }
151
+ parts.unshift(current.name);
152
+ return parts.join('.');
153
+ }
154
+ compileTagName(name) {
155
+ if (!name) {
156
+ throw new Error('[jsx] Encountered JSX element without a tag name.');
157
+ }
158
+ if (name.type === 'JSXIdentifier') {
159
+ if (/^[A-Z]/.test(name.name)) {
160
+ return name.name;
161
+ }
162
+ return JSON.stringify(name.name);
163
+ }
164
+ if (name.type === 'JSXMemberExpression') {
165
+ return this.compileMemberExpressionTagName(name);
166
+ }
167
+ if (name.type === 'JSXNamespacedName') {
168
+ return JSON.stringify(`${name.namespace.name}:${name.name.name}`);
169
+ }
170
+ throw new Error('[jsx] Unsupported JSX tag expression.');
171
+ }
172
+ compileExpression(node) {
173
+ if (node.type === 'JSXElement' || node.type === 'JSXFragment') {
174
+ return this.compileNode(node);
175
+ }
176
+ if (!hasSourceRange(node)) {
177
+ throw new Error('[jsx] Unable to read source range for expression node.');
178
+ }
179
+ const range = node.range;
180
+ const nestedJsxRoots = collectRootJsxNodes(node);
181
+ if (!nestedJsxRoots.length) {
182
+ return this.source.slice(range[0], range[1]);
183
+ }
184
+ const expressionSource = this.source.slice(range[0], range[1]);
185
+ const magic = new MagicString(expressionSource);
186
+ nestedJsxRoots.sort(compareByRangeStartDesc).forEach(jsxNode => {
187
+ magic.overwrite(jsxNode.range[0] - range[0], jsxNode.range[1] - range[0], this.compileNode(jsxNode));
188
+ });
189
+ return magic.toString();
190
+ }
191
+ buildCreateElement(type, props, children) {
192
+ const args = [type, props];
193
+ if (children.length) {
194
+ args.push(children.join(', '));
195
+ }
196
+ return `${this.createElementRef}(${args.join(', ')})`;
197
+ }
198
+ }
199
+ const collectRootJsxNodes = (root) => {
200
+ const nodes = [];
201
+ const isJsxElementOrFragment = (node) => {
202
+ if (!isObjectRecord(node)) {
203
+ return false;
204
+ }
205
+ return node.type === 'JSXElement' || node.type === 'JSXFragment';
206
+ };
207
+ const walk = (value, insideJsx) => {
208
+ if (!isObjectRecord(value)) {
209
+ return;
210
+ }
211
+ if (Array.isArray(value)) {
212
+ value.forEach(entry => walk(entry, insideJsx));
213
+ return;
214
+ }
215
+ const node = value;
216
+ const isJsxNode = isJsxElementOrFragment(node);
217
+ if (isJsxNode && hasSourceRange(node) && !insideJsx) {
218
+ nodes.push(node);
219
+ }
220
+ for (const entry of Object.values(node)) {
221
+ walk(entry, insideJsx || isJsxNode);
222
+ }
223
+ };
224
+ walk(root, false);
225
+ return nodes;
226
+ };
227
+ export function transpileJsxSource(source, options = {}) {
228
+ const sourceType = options.sourceType ?? 'module';
229
+ const createElementRef = options.createElement ?? 'React.createElement';
230
+ const fragmentRef = options.fragment ?? 'React.Fragment';
231
+ const parsed = parseSync('transpile-jsx-source.tsx', source, createModuleParserOptions(sourceType));
232
+ const firstError = parsed.errors[0];
233
+ if (firstError) {
234
+ throw new Error(formatParserError(firstError));
235
+ }
236
+ const jsxRoots = collectRootJsxNodes(parsed.program);
237
+ if (!jsxRoots.length) {
238
+ return { code: source, changed: false };
239
+ }
240
+ const builder = new SourceJsxReactBuilder(source, createElementRef, fragmentRef);
241
+ const magic = new MagicString(source);
242
+ jsxRoots.sort(compareByRangeStartDesc).forEach(node => {
243
+ magic.overwrite(node.range[0], node.range[1], builder.compile(node));
244
+ });
245
+ return {
246
+ code: magic.toString(),
247
+ changed: true,
248
+ };
249
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knighted/jsx",
3
- "version": "1.7.9",
3
+ "version": "1.9.0",
4
4
  "description": "Runtime JSX tagged template that renders DOM or React trees anywhere with or without a build step.",
5
5
  "keywords": [
6
6
  "jsx runtime",
@@ -101,6 +101,11 @@
101
101
  "import": "./dist/lite/node/react/index.js",
102
102
  "default": "./dist/lite/node/react/index.js"
103
103
  },
104
+ "./transpile": {
105
+ "types": "./dist/transpile.d.ts",
106
+ "import": "./dist/transpile.js",
107
+ "default": "./dist/transpile.js"
108
+ },
104
109
  "./loader": {
105
110
  "import": "./dist/loader/jsx.js",
106
111
  "default": "./dist/loader/jsx.js"
@@ -131,6 +136,7 @@
131
136
  "test:watch": "cross-env KNIGHTED_JSX_CLI_TEST=1 vitest",
132
137
  "test:e2e": "npm run build && npm run setup:wasm && npm run build:fixture && playwright test",
133
138
  "build:fixture": "node scripts/build-rspack-fixture.mjs",
139
+ "demo:e2e-fixture": "npm run build && npx serve . -l tcp://127.0.0.1:4173",
134
140
  "demo:node-ssr": "node test/fixtures/node-ssr/render.mjs",
135
141
  "dev": "vite dev --config vite.config.ts",
136
142
  "build:demo": "vite build --config vite.config.ts",