@knighted/jsx 1.7.9 → 1.8.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 CHANGED
@@ -68,6 +68,32 @@ const button = jsx`
68
68
  document.body.append(button)
69
69
  ```
70
70
 
71
+ ### Source transpilation (`transpileJsxSource`)
72
+
73
+ Need to transform raw JSX source text (e.g. code typed in an editor) without Babel? Use `transpileJsxSource`:
74
+
75
+ ```ts
76
+ import { transpileJsxSource } from '@knighted/jsx/transpile'
77
+
78
+ const input = `
79
+ const App = () => {
80
+ return <button>click me</button>
81
+ }
82
+ `
83
+
84
+ const { code } = transpileJsxSource(input)
85
+ // -> const App = () => { return React.createElement("button", null, "click me") }
86
+ ```
87
+
88
+ By default this emits `React.createElement(...)` and `React.Fragment`. Override them when needed:
89
+
90
+ ```ts
91
+ transpileJsxSource(input, {
92
+ createElement: '__jsx',
93
+ fragment: '__fragment',
94
+ })
95
+ ```
96
+
71
97
  ### React runtime (`reactJsx`)
72
98
 
73
99
  Need to compose React elements instead of DOM nodes? Import the dedicated helper from the `@knighted/jsx/react` subpath (React 18+ and `react-dom` are still required to mount the tree):
@@ -0,0 +1,255 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.transpileJsxSource = transpileJsxSource;
7
+ const magic_string_1 = __importDefault(require("magic-string"));
8
+ const oxc_parser_1 = require("oxc-parser");
9
+ const normalize_text_js_1 = require("./shared/normalize-text.cjs");
10
+ const createModuleParserOptions = (sourceType) => ({
11
+ lang: 'tsx',
12
+ sourceType,
13
+ range: true,
14
+ preserveParens: true,
15
+ });
16
+ const formatParserError = (error) => {
17
+ let message = `[jsx] ${error.message}`;
18
+ if (error.labels?.length) {
19
+ const label = error.labels[0];
20
+ if (label.message) {
21
+ message += `\n${label.message}`;
22
+ }
23
+ }
24
+ if (error.codeframe) {
25
+ message += `\n${error.codeframe}`;
26
+ }
27
+ if (error.helpMessage) {
28
+ message += `\n${error.helpMessage}`;
29
+ }
30
+ return message;
31
+ };
32
+ const isObjectRecord = (value) => typeof value === 'object' && value !== null;
33
+ const isSourceRange = (value) => Array.isArray(value) &&
34
+ value.length === 2 &&
35
+ typeof value[0] === 'number' &&
36
+ typeof value[1] === 'number';
37
+ const hasSourceRange = (value) => isObjectRecord(value) && isSourceRange(value.range);
38
+ const compareByRangeStartDesc = (first, second) => second.range[0] - first.range[0];
39
+ class SourceJsxReactBuilder {
40
+ source;
41
+ createElementRef;
42
+ fragmentRef;
43
+ constructor(source, createElementRef, fragmentRef) {
44
+ this.source = source;
45
+ this.createElementRef = createElementRef;
46
+ this.fragmentRef = fragmentRef;
47
+ }
48
+ compile(node) {
49
+ return this.compileNode(node);
50
+ }
51
+ compileNode(node) {
52
+ if (node.type === 'JSXFragment') {
53
+ const children = this.compileChildren(node.children);
54
+ return this.buildCreateElement(this.fragmentRef, 'null', children);
55
+ }
56
+ const opening = node.openingElement;
57
+ const tagExpr = this.compileTagName(opening.name);
58
+ const propsExpr = this.compileProps(opening.attributes);
59
+ const children = this.compileChildren(node.children);
60
+ return this.buildCreateElement(tagExpr, propsExpr, children);
61
+ }
62
+ compileChildren(children) {
63
+ const compiled = [];
64
+ children.forEach(child => {
65
+ switch (child.type) {
66
+ case 'JSXText': {
67
+ const normalized = (0, normalize_text_js_1.normalizeJsxText)(child.value);
68
+ if (normalized) {
69
+ compiled.push(JSON.stringify(normalized));
70
+ }
71
+ break;
72
+ }
73
+ case 'JSXExpressionContainer': {
74
+ if (child.expression.type === 'JSXEmptyExpression') {
75
+ break;
76
+ }
77
+ compiled.push(this.compileExpression(child.expression));
78
+ break;
79
+ }
80
+ case 'JSXSpreadChild': {
81
+ compiled.push(this.compileExpression(child.expression));
82
+ break;
83
+ }
84
+ case 'JSXElement':
85
+ case 'JSXFragment': {
86
+ compiled.push(this.compileNode(child));
87
+ break;
88
+ }
89
+ }
90
+ });
91
+ return compiled;
92
+ }
93
+ compileProps(attributes) {
94
+ const segments = [];
95
+ let staticEntries = [];
96
+ const flushStatics = () => {
97
+ if (!staticEntries.length) {
98
+ return;
99
+ }
100
+ segments.push(`{ ${staticEntries.join(', ')} }`);
101
+ staticEntries = [];
102
+ };
103
+ attributes.forEach(attribute => {
104
+ if (attribute.type === 'JSXSpreadAttribute') {
105
+ flushStatics();
106
+ const spreadValue = this.compileExpression(attribute.argument);
107
+ segments.push(`(${spreadValue} ?? {})`);
108
+ return;
109
+ }
110
+ const name = this.compileAttributeName(attribute.name);
111
+ let value;
112
+ if (!attribute.value) {
113
+ value = 'true';
114
+ }
115
+ else if (attribute.value.type === 'Literal') {
116
+ value = JSON.stringify(attribute.value.value);
117
+ }
118
+ else if (attribute.value.type === 'JSXExpressionContainer') {
119
+ if (attribute.value.expression.type === 'JSXEmptyExpression') {
120
+ return;
121
+ }
122
+ value = this.compileExpression(attribute.value.expression);
123
+ }
124
+ else {
125
+ value = 'undefined';
126
+ }
127
+ staticEntries.push(`${JSON.stringify(name)}: ${value}`);
128
+ });
129
+ flushStatics();
130
+ if (!segments.length) {
131
+ return 'null';
132
+ }
133
+ if (segments.length === 1) {
134
+ return segments[0] ?? 'null';
135
+ }
136
+ return `Object.assign({}, ${segments.join(', ')})`;
137
+ }
138
+ compileAttributeName(name) {
139
+ switch (name.type) {
140
+ case 'JSXIdentifier':
141
+ return name.name;
142
+ case 'JSXNamespacedName':
143
+ return `${name.namespace.name}:${name.name.name}`;
144
+ case 'JSXMemberExpression':
145
+ return `${this.compileAttributeName(name.object)}.${name.property.name}`;
146
+ default:
147
+ return '';
148
+ }
149
+ }
150
+ compileMemberExpressionTagName(name) {
151
+ const parts = [];
152
+ let current = name;
153
+ while (current.type === 'JSXMemberExpression') {
154
+ parts.unshift(current.property.name);
155
+ current = current.object;
156
+ }
157
+ parts.unshift(current.name);
158
+ return parts.join('.');
159
+ }
160
+ compileTagName(name) {
161
+ if (!name) {
162
+ throw new Error('[jsx] Encountered JSX element without a tag name.');
163
+ }
164
+ if (name.type === 'JSXIdentifier') {
165
+ if (/^[A-Z]/.test(name.name)) {
166
+ return name.name;
167
+ }
168
+ return JSON.stringify(name.name);
169
+ }
170
+ if (name.type === 'JSXMemberExpression') {
171
+ return this.compileMemberExpressionTagName(name);
172
+ }
173
+ if (name.type === 'JSXNamespacedName') {
174
+ return JSON.stringify(`${name.namespace.name}:${name.name.name}`);
175
+ }
176
+ throw new Error('[jsx] Unsupported JSX tag expression.');
177
+ }
178
+ compileExpression(node) {
179
+ if (node.type === 'JSXElement' || node.type === 'JSXFragment') {
180
+ return this.compileNode(node);
181
+ }
182
+ if (!hasSourceRange(node)) {
183
+ throw new Error('[jsx] Unable to read source range for expression node.');
184
+ }
185
+ const range = node.range;
186
+ const nestedJsxRoots = collectRootJsxNodes(node);
187
+ if (!nestedJsxRoots.length) {
188
+ return this.source.slice(range[0], range[1]);
189
+ }
190
+ const expressionSource = this.source.slice(range[0], range[1]);
191
+ const magic = new magic_string_1.default(expressionSource);
192
+ nestedJsxRoots.sort(compareByRangeStartDesc).forEach(jsxNode => {
193
+ magic.overwrite(jsxNode.range[0] - range[0], jsxNode.range[1] - range[0], this.compileNode(jsxNode));
194
+ });
195
+ return magic.toString();
196
+ }
197
+ buildCreateElement(type, props, children) {
198
+ const args = [type, props];
199
+ if (children.length) {
200
+ args.push(children.join(', '));
201
+ }
202
+ return `${this.createElementRef}(${args.join(', ')})`;
203
+ }
204
+ }
205
+ const collectRootJsxNodes = (root) => {
206
+ const nodes = [];
207
+ const isJsxElementOrFragment = (node) => {
208
+ if (!isObjectRecord(node)) {
209
+ return false;
210
+ }
211
+ return node.type === 'JSXElement' || node.type === 'JSXFragment';
212
+ };
213
+ const walk = (value, insideJsx) => {
214
+ if (!isObjectRecord(value)) {
215
+ return;
216
+ }
217
+ if (Array.isArray(value)) {
218
+ value.forEach(entry => walk(entry, insideJsx));
219
+ return;
220
+ }
221
+ const node = value;
222
+ const isJsxNode = isJsxElementOrFragment(node);
223
+ if (isJsxNode && hasSourceRange(node) && !insideJsx) {
224
+ nodes.push(node);
225
+ }
226
+ for (const entry of Object.values(node)) {
227
+ walk(entry, insideJsx || isJsxNode);
228
+ }
229
+ };
230
+ walk(root, false);
231
+ return nodes;
232
+ };
233
+ function transpileJsxSource(source, options = {}) {
234
+ const sourceType = options.sourceType ?? 'module';
235
+ const createElementRef = options.createElement ?? 'React.createElement';
236
+ const fragmentRef = options.fragment ?? 'React.Fragment';
237
+ const parsed = (0, oxc_parser_1.parseSync)('transpile-jsx-source.tsx', source, createModuleParserOptions(sourceType));
238
+ const firstError = parsed.errors[0];
239
+ if (firstError) {
240
+ throw new Error(formatParserError(firstError));
241
+ }
242
+ const jsxRoots = collectRootJsxNodes(parsed.program);
243
+ if (!jsxRoots.length) {
244
+ return { code: source, changed: false };
245
+ }
246
+ const builder = new SourceJsxReactBuilder(source, createElementRef, fragmentRef);
247
+ const magic = new magic_string_1.default(source);
248
+ jsxRoots.sort(compareByRangeStartDesc).forEach(node => {
249
+ magic.overwrite(node.range[0], node.range[1], builder.compile(node));
250
+ });
251
+ return {
252
+ code: magic.toString(),
253
+ changed: true,
254
+ };
255
+ }
@@ -0,0 +1,12 @@
1
+ type TranspileSourceType = 'module' | 'script';
2
+ export type TranspileJsxSourceOptions = {
3
+ sourceType?: TranspileSourceType;
4
+ createElement?: string;
5
+ fragment?: string;
6
+ };
7
+ export type TranspileJsxSourceResult = {
8
+ code: string;
9
+ changed: boolean;
10
+ };
11
+ export declare function transpileJsxSource(source: string, options?: TranspileJsxSourceOptions): TranspileJsxSourceResult;
12
+ export {};
@@ -0,0 +1,12 @@
1
+ type TranspileSourceType = 'module' | 'script';
2
+ export type TranspileJsxSourceOptions = {
3
+ sourceType?: TranspileSourceType;
4
+ createElement?: string;
5
+ fragment?: string;
6
+ };
7
+ export type TranspileJsxSourceResult = {
8
+ code: string;
9
+ changed: boolean;
10
+ };
11
+ export declare function transpileJsxSource(source: string, options?: TranspileJsxSourceOptions): TranspileJsxSourceResult;
12
+ export {};
@@ -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.8.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"