@knighted/jsx 1.9.2 → 1.10.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,39 +68,52 @@ const button = jsx`
68
68
  document.body.append(button)
69
69
  ```
70
70
 
71
- ### Source transpilation (`transpileJsxSource`)
71
+ ### Source transpilation (`transpileJsxSource` + `transformJsxSource`)
72
72
 
73
- Need to transform raw JSX source text (e.g. code typed in an editor) without Babel? Use `transpileJsxSource`:
73
+ Need to transform raw JSX source text (for example, code typed in an editor) without Babel?
74
+ Use one of these subpath exports:
75
+
76
+ - `@knighted/jsx/transpile` for code-only output (`{ code, changed }`).
77
+ - `@knighted/jsx/transform` for code plus parser-backed import metadata and diagnostics
78
+ (`{ code, changed, imports, diagnostics }`).
74
79
 
75
80
  ```ts
76
81
  import { transpileJsxSource } from '@knighted/jsx/transpile'
82
+ import { transformJsxSource } from '@knighted/jsx/transform'
77
83
 
78
84
  const input = `
79
- const App = () => {
80
- return <button>click me</button>
81
- }
85
+ import React from 'react'
86
+ const App = () => <button>click me</button>
82
87
  `
83
88
 
84
- const { code } = transpileJsxSource(input)
85
- // -> const App = () => { return React.createElement("button", null, "click me") }
89
+ const transpiled = transpileJsxSource(input)
90
+ // -> { code, changed }
91
+
92
+ const transformed = transformJsxSource(input, { typescript: 'strip' })
93
+ // -> { code, changed, imports, diagnostics }
86
94
  ```
87
95
 
88
- By default this emits `React.createElement(...)` and `React.Fragment`. Override them when needed:
96
+ Both entrypoints emit `React.createElement(...)` and `React.Fragment` by default. Override
97
+ them when needed:
89
98
 
90
99
  ```ts
91
100
  transpileJsxSource(input, {
92
101
  createElement: '__jsx',
93
102
  fragment: '__fragment',
94
103
  })
104
+
105
+ transformJsxSource(input, {
106
+ createElement: '__jsx',
107
+ fragment: '__fragment',
108
+ })
95
109
  ```
96
110
 
97
- By default, TypeScript syntax is preserved in the output. If your source needs to run directly
98
- as JavaScript (for example, code entered in an editor), enable type stripping:
111
+ By default, TypeScript syntax is preserved in output. If your source needs to run directly as
112
+ JavaScript, enable type stripping:
99
113
 
100
114
  ```ts
101
- transpileJsxSource(input, {
102
- typescript: 'strip',
103
- })
115
+ transpileJsxSource(input, { typescript: 'strip' })
116
+ transformJsxSource(input, { typescript: 'strip' })
104
117
  ```
105
118
 
106
119
  Supported `typescript` modes:
@@ -109,6 +122,13 @@ Supported `typescript` modes:
109
122
  - `'strip'`: remove type-only declarations and erase inline type syntax (`: T`, `as T`,
110
123
  `satisfies T`, non-null assertions, and type assertions) while still transpiling JSX.
111
124
 
125
+ Environment note:
126
+
127
+ - Both APIs are ESM-first and work in Node.
128
+ - For direct browser usage of `@knighted/jsx/transform`, use a CDN/runtime that can resolve
129
+ `oxc-transform` for the browser build (for example, modern ESM CDNs that bundle or map
130
+ WASM/native bindings automatically).
131
+
112
132
  ### React runtime (`reactJsx`)
113
133
 
114
134
  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,208 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.transformJsxSource = transformJsxSource;
4
+ const oxc_parser_1 = require("oxc-parser");
5
+ const oxc_transform_1 = require("oxc-transform");
6
+ const transpile_js_1 = require("./transpile.cjs");
7
+ const createParserOptions = (sourceType) => ({
8
+ lang: 'tsx',
9
+ sourceType,
10
+ range: true,
11
+ preserveParens: true,
12
+ });
13
+ const isObjectRecord = (value) => typeof value === 'object' && value !== null;
14
+ const isSourceRange = (value) => Array.isArray(value) &&
15
+ value.length === 2 &&
16
+ typeof value[0] === 'number' &&
17
+ typeof value[1] === 'number';
18
+ const toSourceRange = (value) => {
19
+ if (!isObjectRecord(value) || !isSourceRange(value.range)) {
20
+ return null;
21
+ }
22
+ return value.range;
23
+ };
24
+ const asImportKind = (value) => value === 'type' ? 'type' : 'value';
25
+ const toDiagnostic = (source, diagnostic) => {
26
+ const firstLabel = diagnostic.labels?.[0];
27
+ const range = firstLabel &&
28
+ typeof firstLabel.start === 'number' &&
29
+ typeof firstLabel.end === 'number'
30
+ ? [firstLabel.start, firstLabel.end]
31
+ : null;
32
+ return {
33
+ source,
34
+ severity: diagnostic.severity,
35
+ message: diagnostic.message,
36
+ range,
37
+ codeframe: diagnostic.codeframe,
38
+ helpMessage: diagnostic.helpMessage,
39
+ };
40
+ };
41
+ const toImportBinding = (specifier, declarationImportKind) => {
42
+ if (!isObjectRecord(specifier) || typeof specifier.type !== 'string') {
43
+ return null;
44
+ }
45
+ if (specifier.type === 'ImportDefaultSpecifier') {
46
+ const localName = isObjectRecord(specifier.local) && typeof specifier.local.name === 'string'
47
+ ? specifier.local.name
48
+ : null;
49
+ if (!localName) {
50
+ return null;
51
+ }
52
+ return {
53
+ kind: 'default',
54
+ local: localName,
55
+ imported: 'default',
56
+ isTypeOnly: declarationImportKind === 'type',
57
+ range: toSourceRange(specifier),
58
+ };
59
+ }
60
+ if (specifier.type === 'ImportNamespaceSpecifier') {
61
+ const localName = isObjectRecord(specifier.local) && typeof specifier.local.name === 'string'
62
+ ? specifier.local.name
63
+ : null;
64
+ if (!localName) {
65
+ return null;
66
+ }
67
+ return {
68
+ kind: 'namespace',
69
+ local: localName,
70
+ imported: '*',
71
+ isTypeOnly: declarationImportKind === 'type',
72
+ range: toSourceRange(specifier),
73
+ };
74
+ }
75
+ if (specifier.type === 'ImportSpecifier') {
76
+ const importedName = isObjectRecord(specifier.imported) && typeof specifier.imported.name === 'string'
77
+ ? specifier.imported.name
78
+ : null;
79
+ const localName = isObjectRecord(specifier.local) && typeof specifier.local.name === 'string'
80
+ ? specifier.local.name
81
+ : null;
82
+ if (!importedName || !localName) {
83
+ return null;
84
+ }
85
+ return {
86
+ kind: 'named',
87
+ local: localName,
88
+ imported: importedName,
89
+ isTypeOnly: declarationImportKind === 'type' || asImportKind(specifier.importKind) === 'type',
90
+ range: toSourceRange(specifier),
91
+ };
92
+ }
93
+ return null;
94
+ };
95
+ const collectImportMetadata = (body) => {
96
+ if (!Array.isArray(body)) {
97
+ return [];
98
+ }
99
+ const imports = [];
100
+ body.forEach(statement => {
101
+ if (!isObjectRecord(statement) ||
102
+ statement.type !== 'ImportDeclaration' ||
103
+ !isObjectRecord(statement.source) ||
104
+ typeof statement.source.value !== 'string') {
105
+ return;
106
+ }
107
+ const importKind = asImportKind(statement.importKind);
108
+ const bindings = Array.isArray(statement.specifiers)
109
+ ? statement.specifiers
110
+ .map(specifier => toImportBinding(specifier, importKind))
111
+ .filter((binding) => binding !== null)
112
+ : [];
113
+ imports.push({
114
+ source: statement.source.value,
115
+ importKind,
116
+ sideEffectOnly: bindings.length === 0 && importKind === 'value',
117
+ bindings,
118
+ range: toSourceRange(statement),
119
+ });
120
+ });
121
+ return imports;
122
+ };
123
+ const ensureSupportedOptions = (options) => {
124
+ if (options.sourceType !== undefined &&
125
+ options.sourceType !== 'module' &&
126
+ options.sourceType !== 'script') {
127
+ throw new Error(`[jsx] Unsupported sourceType "${String(options.sourceType)}". Use "module" or "script".`);
128
+ }
129
+ if (options.typescript !== undefined &&
130
+ options.typescript !== 'preserve' &&
131
+ options.typescript !== 'strip') {
132
+ throw new Error(`[jsx] Unsupported typescript mode "${String(options.typescript)}". Use "preserve" or "strip".`);
133
+ }
134
+ if (options.typescriptStripBackend !== undefined &&
135
+ options.typescriptStripBackend !== 'oxc-transform' &&
136
+ options.typescriptStripBackend !== 'transpile-manual') {
137
+ throw new Error(`[jsx] Unsupported typescriptStripBackend "${String(options.typescriptStripBackend)}". Use "oxc-transform" or "transpile-manual".`);
138
+ }
139
+ };
140
+ function transformJsxSource(source, options = {}) {
141
+ const internalOptions = options;
142
+ ensureSupportedOptions(internalOptions);
143
+ const sourceType = internalOptions.sourceType ?? 'module';
144
+ const typescriptMode = internalOptions.typescript ?? 'preserve';
145
+ const typescriptStripBackend = internalOptions.typescriptStripBackend ?? 'oxc-transform';
146
+ const parsed = (0, oxc_parser_1.parseSync)('transform-jsx-source.tsx', source, createParserOptions(sourceType));
147
+ const parserDiagnostics = parsed.errors.map(error => toDiagnostic('parser', error));
148
+ const imports = collectImportMetadata(parsed.program.body);
149
+ if (parserDiagnostics.length) {
150
+ return {
151
+ code: source,
152
+ changed: false,
153
+ imports,
154
+ diagnostics: parserDiagnostics,
155
+ };
156
+ }
157
+ const transpileBaseOptions = {
158
+ sourceType,
159
+ createElement: internalOptions.createElement,
160
+ fragment: internalOptions.fragment,
161
+ typescript: 'preserve',
162
+ };
163
+ if (typescriptMode !== 'strip') {
164
+ const result = (0, transpile_js_1.transpileJsxSource)(source, transpileBaseOptions);
165
+ return {
166
+ code: result.code,
167
+ changed: result.changed,
168
+ imports,
169
+ diagnostics: parserDiagnostics,
170
+ };
171
+ }
172
+ if (typescriptStripBackend === 'transpile-manual') {
173
+ const result = (0, transpile_js_1.transpileJsxSource)(source, {
174
+ ...transpileBaseOptions,
175
+ typescript: 'strip',
176
+ });
177
+ return {
178
+ code: result.code,
179
+ changed: result.changed,
180
+ imports,
181
+ diagnostics: parserDiagnostics,
182
+ };
183
+ }
184
+ const transformed = (0, oxc_transform_1.transformSync)('transform-jsx-source.tsx', source, {
185
+ lang: 'tsx',
186
+ sourceType,
187
+ jsx: 'preserve',
188
+ typescript: {},
189
+ });
190
+ const transformDiagnostics = transformed.errors.map(error => toDiagnostic('transform', error));
191
+ const diagnostics = [...parserDiagnostics, ...transformDiagnostics];
192
+ if (transformDiagnostics.length) {
193
+ const fallbackCode = transformed.code || source;
194
+ return {
195
+ code: fallbackCode,
196
+ changed: fallbackCode !== source,
197
+ imports,
198
+ diagnostics,
199
+ };
200
+ }
201
+ const jsxResult = (0, transpile_js_1.transpileJsxSource)(transformed.code, transpileBaseOptions);
202
+ return {
203
+ code: jsxResult.code,
204
+ changed: jsxResult.code !== source,
205
+ imports,
206
+ diagnostics,
207
+ };
208
+ }
@@ -0,0 +1,34 @@
1
+ import { type TranspileJsxSourceOptions } from './transpile.cjs';
2
+ type SourceRange = [number, number];
3
+ type DiagnosticSource = 'parser' | 'transform';
4
+ export type TransformDiagnostic = {
5
+ source: DiagnosticSource;
6
+ severity: string;
7
+ message: string;
8
+ range: SourceRange | null;
9
+ codeframe: string | null;
10
+ helpMessage: string | null;
11
+ };
12
+ export type TransformImportBinding = {
13
+ kind: 'default' | 'named' | 'namespace';
14
+ local: string;
15
+ imported: string | null;
16
+ isTypeOnly: boolean;
17
+ range: SourceRange | null;
18
+ };
19
+ export type TransformImport = {
20
+ source: string;
21
+ importKind: 'type' | 'value';
22
+ sideEffectOnly: boolean;
23
+ bindings: TransformImportBinding[];
24
+ range: SourceRange | null;
25
+ };
26
+ export type TransformJsxSourceOptions = TranspileJsxSourceOptions;
27
+ export type TransformJsxSourceResult = {
28
+ code: string;
29
+ changed: boolean;
30
+ imports: TransformImport[];
31
+ diagnostics: TransformDiagnostic[];
32
+ };
33
+ export declare function transformJsxSource(source: string, options?: TransformJsxSourceOptions): TransformJsxSourceResult;
34
+ export {};
@@ -0,0 +1,34 @@
1
+ import { type TranspileJsxSourceOptions } from './transpile.js';
2
+ type SourceRange = [number, number];
3
+ type DiagnosticSource = 'parser' | 'transform';
4
+ export type TransformDiagnostic = {
5
+ source: DiagnosticSource;
6
+ severity: string;
7
+ message: string;
8
+ range: SourceRange | null;
9
+ codeframe: string | null;
10
+ helpMessage: string | null;
11
+ };
12
+ export type TransformImportBinding = {
13
+ kind: 'default' | 'named' | 'namespace';
14
+ local: string;
15
+ imported: string | null;
16
+ isTypeOnly: boolean;
17
+ range: SourceRange | null;
18
+ };
19
+ export type TransformImport = {
20
+ source: string;
21
+ importKind: 'type' | 'value';
22
+ sideEffectOnly: boolean;
23
+ bindings: TransformImportBinding[];
24
+ range: SourceRange | null;
25
+ };
26
+ export type TransformJsxSourceOptions = TranspileJsxSourceOptions;
27
+ export type TransformJsxSourceResult = {
28
+ code: string;
29
+ changed: boolean;
30
+ imports: TransformImport[];
31
+ diagnostics: TransformDiagnostic[];
32
+ };
33
+ export declare function transformJsxSource(source: string, options?: TransformJsxSourceOptions): TransformJsxSourceResult;
34
+ export {};
@@ -0,0 +1,205 @@
1
+ import { parseSync } from 'oxc-parser';
2
+ import { transformSync } from 'oxc-transform';
3
+ import { transpileJsxSource } from './transpile.js';
4
+ const createParserOptions = (sourceType) => ({
5
+ lang: 'tsx',
6
+ sourceType,
7
+ range: true,
8
+ preserveParens: true,
9
+ });
10
+ const isObjectRecord = (value) => typeof value === 'object' && value !== null;
11
+ const isSourceRange = (value) => Array.isArray(value) &&
12
+ value.length === 2 &&
13
+ typeof value[0] === 'number' &&
14
+ typeof value[1] === 'number';
15
+ const toSourceRange = (value) => {
16
+ if (!isObjectRecord(value) || !isSourceRange(value.range)) {
17
+ return null;
18
+ }
19
+ return value.range;
20
+ };
21
+ const asImportKind = (value) => value === 'type' ? 'type' : 'value';
22
+ const toDiagnostic = (source, diagnostic) => {
23
+ const firstLabel = diagnostic.labels?.[0];
24
+ const range = firstLabel &&
25
+ typeof firstLabel.start === 'number' &&
26
+ typeof firstLabel.end === 'number'
27
+ ? [firstLabel.start, firstLabel.end]
28
+ : null;
29
+ return {
30
+ source,
31
+ severity: diagnostic.severity,
32
+ message: diagnostic.message,
33
+ range,
34
+ codeframe: diagnostic.codeframe,
35
+ helpMessage: diagnostic.helpMessage,
36
+ };
37
+ };
38
+ const toImportBinding = (specifier, declarationImportKind) => {
39
+ if (!isObjectRecord(specifier) || typeof specifier.type !== 'string') {
40
+ return null;
41
+ }
42
+ if (specifier.type === 'ImportDefaultSpecifier') {
43
+ const localName = isObjectRecord(specifier.local) && typeof specifier.local.name === 'string'
44
+ ? specifier.local.name
45
+ : null;
46
+ if (!localName) {
47
+ return null;
48
+ }
49
+ return {
50
+ kind: 'default',
51
+ local: localName,
52
+ imported: 'default',
53
+ isTypeOnly: declarationImportKind === 'type',
54
+ range: toSourceRange(specifier),
55
+ };
56
+ }
57
+ if (specifier.type === 'ImportNamespaceSpecifier') {
58
+ const localName = isObjectRecord(specifier.local) && typeof specifier.local.name === 'string'
59
+ ? specifier.local.name
60
+ : null;
61
+ if (!localName) {
62
+ return null;
63
+ }
64
+ return {
65
+ kind: 'namespace',
66
+ local: localName,
67
+ imported: '*',
68
+ isTypeOnly: declarationImportKind === 'type',
69
+ range: toSourceRange(specifier),
70
+ };
71
+ }
72
+ if (specifier.type === 'ImportSpecifier') {
73
+ const importedName = isObjectRecord(specifier.imported) && typeof specifier.imported.name === 'string'
74
+ ? specifier.imported.name
75
+ : null;
76
+ const localName = isObjectRecord(specifier.local) && typeof specifier.local.name === 'string'
77
+ ? specifier.local.name
78
+ : null;
79
+ if (!importedName || !localName) {
80
+ return null;
81
+ }
82
+ return {
83
+ kind: 'named',
84
+ local: localName,
85
+ imported: importedName,
86
+ isTypeOnly: declarationImportKind === 'type' || asImportKind(specifier.importKind) === 'type',
87
+ range: toSourceRange(specifier),
88
+ };
89
+ }
90
+ return null;
91
+ };
92
+ const collectImportMetadata = (body) => {
93
+ if (!Array.isArray(body)) {
94
+ return [];
95
+ }
96
+ const imports = [];
97
+ body.forEach(statement => {
98
+ if (!isObjectRecord(statement) ||
99
+ statement.type !== 'ImportDeclaration' ||
100
+ !isObjectRecord(statement.source) ||
101
+ typeof statement.source.value !== 'string') {
102
+ return;
103
+ }
104
+ const importKind = asImportKind(statement.importKind);
105
+ const bindings = Array.isArray(statement.specifiers)
106
+ ? statement.specifiers
107
+ .map(specifier => toImportBinding(specifier, importKind))
108
+ .filter((binding) => binding !== null)
109
+ : [];
110
+ imports.push({
111
+ source: statement.source.value,
112
+ importKind,
113
+ sideEffectOnly: bindings.length === 0 && importKind === 'value',
114
+ bindings,
115
+ range: toSourceRange(statement),
116
+ });
117
+ });
118
+ return imports;
119
+ };
120
+ const ensureSupportedOptions = (options) => {
121
+ if (options.sourceType !== undefined &&
122
+ options.sourceType !== 'module' &&
123
+ options.sourceType !== 'script') {
124
+ throw new Error(`[jsx] Unsupported sourceType "${String(options.sourceType)}". Use "module" or "script".`);
125
+ }
126
+ if (options.typescript !== undefined &&
127
+ options.typescript !== 'preserve' &&
128
+ options.typescript !== 'strip') {
129
+ throw new Error(`[jsx] Unsupported typescript mode "${String(options.typescript)}". Use "preserve" or "strip".`);
130
+ }
131
+ if (options.typescriptStripBackend !== undefined &&
132
+ options.typescriptStripBackend !== 'oxc-transform' &&
133
+ options.typescriptStripBackend !== 'transpile-manual') {
134
+ throw new Error(`[jsx] Unsupported typescriptStripBackend "${String(options.typescriptStripBackend)}". Use "oxc-transform" or "transpile-manual".`);
135
+ }
136
+ };
137
+ export function transformJsxSource(source, options = {}) {
138
+ const internalOptions = options;
139
+ ensureSupportedOptions(internalOptions);
140
+ const sourceType = internalOptions.sourceType ?? 'module';
141
+ const typescriptMode = internalOptions.typescript ?? 'preserve';
142
+ const typescriptStripBackend = internalOptions.typescriptStripBackend ?? 'oxc-transform';
143
+ const parsed = parseSync('transform-jsx-source.tsx', source, createParserOptions(sourceType));
144
+ const parserDiagnostics = parsed.errors.map(error => toDiagnostic('parser', error));
145
+ const imports = collectImportMetadata(parsed.program.body);
146
+ if (parserDiagnostics.length) {
147
+ return {
148
+ code: source,
149
+ changed: false,
150
+ imports,
151
+ diagnostics: parserDiagnostics,
152
+ };
153
+ }
154
+ const transpileBaseOptions = {
155
+ sourceType,
156
+ createElement: internalOptions.createElement,
157
+ fragment: internalOptions.fragment,
158
+ typescript: 'preserve',
159
+ };
160
+ if (typescriptMode !== 'strip') {
161
+ const result = transpileJsxSource(source, transpileBaseOptions);
162
+ return {
163
+ code: result.code,
164
+ changed: result.changed,
165
+ imports,
166
+ diagnostics: parserDiagnostics,
167
+ };
168
+ }
169
+ if (typescriptStripBackend === 'transpile-manual') {
170
+ const result = transpileJsxSource(source, {
171
+ ...transpileBaseOptions,
172
+ typescript: 'strip',
173
+ });
174
+ return {
175
+ code: result.code,
176
+ changed: result.changed,
177
+ imports,
178
+ diagnostics: parserDiagnostics,
179
+ };
180
+ }
181
+ const transformed = transformSync('transform-jsx-source.tsx', source, {
182
+ lang: 'tsx',
183
+ sourceType,
184
+ jsx: 'preserve',
185
+ typescript: {},
186
+ });
187
+ const transformDiagnostics = transformed.errors.map(error => toDiagnostic('transform', error));
188
+ const diagnostics = [...parserDiagnostics, ...transformDiagnostics];
189
+ if (transformDiagnostics.length) {
190
+ const fallbackCode = transformed.code || source;
191
+ return {
192
+ code: fallbackCode,
193
+ changed: fallbackCode !== source,
194
+ imports,
195
+ diagnostics,
196
+ };
197
+ }
198
+ const jsxResult = transpileJsxSource(transformed.code, transpileBaseOptions);
199
+ return {
200
+ code: jsxResult.code,
201
+ changed: jsxResult.code !== source,
202
+ imports,
203
+ diagnostics,
204
+ };
205
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knighted/jsx",
3
- "version": "1.9.2",
3
+ "version": "1.10.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",
@@ -106,6 +106,11 @@
106
106
  "import": "./dist/transpile.js",
107
107
  "default": "./dist/transpile.js"
108
108
  },
109
+ "./transform": {
110
+ "types": "./dist/transform.d.ts",
111
+ "import": "./dist/transform.js",
112
+ "default": "./dist/transform.js"
113
+ },
109
114
  "./loader": {
110
115
  "import": "./dist/loader/jsx.js",
111
116
  "default": "./dist/loader/jsx.js"
@@ -185,6 +190,7 @@
185
190
  "@napi-rs/wasm-runtime": "^1.1.1",
186
191
  "magic-string": "^0.30.21",
187
192
  "oxc-parser": "^0.116.0",
193
+ "oxc-transform": "^0.116.0",
188
194
  "property-information": "^7.1.0",
189
195
  "tar": "^7.5.11"
190
196
  },