Skip to content

Commit 4b41dd3

Browse files
feat: top-level declarations from transform. (#91)
1 parent af5d069 commit 4b41dd3

4 files changed

Lines changed: 539 additions & 4 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@knighted/jsx",
3-
"version": "1.10.0",
3+
"version": "1.11.0",
44
"description": "Runtime JSX tagged template that renders DOM or React trees anywhere with or without a build step.",
55
"keywords": [
66
"jsx runtime",

src/transform.ts

Lines changed: 198 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,29 @@ export type TransformImport = {
4242
range: SourceRange | null
4343
}
4444

45-
export type TransformJsxSourceOptions = TranspileJsxSourceOptions
45+
export type TransformTopLevelDeclarationKind = 'function' | 'class' | 'variable'
46+
47+
export type TransformTopLevelDeclarationExportKind = 'none' | 'named' | 'default'
48+
49+
export type TransformVariableInitializerKind =
50+
| 'arrow-function'
51+
| 'function-expression'
52+
| 'class-expression'
53+
| 'other'
54+
| null
55+
56+
export type TransformTopLevelDeclaration = {
57+
name: string
58+
kind: TransformTopLevelDeclarationKind
59+
exportKind: TransformTopLevelDeclarationExportKind
60+
range: SourceRange | null
61+
statementRange: SourceRange | null
62+
initializerKind: TransformVariableInitializerKind
63+
}
64+
65+
export type TransformJsxSourceOptions = TranspileJsxSourceOptions & {
66+
collectTopLevelDeclarations?: boolean
67+
}
4668

4769
type InternalTransformJsxSourceOptions = TransformJsxSourceOptions & {
4870
/* Internal compare switch for parity spikes. */
@@ -54,6 +76,7 @@ export type TransformJsxSourceResult = {
5476
changed: boolean
5577
imports: TransformImport[]
5678
diagnostics: TransformDiagnostic[]
79+
declarations?: TransformTopLevelDeclaration[]
5780
}
5881

5982
const createParserOptions = (sourceType: TransformSourceType) => ({
@@ -214,6 +237,163 @@ const collectImportMetadata = (body: unknown): TransformImport[] => {
214237
return imports
215238
}
216239

240+
const toIdentifierName = (value: unknown): string | null => {
241+
if (!isObjectRecord(value)) {
242+
return null
243+
}
244+
245+
if (value.type !== 'Identifier') {
246+
return null
247+
}
248+
249+
return typeof value.name === 'string' ? value.name : null
250+
}
251+
252+
const toVariableInitializerKind = (value: unknown): TransformVariableInitializerKind => {
253+
if (!isObjectRecord(value) || typeof value.type !== 'string') {
254+
return null
255+
}
256+
257+
if (value.type === 'ArrowFunctionExpression') {
258+
return 'arrow-function'
259+
}
260+
261+
if (value.type === 'FunctionExpression') {
262+
return 'function-expression'
263+
}
264+
265+
if (value.type === 'ClassExpression') {
266+
return 'class-expression'
267+
}
268+
269+
return 'other'
270+
}
271+
272+
const pushTopLevelDeclarationMetadata = ({
273+
declaration,
274+
exportKind,
275+
statementRange,
276+
declarations,
277+
}: {
278+
declaration: Record<string, unknown>
279+
exportKind: TransformTopLevelDeclarationExportKind
280+
statementRange: SourceRange | null
281+
declarations: TransformTopLevelDeclaration[]
282+
}) => {
283+
if (declaration.type === 'FunctionDeclaration') {
284+
const name = toIdentifierName(declaration.id)
285+
if (!name) {
286+
return
287+
}
288+
289+
declarations.push({
290+
name,
291+
kind: 'function',
292+
exportKind,
293+
range: toSourceRange(declaration),
294+
statementRange,
295+
initializerKind: null,
296+
})
297+
return
298+
}
299+
300+
if (declaration.type === 'ClassDeclaration') {
301+
const name = toIdentifierName(declaration.id)
302+
if (!name) {
303+
return
304+
}
305+
306+
declarations.push({
307+
name,
308+
kind: 'class',
309+
exportKind,
310+
range: toSourceRange(declaration),
311+
statementRange,
312+
initializerKind: null,
313+
})
314+
return
315+
}
316+
317+
if (!Array.isArray(declaration.declarations)) {
318+
return
319+
}
320+
321+
for (const declarator of declaration.declarations) {
322+
if (!isObjectRecord(declarator)) {
323+
continue
324+
}
325+
326+
const name = toIdentifierName(declarator.id)
327+
if (!name) {
328+
continue
329+
}
330+
331+
declarations.push({
332+
name,
333+
kind: 'variable',
334+
exportKind,
335+
range: toSourceRange(declarator),
336+
statementRange,
337+
initializerKind: toVariableInitializerKind(declarator.init),
338+
})
339+
}
340+
}
341+
342+
const collectTopLevelDeclarationMetadata = (
343+
body: unknown,
344+
): TransformTopLevelDeclaration[] => {
345+
if (!Array.isArray(body)) {
346+
return []
347+
}
348+
349+
const declarations: TransformTopLevelDeclaration[] = []
350+
351+
for (const statement of body) {
352+
if (!isObjectRecord(statement) || typeof statement.type !== 'string') {
353+
continue
354+
}
355+
356+
const statementRange = toSourceRange(statement)
357+
358+
if (statement.type === 'ExportNamedDeclaration') {
359+
if (!isObjectRecord(statement.declaration)) {
360+
continue
361+
}
362+
363+
pushTopLevelDeclarationMetadata({
364+
declaration: statement.declaration,
365+
exportKind: 'named',
366+
statementRange,
367+
declarations,
368+
})
369+
continue
370+
}
371+
372+
if (statement.type === 'ExportDefaultDeclaration') {
373+
if (!isObjectRecord(statement.declaration)) {
374+
continue
375+
}
376+
377+
pushTopLevelDeclarationMetadata({
378+
declaration: statement.declaration,
379+
exportKind: 'default',
380+
statementRange,
381+
declarations,
382+
})
383+
continue
384+
}
385+
386+
pushTopLevelDeclarationMetadata({
387+
declaration: statement,
388+
exportKind: 'none',
389+
statementRange,
390+
declarations,
391+
})
392+
}
393+
394+
return declarations
395+
}
396+
217397
const ensureSupportedOptions = (options: InternalTransformJsxSourceOptions) => {
218398
if (
219399
options.sourceType !== undefined &&
@@ -244,6 +424,15 @@ const ensureSupportedOptions = (options: InternalTransformJsxSourceOptions) => {
244424
`[jsx] Unsupported typescriptStripBackend "${String(options.typescriptStripBackend)}". Use "oxc-transform" or "transpile-manual".`,
245425
)
246426
}
427+
428+
if (
429+
options.collectTopLevelDeclarations !== undefined &&
430+
typeof options.collectTopLevelDeclarations !== 'boolean'
431+
) {
432+
throw new Error(
433+
`[jsx] Unsupported collectTopLevelDeclarations value "${String(options.collectTopLevelDeclarations)}". Use true or false.`,
434+
)
435+
}
247436
}
248437

249438
export function transformJsxSource(
@@ -266,13 +455,17 @@ export function transformJsxSource(
266455

267456
const parserDiagnostics = parsed.errors.map(error => toDiagnostic('parser', error))
268457
const imports = collectImportMetadata(parsed.program.body)
458+
const declarations = internalOptions.collectTopLevelDeclarations
459+
? collectTopLevelDeclarationMetadata(parsed.program.body)
460+
: undefined
269461

270462
if (parserDiagnostics.length) {
271463
return {
272464
code: source,
273465
changed: false,
274466
imports,
275467
diagnostics: parserDiagnostics,
468+
declarations,
276469
}
277470
}
278471

@@ -290,6 +483,7 @@ export function transformJsxSource(
290483
changed: result.changed,
291484
imports,
292485
diagnostics: parserDiagnostics,
486+
declarations,
293487
}
294488
}
295489

@@ -304,6 +498,7 @@ export function transformJsxSource(
304498
changed: result.changed,
305499
imports,
306500
diagnostics: parserDiagnostics,
501+
declarations,
307502
}
308503
}
309504

@@ -326,6 +521,7 @@ export function transformJsxSource(
326521
changed: fallbackCode !== source,
327522
imports,
328523
diagnostics,
524+
declarations,
329525
}
330526
}
331527

@@ -336,5 +532,6 @@ export function transformJsxSource(
336532
changed: jsxResult.code !== source,
337533
imports,
338534
diagnostics,
535+
declarations,
339536
}
340537
}

0 commit comments

Comments
 (0)