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
121 changes: 77 additions & 44 deletions docs/article.md
Original file line number Diff line number Diff line change
@@ -1,90 +1,123 @@
# Forget The Build Step: A Browser-Native Editor For JSX + CSS
---
title: 'Forget The Build Step: A Browser-Native Editor For JSX + CSS'
published: true
description: '@knighted/develop is a browser-native editor for JSX and CSS — compile, lint, typecheck, and sync to GitHub without a local build step.'
tags: javascript, webdev, showdev, react
---

Frontend tooling is incredibly capable.

It is also often front-loaded.

For many UI ideas, the first thing you do is not write code. You install,
configure, and wait. The creative loop starts late.
For many UI ideas, the first thing you do is not write code. You install, configure, and wait. The creative loop starts late.

[@knighted/develop](https://github.com/knightedcodemonkey/develop) is built
for a different default: fast prototyping from anywhere you can open a browser.
[@knighted/develop](https://github.com/knightedcodemonkey/develop) is built for a different default: fast prototyping from anywhere you can open a browser.

It is a browser-native editor/workbench for
[@knighted/jsx](https://github.com/knightedcodemonkey/jsx) and
[@knighted/css](https://github.com/knightedcodemonkey/css), delivered through
CDN ESM with mode-aware loading.
It is a browser-native editor/workbench for [@knighted/jsx](https://github.com/knightedcodemonkey/jsx) and [@knighted/css](https://github.com/knightedcodemonkey/css), delivered through CDN ESM with mode-aware loading.

## The Loop, In Practice

Open the app, edit multiple files in dynamic tabs, switch render/style modes,
run lint/type diagnostics, and preview instantly.
Open the app, spin up isolated workspaces, edit multiple files in dynamic tabs, switch render/style modes, run lint/type diagnostics, and preview instantly.

No local bundler is required for that inner loop.

In DOM mode, JSX expressions resolve to real DOM nodes — no virtual DOM, no diffing, no reconciler. Tabs are standard ESM modules too, so your entry tab can import from sibling tabs with relative paths:

`App.tsx`

```tsx
import { Counter } from './Counter.js'

export const App = () => (
<main>
<Counter label="Clicks" />
</main>
)
```

`Counter.tsx`

```tsx
import '../styles/app.css'

type CounterProps = {
label: string
}

export const Counter = ({ label }: CounterProps) => {
const el = (
<button class="counter-button" type="button">
{label}: 0
</button>
) as HTMLButtonElement
let count = 0

el.onclick = () => {
count += 1
el.textContent = `${label}: ${count}`
el.classList.toggle('is-even', count % 2 === 0)
}

return el
}
```

## What The App Gives You

- Dynamic tabbed editing (add, rename, remove, and protect required entry tabs)
- Render mode switch: DOM or React
- Style mode switch: CSS, CSS Modules, Less, Sass
- Live preview with iframe-isolated style encapsulation
- In-browser lint and type diagnostics with jump-to-source navigation
- GitHub-connected workflows for Open PR and Push Commit
- AI chat with tab-aware edit proposals and explicit apply/undo controls
- **Multiple Workspaces:** Create and switch between isolated projects without losing your current files.
- **Dynamic Tabbed Editing:** Add, rename, remove, and protect required entry tabs within any workspace.
- **Instant Share URLs:** Encode the current workspace state into a URL you can share or bookmark.
- **Direct GitHub Synchronization:** Open pull requests and push commits from the browser.
- **Render Mode Switch:** Toggle instantly between DOM or React runtimes.
- **Style Mode Switch:** Support for CSS, CSS Modules, Less, and Sass.
- **Live Preview:** Real-time updates with iframe-isolated style encapsulation.
- **In-Browser Diagnostics:** Full lint and type diagnostics with jump-to-source navigation.
- **AI Integration:** Chat with tab-aware edit proposals and explicit apply/undo controls.

This is not only "can this compile?" It is about shipping the whole iteration
loop in one place: edit, validate, preview, sync, and refine.
The goal is a complete iteration loop without switching tools: edit, check, preview, and sync.

## Why `@knighted/jsx` + `@knighted/css` Matter Here

The app demonstrates both libraries in realistic authoring conditions:

- `@knighted/jsx` provides a direct path from JSX to rendered output,
including DOM-first workflows.
- `@knighted/css` handles modern browser-side style compilation,
including Modules/Less/Sass modes.
- `@knighted/jsx` provides a direct path from JSX to rendered output, including DOM-first workflows.
- `@knighted/css` handles modern browser-side style compilation, including Modules/Less/Sass modes.

Together they show how much of the authoring cycle modern browsers can run
directly.
Together they show what a browser can handle natively when you stop routing everything through a local build tool.

## "Compiler-as-a-Service" Without A Build Farm

In this project, Compiler-as-a-Service means:

- CDN delivers modules and WASM artifacts.
- The browser session performs compile, lint, typecheck, render, and editor
interactions locally.
- The browser session performs compile, lint, typecheck, render, and editor interactions locally.

It is service-oriented distribution with local execution.

Mode-aware loading keeps costs aligned to usage: if you do not use Sass,
Sass does not load.
It is service-oriented distribution with local execution. Mode-aware loading means you only download what you use: skip Sass mode and the Sass bundle never loads.

## Why This Matters

This does not replace production pipelines.

It lowers the cost of exploration while preserving enough workflow surface to
be useful for real component work.
This does not replace production pipelines — it removes the setup cost for exploratory work where a full project scaffold is overkill.

When setup friction drops, teams try more ideas. When feedback is immediate,
they converge faster. When browser-native workspaces can sync to GitHub and
carry chat-assisted edit proposals, collaboration is lighter too.
The loop is complete enough for real component work: write, render, diagnose, push. If you can share a URL or open a PR from the same tab you are editing in, the feedback cycle gets shorter.

For prototyping and component development, that is a meaningful shift.
For prototyping and focused component work, that is worth something.

## Try It

> **Note:** The app loads its compiler and runtime from CDN on first visit. If something fails to initialize, a hard reload (`Cmd/Ctrl + Shift + R`) is usually enough to recover.

- Live workbench: https://knightedcodemonkey.github.io/develop/
- Source: https://github.com/knightedcodemonkey/develop
- Worker DOM snapshot (Share URL): [Open workspace example](https://knightedcodemonkey.github.io/develop/?sws=H4sIAAAAAAAAE4VWYW_bNhD9Kwd2m-1VkqXUdVM5SpumHVYsWYskxT7UxUJTJ5kJRQokbdc1_N8HklacpN1mwLZ4vHs8vnt39oYsURuuJMmziDDVtBpNWJP6G29JRJhGarE8sSTPXhw-H2ejLB2n6bOIGElbM1eW5BvCS5KTlfm7epaVmLGX8fj5GOMRKw_il-noMD4o8RDT6vBFOUYSEbNoURsssXzvAklEVkrfmpYyvGSqRZIToRgV9zf-wHVnzvMKqY1dvkqitHFZ1i9iJkaGRERjqwLmjBqH1FAuSUTmSN1ZLnL448hW_7loZqhJLhdCuPUVt8JB_KX0LWp4--Hcu50qafGrvbTUul0uKbN8if5wWaI-V6Wzl6ohEamUtKfGfNKC5GRubWvy4dAZTVIrVQukLTcJU82QGXPwqqINF-vivbSoc26piFRrvkWrem5fp1E2SpJnB1GWpknyMk0n2WPLL7v4CzVTVgWALvZeUOddctMKui7MirpiWzozJP-8qydKq9ckIpI27jonbZtY89UxQO2c5MRotmfSDPf7gsp6QWsXdEOX1DDNWxvf-D2tPKMdNjcngbvc6gVGxDCthCB56uQoLUpLcvdkLJy0LRTQH0BxDJupBAhmphaOq3cCCjgq-RKMXQsspp742PBvmMNzjc0EvGGFvJ7bHGZKlJMpOc7S9GhY8uUxUAO_X52fveXLdwIblHayP2TlBXCqSoQCrp0dQKAFY6m2V7xx5hZ1pXRDJcNEqlV_MNn7Wc5uDRSQ7mzVQjLLlfQb_UG4j3t5x6dPJ93aoKiSVhl7jsbQGvtZmkIc3AZ3XryCfjjiCLI0vYfnEHyCamG9S-QcHES__13CEO8v5BY7zF99yGCwP28bHnZf4Q5-8_o7zqAAiSsIHdT_dHGWhInyYXaDzH66OOu77TdCzfqf9yx_iWADdt1iDj3atoIz6uga7uXUg23IyB0YAhMlm8CS0wnuhQJ7kSSucU-DsqCAS6u5rPuYlNTS7n6OzWCBowLSQQdvUTdcUou7y279p3trtAstH8pv11p5JfDrBG4WxvJqHe9EnTN0-UxgRtltrdVClvmT7CAbZWwCTAml8ydVVU1gHuSapelyPgEqeC1jbrExHcKUHG_ubrcNUp5M5Xbi21nXaD_q37jAj__Ts9y85dqu7_pwLVmY-mEWCmrspbddYKMsXs5ptxVcT--6lTet0hZ6STL0XJghbduEGdNzxXJFhdOQ8JuFtUp-1Kp1veFLJegMRQ7G18UZlDwVnN3m0MelIw7O1cLgO_fsC7xUvJzKrYMOsnuA7YSwCaBRBwXb_AcJeLC-O_FoFkKZoMYUU7JjNw7mKfG6LKbkbr3DLTa7h-1x0NHGn-ub5GgYvI-ncrBP9buJ5kaFP86Nin0nzaksBYbki38j4r7S4WkBWbd2CLOODR-bsIXWKO2V10c3-AIZu9kXgkPYo5659omYHH4KwtteP3B2fWPQJuE3EYpdRj_DARRFASm8gl5FhcEe5NBzaus9iPe0n3FjE6vqWmC_x03s0u5Fj6EGvgvDCOha8GH5fQWKXpdx2tsX6x6pWxgeew2RIPRzVfKKY_noL8_2S0TCra7o7P39X8jHf5LS8Xh0cHh4-N9w238AZi93_38JAAA)

If you want a fast product tour, try this sequence:

1. Add a new tab, rename it, and make an edit.
1. Spin up a new workspace or add a new file tab, rename it, and make an edit.
2. Toggle DOM -> React render mode.
3. Toggle CSS -> Modules -> Less -> Sass style mode.
4. Open diagnostics and jump to a reported line.
5. Connect BYOT (Bring Your Own Token) by adding a GitHub personal access token, then run Open PR / Push Commit.
6. Ask chat for a targeted tab update, then apply it.
5. Copy a Share URL to send your current workspace state to someone else.
6. Connect your GitHub personal access token (BYOT) to run an Open PR or Push Commit.
7. Ask chat for a targeted tab update, then apply it.

That flow tells the product story better than any architecture diagram.
That covers most of what the app does.
49 changes: 49 additions & 0 deletions playwright/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
name: knighted-develop-playwright-agent
description: Focused guidance for Playwright E2E authoring and triage in @knighted/develop.
---

You are working in @knighted/develop Playwright E2E tests. Keep feedback loops short and avoid full-suite reruns unless explicitly requested.

## Scope

- Folder: playwright/
- Focus: test behavior, selectors, test stability, and fixture/setup correctness
- Keep changes minimal and localized to the failing behavior

## Fast Failure Loop

- Start with one browser at a time: Chromium first.
- Run one spec file before running broader groups.
- When possible, run only the failing test name(s).
- Do not run full Playwright shards locally unless explicitly requested.

## Flake Triage

- Check failure output for network/CDN/API timing flakes first.
- Retry flaky failures once.
- If the same assertion fails again, treat it as deterministic and fix code/tests.
- Prefer fixing stale expectations when product behavior intentionally changed.

## Test Authoring Rules

- Prefer semantic selectors: getByRole, getByLabel, getByText.
- Use explicit accessible names for interactive controls.
- Use locator() only when semantic selectors are not reliable.
- For known WebKit dialog issues, prefer a stable dialog id and evaluate-based click for dialog confirmation controls.

## PR and Workspace Assertions

- When asserting Git payloads, prefer verifying exact file paths/content over fragile counts when defaults may evolve.
- Keep assertions aligned with default workspace/tab contracts.

## Validation Commands

- Lint after JS/TS edits: npm run lint
- For Playwright changes, prefer targeted execution first.

## Boundaries

- Do not change build/import-map scripts unless required by the test task.
- Do not broaden CI scope or shard counts unless explicitly requested.
- Do not modify generated outputs or lockfiles unless explicitly requested.
30 changes: 27 additions & 3 deletions playwright/github-pr-drawer/open-pr-create.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
waitForAppReady,
} from './github-pr-drawer.helpers.js'

test('Open PR drawer confirms and submits component/styles filepaths', async ({
test('Open PR drawer confirms and submits default workspace filepaths', async ({
page,
}) => {
const customCommitMessage = 'chore: sync develop editor outputs'
Expand Down Expand Up @@ -191,7 +191,19 @@ test('Open PR drawer confirms and submits component/styles filepaths', async ({
expect(createdRefPayload?.ref).toBe('refs/heads/Develop/Open-Pr-Test')
expect(createdRefPayload?.sha).toBe('abc123mainsha')
expect(treeRequests).toHaveLength(1)
expect((treeRequests[0]?.tree as Array<Record<string, unknown>>)?.length).toBe(2)
const submittedTree = treeRequests[0]?.tree
expect(Array.isArray(submittedTree)).toBe(true)
const submittedPaths = (submittedTree as Array<Record<string, unknown>>).map(
entry => entry.path,
)
expect(submittedPaths).toHaveLength(3)
expect(submittedPaths).toEqual(
expect.arrayContaining([
'src/components/App.tsx',
'src/components/Counter.tsx',
'src/styles/app.css',
]),
)
expect(commitRequests).toHaveLength(1)
expect(commitRequests[0]?.message).toBe(customCommitMessage)
expect(updateRefRequests).toHaveLength(1)
Expand Down Expand Up @@ -2083,7 +2095,19 @@ test('Open PR drawer uses Git Database API atomic commit path by default', async
)

expect(treeRequests).toHaveLength(1)
expect((treeRequests[0]?.tree as Array<Record<string, unknown>>)?.length).toBe(2)
const submittedTree = treeRequests[0]?.tree
expect(Array.isArray(submittedTree)).toBe(true)
const submittedPaths = (submittedTree as Array<Record<string, unknown>>).map(
entry => entry.path,
)
expect(submittedPaths).toHaveLength(3)
expect(submittedPaths).toEqual(
expect.arrayContaining([
'src/components/App.tsx',
'src/components/Counter.tsx',
'src/styles/app.css',
]),
)
expect(commitRequests).toHaveLength(1)
expect(updateRefRequests).toHaveLength(1)
expect(updateRefRequests[0]?.sha).toBe('new-commit-sha')
Expand Down
13 changes: 12 additions & 1 deletion src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { createLayoutDiagnosticsSetup } from './modules/app-core/layout-diagnost
import { createWorkspaceControllersSetup } from './modules/app-core/workspace-controllers-setup.js'
import { createWorkspaceScopeForkActions } from './modules/app-core/workspace-scope-fork-actions.js'
import { createGitHubWorkflowsSetup } from './modules/app-core/github-workflows-setup.js'
import { defaultCss, defaultJsx } from './modules/app-core/defaults.js'
import { defaultCss, defaultJsx, defaultModuleJsx } from './modules/app-core/defaults.js'
import { createGitHubPrContextUiController } from './modules/app-core/github-pr-context-ui.js'
import { createGitHubTokenInfoUiController } from './modules/app-core/github-token-info-ui.js'
import {
Expand Down Expand Up @@ -227,8 +227,10 @@ const clearConfirmCopy = document.getElementById('clear-confirm-copy')
const clearConfirmButton = clearConfirmDialog?.querySelector('button[value="confirm"]')

const defaultComponentTabPath = 'src/components/App.tsx'
const defaultModuleTabPath = 'src/components/Counter.tsx'
const defaultStylesTabPath = 'src/styles/app.css'
const defaultComponentTabName = 'App.tsx'
const defaultModuleTabName = 'Counter.tsx'
const defaultStylesTabName = 'app.css'
const editorKinds = ['component', 'styles']
const editorPanelsByKind = {
Expand Down Expand Up @@ -279,6 +281,15 @@ const workspaceTabsState = createWorkspaceTabsState({
isActive: true,
content: defaultJsx,
},
{
id: 'counter',
name: defaultModuleTabName,
path: defaultModuleTabPath,
language: 'javascript-jsx',
role: 'module',
isActive: false,
content: defaultModuleJsx,
},
{
id: 'styles',
name: defaultStylesTabName,
Expand Down
39 changes: 24 additions & 15 deletions src/modules/app-core/defaults.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
export const defaultJsx = [
"import { Counter } from './Counter.js'",
'',
'export const App = () => (',
' <main>',
" <Counter label='Clicks' />",
' </main>',
')',
'',
].join('\n')

export const defaultModuleJsx = [
"import '../styles/app.css'",
'',
'type CounterButtonProps = {',
'type CounterProps = {',
' label: string',
' onClick: (event: MouseEvent) => void',
'}',
'',
'const CounterButton = ({ label, onClick }: CounterButtonProps) => (',
' <button class="counter-button" type="button" onClick={onClick}>',
' {label}',
' </button>',
')',
'',
'const App = () => {',
'export const Counter = ({ label }: CounterProps) => {',
' const el = (',
" <button class='counter-button' type='button'>",
' {label}: 0',
' </button>',
' ) as HTMLButtonElement',
' let count = 0',
' const handleClick = (event: MouseEvent) => {',
'',
' el.onclick = () => {',
' count += 1',
' const button = event.currentTarget as HTMLButtonElement',
' button.textContent = `Clicks: ${count}`',
" button.dataset.active = count % 2 === 0 ? 'false' : 'true'",
" button.classList.toggle('is-even', count % 2 === 0)",
' el.textContent = `${label}: ${count}`',
" el.dataset.active = count % 2 === 0 ? 'false' : 'true'",
" el.classList.toggle('is-even', count % 2 === 0)",
' }',
'',
" return <CounterButton label='Clicks: 0' onClick={handleClick} />",
' return el',
'}',
'',
].join('\n')
Expand Down
Loading