Skip to content

Commit 212028d

Browse files
refactor: modules and defaults. (#11)
1 parent e75dc3b commit 212028d

14 files changed

Lines changed: 162 additions & 145 deletions

playwright/app.spec.ts

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ const waitForInitialRender = async (page: Page) => {
1111
await expect(page.locator('#cdn-loading')).toHaveAttribute('hidden', '')
1212
}
1313

14+
const expectPreviewHasRenderedContent = async (page: Page) => {
15+
const previewHost = page.locator('#preview-host')
16+
await expect(previewHost.locator('pre')).toHaveCount(0)
17+
await expect
18+
.poll(() => previewHost.evaluate(node => node.childElementCount))
19+
.toBeGreaterThan(0)
20+
}
21+
1422
const setComponentEditorSource = async (page: Page, source: string) => {
1523
const editorContent = page.locator('.component-panel .cm-content').first()
1624
await editorContent.fill(source)
@@ -26,10 +34,7 @@ test('renders default playground preview', async ({ page }) => {
2634

2735
await page.getByLabel('ShadowRoot (open)').uncheck()
2836
await expect(page.locator('#status')).toHaveText('Rendered')
29-
30-
const previewItems = page.locator('#preview-host li')
31-
await expect(previewItems).toHaveCount(3)
32-
await expect(previewItems.first()).toContainText('apple')
37+
await expectPreviewHasRenderedContent(page)
3338
})
3439

3540
test('supports layout and theme toggles', async ({ page }) => {
@@ -56,10 +61,7 @@ test('renders in react mode with css modules', async ({ page }) => {
5661
await page.locator('#render-mode').selectOption('react')
5762
await page.locator('#style-mode').selectOption('module')
5863
await expect(page.locator('#status')).toHaveText('Rendered')
59-
60-
const previewItems = page.locator('#preview-host li')
61-
await expect(previewItems).toHaveCount(3)
62-
await expect(previewItems.first()).toContainText('apple')
64+
await expectPreviewHasRenderedContent(page)
6365
})
6466

6567
test('transpiles TypeScript annotations in component source', async ({ page }) => {
@@ -156,10 +158,7 @@ test('renders with less style mode', async ({ page }) => {
156158
await expect(page.locator('#style-warning')).toContainText(
157159
'Less is compiled in-browser via @knighted/css/browser.',
158160
)
159-
160-
const previewItems = page.locator('#preview-host li')
161-
await expect(previewItems).toHaveCount(3)
162-
await expect(previewItems.first()).toContainText('apple')
161+
await expectPreviewHasRenderedContent(page)
163162
})
164163

165164
test('renders with sass style mode', async ({ page }) => {
@@ -171,10 +170,7 @@ test('renders with sass style mode', async ({ page }) => {
171170
await expect(page.locator('#style-warning')).toContainText(
172171
'Sass is compiled in-browser via @knighted/css/browser.',
173172
)
174-
175-
const previewItems = page.locator('#preview-host li')
176-
await expect(previewItems).toHaveCount(3)
177-
await expect(previewItems.first()).toContainText('apple')
173+
await expectPreviewHasRenderedContent(page)
178174
})
179175

180176
test('style compilation errors populate styles diagnostics scope', async ({ page }) => {

scripts/build-prepare.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
22
import { dirname, resolve } from 'node:path'
33
import { fileURLToPath } from 'node:url'
4-
import { cdnImportSpecs } from '../src/cdn.js'
4+
import { cdnImportSpecs } from '../src/modules/cdn.js'
55

66
const validPrimaryCdns = new Set(['importMap', 'esm', 'jspmGa'])
77

@@ -49,12 +49,14 @@ const createProdImportsModule = async () => {
4949
]
5050

5151
if (specifiers.length === 0) {
52-
throw new Error('No importMap specifiers found in src/cdn.js (cdnImportSpecs).')
52+
throw new Error(
53+
'No importMap specifiers found in src/modules/cdn.js (cdnImportSpecs).',
54+
)
5355
}
5456

5557
const lines = [
5658
'/*',
57-
' * Generated by scripts/build-prepare.js from src/cdn.js (cdnImportSpecs).',
59+
' * Generated by scripts/build-prepare.js from src/modules/cdn.js (cdnImportSpecs).',
5860
' * JSPM links this module to trace top-level production imports.',
5961
' */',
6062
...specifiers.map(specifier => `import '${specifier}'`),

src/app.js

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
import { cdnImports, getTypeScriptLibUrls, importFromCdnWithFallback } from './cdn.js'
2-
import { createCodeMirrorEditor } from './editor-codemirror.js'
3-
import { defaultCss, defaultJsx } from './defaults.js'
4-
import { createDiagnosticsUiController } from './diagnostics-ui.js'
5-
import { createLayoutThemeController } from './layout-theme.js'
6-
import { createPreviewBackgroundController } from './preview-background.js'
7-
import { createRenderRuntimeController } from './render-runtime.js'
8-
import { createTypeDiagnosticsController } from './type-diagnostics.js'
1+
import {
2+
cdnImports,
3+
getTypeScriptLibUrls,
4+
importFromCdnWithFallback,
5+
} from './modules/cdn.js'
6+
import { createCodeMirrorEditor } from './modules/editor-codemirror.js'
7+
import { defaultCss, defaultJsx, defaultReactJsx } from './modules/defaults.js'
8+
import { createDiagnosticsUiController } from './modules/diagnostics-ui.js'
9+
import { createLayoutThemeController } from './modules/layout-theme.js'
10+
import { createPreviewBackgroundController } from './modules/preview-background.js'
11+
import { createRenderRuntimeController } from './modules/render-runtime.js'
12+
import { createTypeDiagnosticsController } from './modules/type-diagnostics.js'
913

1014
const statusNode = document.getElementById('status')
1115
const appGrid = document.querySelector('.app-grid')
@@ -48,6 +52,7 @@ let getCssSource = () => cssEditor.value
4852
let renderRuntime = null
4953
let pendingClearAction = null
5054
let suppressEditorChangeSideEffects = false
55+
let hasAppliedReactModeDefault = false
5156
const clipboardSupported = Boolean(navigator.clipboard?.writeText)
5257

5358
const previewBackground = createPreviewBackgroundController({
@@ -346,7 +351,15 @@ const updateRenderButtonVisibility = () => {
346351
renderButton.hidden = autoRenderToggle.checked
347352
}
348353

349-
renderMode.addEventListener('change', maybeRender)
354+
renderMode.addEventListener('change', () => {
355+
if (renderMode.value === 'react' && !hasAppliedReactModeDefault) {
356+
hasAppliedReactModeDefault = true
357+
setJsxSource(defaultReactJsx)
358+
markTypeDiagnosticsStale()
359+
}
360+
361+
maybeRender()
362+
})
350363
styleMode.addEventListener('change', () => {
351364
if (cssCodeEditor) {
352365
cssCodeEditor.setLanguage(getStyleEditorLanguage(styleMode.value))

src/bootstrap.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { getPrimaryCdnImportUrls } from './cdn.js'
1+
import { getPrimaryCdnImportUrls } from './modules/cdn.js'
22

33
/*
44
* Preload only the modules needed for the initial render path.
55
* - Included: core runtime + React runtime modules used immediately.
66
* - Excluded: optional style compilers (sass/less/lightningCssWasm), which stay lazy.
7-
* Keep this list aligned with cdnImports keys in cdn.js.
7+
* Keep this list aligned with cdnImports keys in modules/cdn.js.
88
*/
99
const preloadImportKeys = [
1010
'cssBrowser',

src/defaults.js

Lines changed: 0 additions & 114 deletions
This file was deleted.
File renamed without changes.

src/modules/defaults.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
export const defaultJsx = [
2+
'type CounterButtonProps = {',
3+
' label: string',
4+
' onClick: (event: MouseEvent) => void',
5+
'}',
6+
'',
7+
'const CounterButton = ({ label, onClick }: CounterButtonProps) => (',
8+
' <button id="counter-button" type="button" onClick={onClick}>',
9+
' {label}',
10+
' </button>',
11+
')',
12+
'',
13+
'const App = () => {',
14+
' let count = 0',
15+
' const handleClick = (event: MouseEvent) => {',
16+
' count += 1',
17+
' const button = event.currentTarget as HTMLButtonElement',
18+
' button.textContent = `Clicks: ${count}`',
19+
" button.dataset.active = count % 2 === 0 ? 'false' : 'true'",
20+
" button.classList.toggle('is-even', count % 2 === 0)",
21+
' }',
22+
'',
23+
" return <CounterButton label='Clicks: 0' onClick={handleClick} />",
24+
'}',
25+
'',
26+
].join('\n')
27+
28+
export const defaultReactJsx = [
29+
'type CounterButtonProps = {',
30+
' label: string',
31+
' active: boolean',
32+
' onClick: (event: MouseEvent) => void',
33+
'}',
34+
'',
35+
'const CounterButton = ({ label, active, onClick }: CounterButtonProps) => (',
36+
' <button',
37+
' id="counter-button"',
38+
' type="button"',
39+
' data-active={active ? "true" : "false"}',
40+
' className={active ? "is-even" : ""}',
41+
' onClick={onClick}',
42+
' >',
43+
' {label}',
44+
' </button>',
45+
')',
46+
'',
47+
'const App = () => {',
48+
' const { useState } = React',
49+
' const [count, setCount] = useState(0)',
50+
' const handleClick = (_event: MouseEvent) => {',
51+
' setCount(current => current + 1)',
52+
' }',
53+
'',
54+
' return (',
55+
' <CounterButton',
56+
' label={`React clicks: ${count}`}',
57+
' active={count % 2 === 0}',
58+
' onClick={handleClick}',
59+
' />',
60+
' )',
61+
'}',
62+
'',
63+
].join('\n')
64+
65+
export const defaultCss = `#counter-button {
66+
margin: 0;
67+
padding: 0.75rem 1rem;
68+
border: 1px solid #3558b8;
69+
border-radius: 0.5rem;
70+
background: #e9efff;
71+
color: #1a2a52;
72+
font-weight: 600;
73+
cursor: pointer;
74+
transition: background-color 120ms ease;
75+
}
76+
77+
#counter-button:hover {
78+
background: #dce6ff;
79+
}
80+
81+
#counter-button[data-active='true'] {
82+
background: #3558b8;
83+
color: #fff;
84+
}
85+
86+
#counter-button.is-even {
87+
border-style: dashed;
88+
}
89+
90+
#counter-button:focus-visible {
91+
outline: 2px solid #6a84d8;
92+
outline-offset: 2px;
93+
}
94+
`
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ export const createDiagnosticsUiController = ({
5555
return 'pending'
5656
}
5757

58+
if (diagnosticsByScope.component.level === 'ok') {
59+
return 'ok'
60+
}
61+
5862
return 'neutral'
5963
}
6064

@@ -67,6 +71,7 @@ export const createDiagnosticsUiController = ({
6771
if (diagnosticsToggle) {
6872
diagnosticsToggle.classList.remove(
6973
'diagnostics-toggle--neutral',
74+
'diagnostics-toggle--ok',
7075
'diagnostics-toggle--pending',
7176
'diagnostics-toggle--error',
7277
)
@@ -108,7 +113,10 @@ export const createDiagnosticsUiController = ({
108113

109114
if (hasHeadline) {
110115
const headingNode = document.createElement('div')
111-
headingNode.className = 'type-diagnostics-heading'
116+
headingNode.className =
117+
state.level === 'ok'
118+
? 'type-diagnostics-heading type-diagnostics-heading--ok'
119+
: 'type-diagnostics-heading'
112120
headingNode.textContent = state.headline
113121
root.append(headingNode)
114122
}

0 commit comments

Comments
 (0)