Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ Repository structure:

## CDN and runtime expectations

- Keep dependency loading compatible with existing provider/fallback model in src/cdn.js.
- Keep dependency loading compatible with existing provider/fallback model in src/modules/cdn.js.
- Treat src/modules/cdn.js as the source of truth for CDN-managed runtime libraries; add/update
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
18 changes: 10 additions & 8 deletions docs/build-and-deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This project uses two runtime modes:

- Local development mode: dynamic CDN resolution from `src/cdn.js` with esm.sh as default.
- Local development mode: dynamic CDN resolution from `src/modules/cdn.js` with esm.sh as default.
- Production mode: CDN-first build artifacts in `dist`, with `build:esm` as the current preferred deploy build.

## Local Development
Expand Down Expand Up @@ -45,8 +45,8 @@ npm run build:importmap-mode
| Mode | Resolver | Import map step | JSPM index needed | Typical use |
| --- | --- | --- | --- | --- |
| `importMap` | Import map in `dist/index.html` | Yes | Yes | Default production mode |
| `esm` | `src/cdn.js` (`esm.sh` primary) | No | No | Stable fallback mode |
| `jspmGa` | `src/cdn.js` (`ga.jspm.io` primary) | No | No | Direct ga.jspm.io testing |
| `esm` | `src/modules/cdn.js` (`esm.sh` primary) | No | No | Stable fallback mode |
| `jspmGa` | `src/modules/cdn.js` (`ga.jspm.io` primary) | No | No | Direct ga.jspm.io testing |
<!-- prettier-ignore-end -->

Mode notes:
Expand All @@ -72,7 +72,7 @@ This runs two steps:
- `sass=1.93.2`
- `less=4.4.2`
- Traces generated `dist/prod-imports.js`
- Import specifiers come from `importMap` entries in `src/cdn.js` (`cdnImportSpecs`)
- Import specifiers come from `importMap` entries in `src/modules/cdn.js` (`cdnImportSpecs`)

Preview the built site locally:

Expand All @@ -99,18 +99,20 @@ Related docs:

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

- `src/modules/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.

- 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`.
- In `esm` and `jspmGa` modes, runtime resolution is handled entirely by the CDN provider chain configured in `src/cdn.js` without an import map.
provider chain configured in `src/modules/cdn.js`.
- In `esm` and `jspmGa` modes, runtime resolution is handled entirely by the CDN provider chain configured in `src/modules/cdn.js` without an import map.

### Sass Loading Gotchas

- Symptom: switching to Sass mode shows `Unable to load Sass compiler for browser usage: Dynamic require of "url" is not supported`.
- Cause: some `esm.sh` Sass outputs currently include runtime paths that are not browser-safe for this app.
- Current mitigation: `src/cdn.js` keeps `esm.sh` first, then falls back to `unpkg` for Sass via `sass@1.93.2/sass.default.js?module`.
- Current mitigation: `src/modules/cdn.js` keeps `esm.sh` first, then falls back to `unpkg` for Sass via `sass@1.93.2/sass.default.js?module`.
- Important context: this can appear even if the Sass URL has not changed in this repo, because CDN-transformed module output can change upstream.
- If this regresses again:
- Verify Sass import candidates in `src/cdn.js`.
- Verify Sass import candidates in `src/modules/cdn.js`.
- Reproduce directly in browser devtools with `await import('<candidate-url>')`.
- Keep at least one known browser-safe fallback provider in the Sass candidate list.
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."
241 changes: 241 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,234 @@ test('clear all diagnostics removes style compile diagnostics', async ({ page })
/diagnostics-toggle--neutral/,
)
})

test('clear styles diagnostics removes style compile diagnostics', async ({ page }) => {
await waitForInitialRender(page)

await ensurePanelToolsVisible(page, 'styles')

await page.locator('#style-mode').selectOption('sass')
await setStylesEditorSource(page, '.card { color: $missing; }')

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

await page.locator('#diagnostics-toggle').click()
await expect(page.locator('#diagnostics-styles')).toContainText(
'Style compilation failed.',
)

await page.locator('#diagnostics-clear-styles').click()
await expect(page.locator('#diagnostics-component')).toContainText(
'No diagnostics yet.',
)
await expect(page.locator('#diagnostics-styles')).toContainText('No diagnostics yet.')
await expect(page.locator('#diagnostics-toggle')).toHaveText('Diagnostics')
await expect(page.locator('#diagnostics-toggle')).toHaveClass(
/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('clear component diagnostics resets rendered lint-issue status pill', async ({
page,
}) => {
await waitForInitialRender(page)

await setComponentEditorSource(
page,
[
'const unusedValue = 1',
'const App = () => <button type="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('#status')).toHaveClass(/status--error/)

await page.locator('#diagnostics-toggle').click()
await page.locator('#diagnostics-clear-component').click()

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

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