Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ Repository structure:
## CDN and runtime expectations

- Keep dependency loading compatible with existing provider/fallback model in src/cdn.js.
- Treat src/cdn.js as the source of truth for CDN-managed runtime libraries; add/update
Comment thread
knightedcodemonkey marked this conversation as resolved.
Outdated
CDN candidates there instead of hardcoding module URLs in feature modules.
- Prefer extending existing CDN import key patterns instead of ad hoc dynamic imports.
- Maintain graceful fallback behavior when CDN modules fail to load.
- Keep the app usable in local dev without requiring a local bundle step.
Expand Down
2 changes: 2 additions & 0 deletions docs/build-and-deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ Related docs:

- `docs/code-mirror.md` for CodeMirror CDN integration rules, fallback behavior, and validation checklist.

- `src/cdn.js` is the source of truth for CDN-managed runtime libraries (including fallback candidates). Add/update CDN specs there instead of hardcoding module URLs inside feature modules.

Comment thread
knightedcodemonkey marked this conversation as resolved.
Outdated
- In production, the current preferred deploy mode is ESM resolution (`window.__KNIGHTED_PRIMARY_CDN__ = "esm"`).
- In `importMap` mode, runtime resolution is import-map first; if a specifier is missing from the generated map, runtime falls back through the CDN
provider chain configured in `src/cdn.js`.
Expand Down
23 changes: 20 additions & 3 deletions docs/next-steps.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@

Focused follow-up work for `@knighted/develop`.

1. **In-browser component/style linting**
- Explore running lint checks for component and style sources directly in the playground.
- Prefer CDN-delivered tooling where possible and preserve graceful fallback behavior when unavailable.
1. **In-browser lint rules review and expansion**
- Review the currently active Biome lint configuration in `src/modules/lint-diagnostics.js`, including rule groups, severities, and any custom suppression behavior.
- Produce a recommended rule profile for component and style linting that balances signal quality with playground ergonomics.
- Evaluate additional Biome rules to enable (or elevate severity) for:
- correctness and suspicious patterns in component code,
- accessibility and style consistency in JSX output,
- CSS quality checks for style sources currently supported by Biome.
- Revisit existing exceptions (for example unused App/View/render bindings) and document clear criteria for when suppression is acceptable.
- Add/update regression coverage for the chosen rule profile in Playwright so diagnostics button/drawer behavior remains stable as rules evolve.
- Document the finalized lint rule strategy in project docs so contributors can reason about why each rule is enabled, disabled, or downgraded.
- Suggested implementation prompt:
- "Audit the current Biome lint rules used by `@knighted/develop`, propose and apply a refined rule profile for component/styles linting, and add/update Playwright coverage to keep diagnostics UX stable under the new rules. Preserve intentional suppressions only when justified and document the reasoning. Validate with `npm run lint`, `npm run build:esm`, and targeted lint diagnostics Playwright tests."

2. **In-browser component type checking**
- Add editor-linked diagnostics navigation so each issue can jump to the exact line/column in the component source.
Expand Down Expand Up @@ -39,3 +48,11 @@ Focused follow-up work for `@knighted/develop`.
- DOM mode still avoids React type graph hydration.
- Suggested implementation prompt:
- "Refactor `src/modules/type-diagnostics.js` to make TypeScript preprocessor parsing (`preProcessFile`) the source of truth for declaration graph discovery in the lazy React type loader. Keep current CDN fallback and lazy hydration semantics. Ensure references from comments are ignored, `*.d.ts`/relative path handling is correct, and candidate fetch ordering minimizes noisy failed requests. Add regression coverage for `global.d.ts` and commented `./user-context` examples. Validate with `npm run lint`, `npm run build:esm`, and targeted React/typecheck Playwright runs."

6. **Deterministic E2E lane in CI**
- Add an integration-style E2E path that uses locally served/pinned copies of CDN runtime dependencies for test execution, while keeping production runtime behavior unchanged.
- Keep the current true CDN-backed E2E path as a separate smoke check, but make the deterministic lane the required gate for pull requests.
- Run this deterministic E2E suite on **every pull request** in CI.
- Ensure the deterministic lane still exercises the same user-facing flows (render, typecheck, lint, diagnostics drawer/button states), only swapping the source of runtime artifacts.
- Suggested implementation prompt:
- "Add a deterministic E2E execution mode for `@knighted/develop` that serves pinned runtime artifacts locally (instead of live CDN fetches) and wire it into CI as a required check on every PR. Keep a separate lightweight CDN-smoke E2E check for real-network coverage. Validate with `npm run lint`, deterministic Playwright PR checks, and one CDN-smoke Playwright run."
181 changes: 181 additions & 0 deletions playwright/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ const setStylesEditorSource = async (page: Page, source: string) => {
await editorContent.fill(source)
}

const runTypecheck = async (page: Page) => {
await ensurePanelToolsVisible(page, 'component')
await page.locator('#typecheck-button').click()
}

const runComponentLint = async (page: Page) => {
await ensurePanelToolsVisible(page, 'component')
await page.locator('#lint-component-button').click()
}

const getCollapseButton = (page: Page, panelName: 'component' | 'styles' | 'preview') =>
page.locator(`#collapse-${panelName}`)

Expand Down Expand Up @@ -647,3 +657,174 @@ test('clear all diagnostics removes style compile diagnostics', async ({ page })
/diagnostics-toggle--neutral/,
)
})

test('typecheck success reports ok diagnostics state in button and drawer', async ({
page,
}) => {
await waitForInitialRender(page)

await runTypecheck(page)

await expect(page.locator('#status')).toHaveText('Rendered')
await expect(page.locator('#diagnostics-toggle')).toHaveClass(/diagnostics-toggle--ok/)
await expect(page.locator('#diagnostics-toggle')).toHaveText('Diagnostics')

await page.locator('#diagnostics-toggle').click()
await expect(page.locator('#diagnostics-component')).toContainText(
'No TypeScript errors found.',
)
})

test('typecheck error reports diagnostics count in button and details in drawer', async ({
page,
}) => {
await waitForInitialRender(page)

await setComponentEditorSource(
page,
["const broken: number = 'oops'", 'const App = () => <button>hello</button>'].join(
'\n',
),
)

await runTypecheck(page)

await expect(page.locator('#status')).toHaveText(/Rendered \(Type errors: [1-9]\d*\)/)
await expect(page.locator('#diagnostics-toggle')).toHaveClass(
/diagnostics-toggle--error/,
)
await expect(page.locator('#diagnostics-toggle')).toHaveText(/Diagnostics \([1-9]\d*\)/)

await page.locator('#diagnostics-toggle').click()
await expect(page.locator('#diagnostics-component')).toContainText('TypeScript found')
await expect(page.locator('#diagnostics-component')).toContainText('TS')
})

test('component lint error reports diagnostics count and details', async ({ page }) => {
await waitForInitialRender(page)

await setComponentEditorSource(
page,
['const unusedValue = 1', 'const App = () => <button>lint me</button>'].join('\n'),
)

await runComponentLint(page)

await expect(page.locator('#status')).toHaveText(/Rendered \(Lint issues: [1-9]\d*\)/)
await expect(page.locator('#diagnostics-toggle')).toHaveClass(
/diagnostics-toggle--error/,
)
await expect(page.locator('#diagnostics-toggle')).toHaveText(/Diagnostics \([1-9]\d*\)/)

await page.locator('#diagnostics-toggle').click()
await expect(page.locator('#diagnostics-component')).toContainText(
'Biome reported issues.',
)
})

test('component lint ignores unused App View and render bindings', async ({ page }) => {
await waitForInitialRender(page)

await setComponentEditorSource(
page,
[
'function App() { return <button type="button">App</button> }',
'function View() { return <section>View</section> }',
'function render() { return null }',
].join('\n'),
)

await runComponentLint(page)

await page.locator('#diagnostics-toggle').click()
await expect(page.locator('#diagnostics-component')).toContainText(
'No Biome issues found.',
)

await expect(page.locator('#status')).toHaveText('Rendered')
await expect(page.locator('#status')).toHaveClass(/status--neutral/)
await expect(page.locator('#diagnostics-toggle')).toHaveText('Diagnostics')
await expect(page.locator('#diagnostics-toggle')).toHaveClass(/diagnostics-toggle--ok/)

const diagnosticsText = await page.locator('#diagnostics-component').innerText()
expect(diagnosticsText).not.toContain('This variable App is unused')
expect(diagnosticsText).not.toContain('This variable View is unused')
expect(diagnosticsText).not.toContain('This variable render is unused')
expect(diagnosticsText).not.toContain('This function App is unused')
expect(diagnosticsText).not.toContain('This function View is unused')
expect(diagnosticsText).not.toContain('This function render is unused')
})

test('component lint with unresolved issues enters pending diagnostics state while typing', async ({
page,
}) => {
await waitForInitialRender(page)

await setComponentEditorSource(
page,
['const unusedValue = 1', 'const App = () => <button>pending</button>'].join('\n'),
)

await runComponentLint(page)

await expect(page.locator('#diagnostics-toggle')).toHaveClass(
/diagnostics-toggle--error/,
)

await setComponentEditorSource(
page,
['const unusedValue = 1', 'const App = () => <button>pending now</button>'].join(
'\n',
),
)

await expect(page.locator('#diagnostics-toggle')).toHaveClass(
/diagnostics-toggle--pending/,
)
await expect(page.locator('#diagnostics-toggle')).toHaveAttribute('aria-busy', 'true')

await expect(page.locator('#status')).toHaveText(/Rendered \(Lint issues: [1-9]\d*\)/)
await expect(page.locator('#diagnostics-toggle')).toHaveClass(
/diagnostics-toggle--error/,
)
await expect(page.locator('#diagnostics-toggle')).toHaveAttribute('aria-busy', 'false')
})

test('changing css dialect resets diagnostics after lint and typecheck runs', async ({
page,
}) => {
await waitForInitialRender(page)
await ensurePanelToolsVisible(page, 'styles')

await setComponentEditorSource(
page,
[
"const broken: number = 'oops'",
'const unusedValue = 1',
'const App = () => <button>reset me</button>',
].join('\n'),
)

await runTypecheck(page)
await runComponentLint(page)

await expect(page.locator('#diagnostics-toggle')).toHaveClass(
/diagnostics-toggle--error/,
)
await expect(page.locator('#diagnostics-toggle')).toHaveText(/Diagnostics \([1-9]\d*\)/)

await page.locator('#style-mode').selectOption('less')

await expect(page.locator('#status')).toHaveText('Rendered')
await expect(page.locator('#status')).toHaveClass(/status--neutral/)
await expect(page.locator('#diagnostics-toggle')).toHaveClass(
/diagnostics-toggle--neutral/,
)
await expect(page.locator('#diagnostics-toggle')).toHaveText('Diagnostics')

await page.locator('#diagnostics-toggle').click()
await expect(page.locator('#diagnostics-component')).toContainText(
'No diagnostics yet.',
)
await expect(page.locator('#diagnostics-styles')).toContainText('No diagnostics yet.')
})
Loading
Loading