Skip to content

Commit 25062c2

Browse files
feat: react idomatic imports.
1 parent ce96325 commit 25062c2

4 files changed

Lines changed: 189 additions & 22 deletions

File tree

src/bootstrap.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { getPrimaryCdnImportUrls } from './modules/cdn.js'
99
const preloadImportKeys = [
1010
'cssBrowser',
1111
'jsxDom',
12-
'jsxTranspile',
12+
'jsxTransform',
1313
'jsxReact',
1414
'react',
1515
'reactDomClient',

src/modules/cdn.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,10 @@ export const cdnImportSpecs = {
5454
esm: '@knighted/jsx',
5555
jspmGa: 'npm:@knighted/jsx',
5656
},
57-
jsxTranspile: {
58-
importMap: '@knighted/jsx/transpile',
59-
esm: '@knighted/jsx/transpile',
60-
jspmGa: 'npm:@knighted/jsx/transpile',
57+
jsxTransform: {
58+
importMap: '@knighted/jsx/transform',
59+
esm: '@knighted/jsx/transform',
60+
jspmGa: 'npm:@knighted/jsx/transform',
6161
},
6262
jsxReact: {
6363
importMap: '@knighted/jsx/react',

src/modules/defaults.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export const defaultJsx = [
2626
].join('\n')
2727

2828
export const defaultReactJsx = [
29+
"import { useState } from 'react'",
30+
'',
2931
'type CounterButtonProps = {',
3032
' label: string',
3133
' active: boolean',
@@ -44,7 +46,6 @@ export const defaultReactJsx = [
4446
')',
4547
'',
4648
'const App = () => {',
47-
' const { useState } = React',
4849
' const [count, setCount] = useState(0)',
4950
' const handleClick = (_event: MouseEvent) => {',
5051
' setCount(current => current + 1)',

src/modules/render-runtime.js

Lines changed: 182 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ export const createRenderRuntimeController = ({
4343
if (coreRuntime) return coreRuntime
4444

4545
try {
46-
const [cssBrowser, jsxDom, jsxTranspile] = await Promise.all([
46+
const [cssBrowser, jsxDom, jsxTransform] = await Promise.all([
4747
importFromCdnWithFallback(cdnImports.cssBrowser),
4848
importFromCdnWithFallback(cdnImports.jsxDom),
49-
importFromCdnWithFallback(cdnImports.jsxTranspile),
49+
importFromCdnWithFallback(cdnImports.jsxTransform),
5050
])
5151

5252
if (typeof cssBrowser.module.cssFromSource !== 'function') {
@@ -57,16 +57,16 @@ export const createRenderRuntimeController = ({
5757
throw new Error(`jsx export was not found from ${jsxDom.url}`)
5858
}
5959

60-
if (typeof jsxTranspile.module.transpileJsxSource !== 'function') {
60+
if (typeof jsxTransform.module.transformJsxSource !== 'function') {
6161
throw new Error(
62-
`transpileJsxSource export was not found from ${jsxTranspile.url}`,
62+
`transformJsxSource export was not found from ${jsxTransform.url}`,
6363
)
6464
}
6565

6666
coreRuntime = {
6767
cssFromSource: cssBrowser.module.cssFromSource,
6868
jsx: jsxDom.module.jsx,
69-
transpileJsxSource: jsxTranspile.module.transpileJsxSource,
69+
transformJsxSource: jsxTransform.module.transformJsxSource,
7070
}
7171

7272
return coreRuntime
@@ -261,7 +261,109 @@ export const createRenderRuntimeController = ({
261261
return React.cloneElement(value, nextProps)
262262
}
263263

264-
const shouldAttemptTranspileFallback = error => error instanceof SyntaxError
264+
const shouldAttemptTranspileFallback = error => {
265+
if (error instanceof SyntaxError) {
266+
return true
267+
}
268+
269+
if (!(error instanceof Error)) {
270+
return false
271+
}
272+
273+
return /Unexpected token|Cannot use import statement|Unexpected identifier/.test(
274+
error.message,
275+
)
276+
}
277+
278+
const isImportRange = range =>
279+
Array.isArray(range) &&
280+
range.length === 2 &&
281+
Number.isInteger(range[0]) &&
282+
Number.isInteger(range[1])
283+
284+
const stripImportDeclarations = (code, imports) => {
285+
const ranges = imports
286+
.map(entry => entry?.range)
287+
.filter(isImportRange)
288+
.slice()
289+
.sort((first, second) => second[0] - first[0])
290+
291+
let output = code
292+
293+
for (const [start, end] of ranges) {
294+
if (start < 0 || end < start || end > output.length) {
295+
continue
296+
}
297+
298+
output = `${output.slice(0, start)}${output.slice(end)}`
299+
}
300+
301+
return output
302+
}
303+
304+
const buildRuntimeImportPlan = imports => {
305+
const preamble = []
306+
const unsupportedSources = new Set()
307+
let requiresReactRuntime = false
308+
309+
for (const entry of imports) {
310+
if (!entry || entry.importKind !== 'value') {
311+
continue
312+
}
313+
314+
if (entry.source !== 'react') {
315+
unsupportedSources.add(entry.source)
316+
continue
317+
}
318+
319+
requiresReactRuntime = true
320+
321+
for (const binding of entry.bindings ?? []) {
322+
if (!binding || binding.isTypeOnly) {
323+
continue
324+
}
325+
326+
if (binding.kind === 'default' || binding.kind === 'namespace') {
327+
preamble.push(`const ${binding.local} = React`)
328+
continue
329+
}
330+
331+
if (binding.kind === 'named') {
332+
if (binding.imported === 'default') {
333+
preamble.push(`const ${binding.local} = React`)
334+
} else {
335+
preamble.push(`const ${binding.local} = React.${binding.imported}`)
336+
}
337+
}
338+
}
339+
}
340+
341+
return {
342+
preamble,
343+
requiresReactRuntime,
344+
unsupportedSources: [...unsupportedSources],
345+
}
346+
}
347+
348+
const formatTransformDiagnosticsError = diagnostics => {
349+
const firstDiagnostic = diagnostics?.[0]
350+
351+
if (!firstDiagnostic) {
352+
return '[jsx] Failed to transform source.'
353+
}
354+
355+
const lines = [`[jsx] ${firstDiagnostic.message}`]
356+
357+
if (firstDiagnostic.codeframe) {
358+
lines.push(firstDiagnostic.codeframe)
359+
}
360+
361+
if (firstDiagnostic.helpMessage) {
362+
lines.push(firstDiagnostic.helpMessage)
363+
}
364+
365+
return lines.join('\n')
366+
}
265367

266368
const createUserModuleFactory = source =>
267369
new Function(
@@ -464,7 +566,8 @@ export const createRenderRuntimeController = ({
464566
}
465567

466568
const evaluateUserModule = async (helpers = {}) => {
467-
const { jsx, transpileJsxSource } = await ensureCoreRuntime()
569+
const { jsx, transformJsxSource } = await ensureCoreRuntime()
570+
let runtimeHelpers = helpers
468571
const userCode = getJsxSource()
469572
.replace(/^\s*export\s+default\s+function\b/gm, '__defaultExport = function')
470573
.replace(/^\s*export\s+default\s+class\b/gm, '__defaultExport = class')
@@ -482,34 +585,97 @@ export const createRenderRuntimeController = ({
482585
const transpileMode = helpers.React && helpers.reactJsx ? 'react' : 'dom'
483586
const transpileOptionsByMode = {
484587
dom: {
485-
sourceType: 'script',
588+
sourceType: 'module',
486589
createElement: 'jsx.createElement',
487590
fragment: 'jsx.Fragment',
488591
typescript: 'strip',
489592
},
490593
react: {
491-
sourceType: 'script',
594+
sourceType: 'module',
492595
createElement: 'React.createElement',
493596
fragment: 'React.Fragment',
494597
typescript: 'strip',
495598
},
496599
}
497-
const transpiledUserCode = transpileJsxSource(
600+
const transformedResult = transformJsxSource(
498601
userCode,
499602
transpileOptionsByMode[transpileMode],
500-
).code
501-
const moduleFactory = createUserModuleFactory(transpiledUserCode)
603+
)
604+
605+
if (transformedResult.diagnostics.length > 0) {
606+
throw new Error(formatTransformDiagnosticsError(transformedResult.diagnostics), {
607+
cause: error,
608+
})
609+
}
610+
611+
const importAnalysisResult = transformJsxSource(transformedResult.code, {
612+
sourceType: 'module',
613+
typescript: 'preserve',
614+
})
502615

503-
if (helpers.React && helpers.reactJsx) {
504-
return moduleFactory(helpers.jsx ?? jsx, helpers.reactJsx, helpers.React)
616+
if (importAnalysisResult.diagnostics.length > 0) {
617+
throw new Error(
618+
formatTransformDiagnosticsError(importAnalysisResult.diagnostics),
619+
{
620+
cause: error,
621+
},
622+
)
623+
}
624+
625+
const runtimeImportPlan = buildRuntimeImportPlan(importAnalysisResult.imports)
626+
627+
if (runtimeImportPlan.unsupportedSources.length > 0) {
628+
throw new Error(
629+
`Unsupported runtime imports in playground execution: ${runtimeImportPlan.unsupportedSources
630+
.map(specifier => `'${specifier}'`)
631+
.join(', ')}.`,
632+
{
633+
cause: error,
634+
},
635+
)
636+
}
637+
638+
if (runtimeImportPlan.requiresReactRuntime && !runtimeHelpers.React) {
639+
const { React, reactJsx } = await ensureReactRuntime()
640+
runtimeHelpers = {
641+
...runtimeHelpers,
642+
React,
643+
reactJsx: runtimeHelpers.reactJsx ?? reactJsx,
644+
}
645+
}
646+
647+
const runtimeCode = stripImportDeclarations(
648+
transformedResult.code,
649+
importAnalysisResult.imports,
650+
)
651+
const executableUserCode = runtimeImportPlan.preamble.length
652+
? `${runtimeImportPlan.preamble.join('\n')}\n${runtimeCode}`
653+
: runtimeCode
654+
655+
const moduleFactory = createUserModuleFactory(executableUserCode)
656+
657+
if (runtimeHelpers.React && runtimeHelpers.reactJsx) {
658+
return moduleFactory(
659+
runtimeHelpers.jsx ?? jsx,
660+
runtimeHelpers.reactJsx,
661+
runtimeHelpers.React,
662+
)
505663
}
506664

507665
if (transpileMode === 'dom') {
508-
return moduleFactory(helpers.jsx ?? jsx, helpers.reactJsx, helpers.React)
666+
return moduleFactory(
667+
runtimeHelpers.jsx ?? jsx,
668+
runtimeHelpers.reactJsx,
669+
runtimeHelpers.React,
670+
)
509671
}
510672

511673
const { React, reactJsx } = await ensureReactRuntime()
512-
return moduleFactory(helpers.jsx ?? jsx, helpers.reactJsx ?? reactJsx, React)
674+
return moduleFactory(
675+
runtimeHelpers.jsx ?? jsx,
676+
runtimeHelpers.reactJsx ?? reactJsx,
677+
React,
678+
)
513679
}
514680
}
515681

0 commit comments

Comments
 (0)