@@ -8,6 +8,8 @@ const appGridLayoutButtons = document.querySelectorAll('[data-app-grid-layout]')
88const appThemeButtons = document . querySelectorAll ( '[data-app-theme]' )
99const renderMode = document . getElementById ( 'render-mode' )
1010const autoRenderToggle = document . getElementById ( 'auto-render' )
11+ const typecheckToggle = document . getElementById ( 'typecheck-enabled' )
12+ const typecheckButton = document . getElementById ( 'typecheck-button' )
1113const renderButton = document . getElementById ( 'render-button' )
1214const copyComponentButton = document . getElementById ( 'copy-component' )
1315const clearComponentButton = document . getElementById ( 'clear-component' )
@@ -18,6 +20,7 @@ const shadowToggle = document.getElementById('shadow-toggle')
1820const jsxEditor = document . getElementById ( 'jsx-editor' )
1921const cssEditor = document . getElementById ( 'css-editor' )
2022const styleWarning = document . getElementById ( 'style-warning' )
23+ const typeDiagnostics = document . getElementById ( 'type-diagnostics' )
2124const cdnLoading = document . getElementById ( 'cdn-loading' )
2225const previewBgColorInput = document . getElementById ( 'preview-bg-color' )
2326const clearConfirmDialog = document . getElementById ( 'clear-confirm-dialog' )
@@ -39,6 +42,7 @@ let sassCompiler = null
3942let lessCompiler = null
4043let lightningCssWasm = null
4144let coreRuntime = null
45+ let typeScriptCompiler = null
4246let compiledStylesCache = {
4347 key : null ,
4448 value : null ,
@@ -47,6 +51,8 @@ let pendingClearAction = null
4751let hasCompletedInitialRender = false
4852let previewBackgroundColor = null
4953let previewBackgroundCustomized = false
54+ let typeCheckRunId = 0
55+ let lastTypeErrorCount = 0
5056const clipboardSupported = Boolean ( navigator . clipboard ?. writeText )
5157const appGridLayoutStorageKey = 'knighted-develop:app-grid-layout'
5258const appThemeStorageKey = 'knighted-develop:theme'
@@ -81,7 +87,10 @@ const initializeCodeEditors = async () => {
8187 parent : jsxHost ,
8288 value : defaultJsx ,
8389 language : 'javascript-jsx' ,
84- onChange : maybeRender ,
90+ onChange : ( ) => {
91+ maybeRender ( )
92+ markTypeDiagnosticsStale ( )
93+ } ,
8594 } ) ,
8695 createCodeMirrorEditor ( {
8796 parent : cssHost ,
@@ -148,6 +157,246 @@ const setStatus = text => {
148157 statusNode . textContent = text
149158}
150159
160+ const updateTypecheckButtonState = ( ) => {
161+ if ( ! typecheckButton ) {
162+ return
163+ }
164+
165+ const enabled = Boolean ( typecheckToggle ?. checked )
166+ typecheckButton . hidden = ! enabled
167+ typecheckButton . disabled = ! enabled
168+ }
169+
170+ const setTypeDiagnostics = ( text , level = 'muted' ) => {
171+ if ( ! typeDiagnostics ) {
172+ return
173+ }
174+
175+ typeDiagnostics . classList . remove (
176+ 'panel-footer--muted' ,
177+ 'panel-footer--ok' ,
178+ 'panel-footer--error' ,
179+ )
180+
181+ if ( ! text ) {
182+ typeDiagnostics . hidden = true
183+ typeDiagnostics . textContent = ''
184+ return
185+ }
186+
187+ typeDiagnostics . hidden = false
188+ typeDiagnostics . textContent = text
189+
190+ if ( level === 'ok' ) {
191+ typeDiagnostics . classList . add ( 'panel-footer--ok' )
192+ return
193+ }
194+
195+ if ( level === 'error' ) {
196+ typeDiagnostics . classList . add ( 'panel-footer--error' )
197+ return
198+ }
199+
200+ typeDiagnostics . classList . add ( 'panel-footer--muted' )
201+ }
202+
203+ const setRenderedStatus = ( ) => {
204+ if ( lastTypeErrorCount > 0 ) {
205+ setStatus ( `Rendered (Type errors: ${ lastTypeErrorCount } )` )
206+ return
207+ }
208+
209+ if ( statusNode . textContent . startsWith ( 'Rendered (Type errors:' ) ) {
210+ setStatus ( 'Rendered' )
211+ }
212+ }
213+
214+ const flattenTypeDiagnosticMessage = ( compiler , messageText ) => {
215+ if ( typeof compiler . flattenDiagnosticMessageText === 'function' ) {
216+ return compiler . flattenDiagnosticMessageText ( messageText , '\n' )
217+ }
218+
219+ if ( typeof messageText === 'string' ) {
220+ return messageText
221+ }
222+
223+ if ( messageText && typeof messageText . messageText === 'string' ) {
224+ return messageText . messageText
225+ }
226+
227+ return 'Unknown TypeScript diagnostic'
228+ }
229+
230+ const formatTypeDiagnostic = ( compiler , diagnostic ) => {
231+ const message = flattenTypeDiagnosticMessage ( compiler , diagnostic . messageText )
232+
233+ if ( ! diagnostic . file || typeof diagnostic . start !== 'number' ) {
234+ return message
235+ }
236+
237+ const position = diagnostic . file . getLineAndCharacterOfPosition ( diagnostic . start )
238+ return `L${ position . line + 1 } :${ position . character + 1 } ${ message } `
239+ }
240+
241+ const ensureTypeScriptCompiler = async ( ) => {
242+ if ( typeScriptCompiler ) {
243+ return typeScriptCompiler
244+ }
245+
246+ try {
247+ const loaded = await importFromCdnWithFallback ( cdnImports . typescript )
248+ typeScriptCompiler = loaded . module . default ?? loaded . module
249+
250+ if ( typeof typeScriptCompiler . transpileModule !== 'function' ) {
251+ throw new Error ( `transpileModule export was not found from ${ loaded . url } ` )
252+ }
253+
254+ return typeScriptCompiler
255+ } catch ( error ) {
256+ const message =
257+ error instanceof Error ? error . message : 'Unknown TypeScript module loading failure'
258+ throw new Error (
259+ `Unable to load TypeScript diagnostics runtime from CDN: ${ message } ` ,
260+ {
261+ cause : error ,
262+ } ,
263+ )
264+ }
265+ }
266+
267+ const shouldIgnoreTypeDiagnostic = diagnostic => {
268+ const ignoredCodes = new Set ( [ 2318 , 6053 ] )
269+ return ignoredCodes . has ( diagnostic . code )
270+ }
271+
272+ const collectTypeDiagnostics = ( compiler , sourceText ) => {
273+ const sourceFileName = 'component.tsx'
274+ const jsxTypesFileName = 'knighted-jsx-runtime.d.ts'
275+ const jsxTypes =
276+ 'declare namespace JSX {\n' +
277+ ' interface Element {}\n' +
278+ ' interface IntrinsicElements { [elemName: string]: any }\n' +
279+ '}\n'
280+
281+ const files = new Map ( [
282+ [ sourceFileName , sourceText ] ,
283+ [ jsxTypesFileName , jsxTypes ] ,
284+ ] )
285+
286+ const options = {
287+ jsx : compiler . JsxEmit ?. Preserve ,
288+ target : compiler . ScriptTarget ?. ES2022 ,
289+ module : compiler . ModuleKind ?. ESNext ,
290+ strict : true ,
291+ noEmit : true ,
292+ noLib : true ,
293+ skipLibCheck : true ,
294+ }
295+
296+ const host = {
297+ fileExists : fileName => files . has ( fileName ) ,
298+ readFile : fileName => files . get ( fileName ) ,
299+ getSourceFile : ( fileName , languageVersion ) => {
300+ const text = files . get ( fileName )
301+ if ( typeof text !== 'string' ) {
302+ return undefined
303+ }
304+
305+ const scriptKind = fileName . endsWith ( '.tsx' )
306+ ? compiler . ScriptKind ?. TSX
307+ : compiler . ScriptKind ?. TS
308+
309+ return compiler . createSourceFile ( fileName , text , languageVersion , true , scriptKind )
310+ } ,
311+ getDefaultLibFileName : ( ) => 'lib.d.ts' ,
312+ writeFile : ( ) => { } ,
313+ getCurrentDirectory : ( ) => '/' ,
314+ getDirectories : ( ) => [ ] ,
315+ getCanonicalFileName : fileName => fileName ,
316+ useCaseSensitiveFileNames : ( ) => true ,
317+ getNewLine : ( ) => '\n' ,
318+ }
319+
320+ const program = compiler . createProgram ( {
321+ rootNames : [ sourceFileName , jsxTypesFileName ] ,
322+ options,
323+ host,
324+ } )
325+
326+ return compiler
327+ . getPreEmitDiagnostics ( program )
328+ . filter ( diagnostic => ! shouldIgnoreTypeDiagnostic ( diagnostic ) )
329+ }
330+
331+ const runTypeDiagnostics = async runId => {
332+ if ( ! typecheckToggle ?. checked ) {
333+ return
334+ }
335+
336+ setTypeDiagnostics ( 'Type checking…' , 'muted' )
337+
338+ try {
339+ const compiler = await ensureTypeScriptCompiler ( )
340+ if ( runId !== typeCheckRunId || ! typecheckToggle . checked ) {
341+ return
342+ }
343+
344+ const diagnostics = collectTypeDiagnostics ( compiler , getJsxSource ( ) )
345+ const errorCategory = compiler . DiagnosticCategory ?. Error
346+ const errors = diagnostics . filter ( diagnostic => diagnostic . category === errorCategory )
347+ lastTypeErrorCount = errors . length
348+
349+ if ( errors . length === 0 ) {
350+ setTypeDiagnostics ( 'Type checking enabled. No TypeScript errors found.' , 'ok' )
351+ } else {
352+ const preview = errors
353+ . slice ( 0 , 3 )
354+ . map ( diagnostic => formatTypeDiagnostic ( compiler , diagnostic ) )
355+ const remainder =
356+ errors . length > preview . length
357+ ? `\n...and ${ errors . length - preview . length } more`
358+ : ''
359+
360+ setTypeDiagnostics (
361+ `TypeScript found ${ errors . length } error${ errors . length === 1 ? '' : 's' } :\n${ preview . join ( '\n' ) } ${ remainder } ` ,
362+ 'error' ,
363+ )
364+ }
365+
366+ if (
367+ statusNode . textContent === 'Rendered' ||
368+ statusNode . textContent . startsWith ( 'Rendered (Type errors:' )
369+ ) {
370+ setRenderedStatus ( )
371+ }
372+ } catch ( error ) {
373+ if ( runId !== typeCheckRunId || ! typecheckToggle ?. checked ) {
374+ return
375+ }
376+
377+ lastTypeErrorCount = 0
378+ const message = error instanceof Error ? error . message : String ( error )
379+ setTypeDiagnostics ( `Type diagnostics unavailable: ${ message } ` , 'error' )
380+
381+ if ( statusNode . textContent . startsWith ( 'Rendered (Type errors:' ) ) {
382+ setStatus ( 'Rendered' )
383+ }
384+ }
385+ }
386+
387+ const markTypeDiagnosticsStale = ( ) => {
388+ if ( ! typecheckToggle ?. checked ) {
389+ return
390+ }
391+
392+ lastTypeErrorCount = 0
393+ setTypeDiagnostics ( 'Source changed. Click Check types to update diagnostics.' , 'muted' )
394+
395+ if ( statusNode . textContent . startsWith ( 'Rendered (Type errors:' ) ) {
396+ setStatus ( 'Rendered' )
397+ }
398+ }
399+
151400const appGridLayouts = [ 'default' , 'preview-right' , 'preview-left' ]
152401
153402const applyAppGridLayout = ( layout , { persist = true } = { } ) => {
@@ -253,6 +502,7 @@ const setCssSource = value => {
253502const clearComponentSource = ( ) => {
254503 setJsxSource ( '' )
255504 setStatus ( 'Component cleared' )
505+ markTypeDiagnosticsStale ( )
256506 if ( ! jsxCodeEditor ) {
257507 maybeRender ( )
258508 }
@@ -927,6 +1177,9 @@ const renderPreview = async () => {
9271177 await renderDom ( )
9281178 }
9291179 setStatus ( 'Rendered' )
1180+ if ( typecheckToggle ?. checked ) {
1181+ setRenderedStatus ( )
1182+ }
9301183 } catch ( error ) {
9311184 setStatus ( 'Error' )
9321185 const target = getRenderTarget ( )
@@ -967,6 +1220,35 @@ autoRenderToggle.addEventListener('change', () => {
9671220 renderPreview ( )
9681221 }
9691222} )
1223+ if ( typecheckToggle ) {
1224+ typecheckToggle . addEventListener ( 'change' , ( ) => {
1225+ updateTypecheckButtonState ( )
1226+
1227+ if ( typecheckToggle . checked ) {
1228+ setTypeDiagnostics (
1229+ 'Type checking enabled. Click Check types to run diagnostics.' ,
1230+ 'muted' ,
1231+ )
1232+ return
1233+ }
1234+
1235+ lastTypeErrorCount = 0
1236+ setTypeDiagnostics ( '' )
1237+ if ( statusNode . textContent . startsWith ( 'Rendered (Type errors:' ) ) {
1238+ setStatus ( 'Rendered' )
1239+ }
1240+ } )
1241+ }
1242+ if ( typecheckButton ) {
1243+ typecheckButton . addEventListener ( 'click' , ( ) => {
1244+ if ( ! typecheckToggle ?. checked ) {
1245+ return
1246+ }
1247+
1248+ typeCheckRunId += 1
1249+ void runTypeDiagnostics ( typeCheckRunId )
1250+ } )
1251+ }
9701252renderButton . addEventListener ( 'click' , renderPreview )
9711253if ( clipboardSupported ) {
9721254 copyComponentButton . addEventListener ( 'click' , ( ) => {
@@ -1002,6 +1284,7 @@ clearStylesButton.addEventListener('click', () => {
10021284 } )
10031285} )
10041286jsxEditor . addEventListener ( 'input' , maybeRender )
1287+ jsxEditor . addEventListener ( 'input' , markTypeDiagnosticsStale )
10051288cssEditor . addEventListener ( 'input' , maybeRender )
10061289
10071290for ( const button of appGridLayoutButtons ) {
@@ -1027,7 +1310,13 @@ for (const button of appThemeButtons) {
10271310applyAppGridLayout ( getInitialAppGridLayout ( ) , { persist : false } )
10281311applyTheme ( getInitialTheme ( ) , { persist : false } )
10291312
1313+ if ( typecheckToggle ) {
1314+ typecheckToggle . checked = false
1315+ }
1316+
10301317updateRenderButtonVisibility ( )
1318+ updateTypecheckButtonState ( )
1319+ setTypeDiagnostics ( '' )
10311320setStyleCompiling ( false )
10321321setCdnLoading ( true )
10331322initializePreviewBackgroundPicker ( )
0 commit comments