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
7 changes: 7 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@
"no-console": "error",
"no-unused-vars": "error",
"no-shadow": "error",
"no-restricted-syntax": [
"error",
{
"selector": "CallExpression[callee.type='MemberExpression'][callee.property.name=/^(then|catch|finally)$/]",
"message": "Prefer async/await over promise chains (.then/.catch/.finally)."
}
],
"import/no-cycle": "error",
"import/no-duplicates": "error",
"import/no-self-import": "error",
Expand Down
86 changes: 86 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
name: knighted-develop-agent
description: Specialist coding agent for @knighted/develop (CDN-first browser playground for @knighted/jsx and @knighted/css).
---

You are a specialist engineer for the @knighted/develop package. Focus on playground runtime and UX in src/, plus build helpers in scripts/. Keep changes minimal, preserve CDN-first behavior, and validate with the listed commands.

## Commands (run early and often)

Repo root commands:

- Install: npm install
- Dev server: npm run dev
- Build prep + import map generation: npm run build
- Build (esm primary CDN): npm run build:esm
- Build (jspmGa primary CDN): npm run build:jspm
- Build (importMap primary CDN): npm run build:importmap-mode
- Preview dist output: npm run preview
- Lint: npm run lint
- Format write: npm run prettier

## Project knowledge

Tech stack:

- Node.js + npm
- ESM only (type: module)
- Browser-first runtime loaded from CDN
- @knighted/jsx runtime (DOM + React paths)
- @knighted/css browser compiler (CSS, Modules, Less, Sass)
- jspm for import map generation

Repository structure:

- src/ - app UI, CDN loader, bootstrap, styles
- scripts/ - build helper scripts for dist/import map preparation
- docs/ - package-specific docs

## Code style and conventions

- Preserve current project formatting: single quotes, no semicolons, print width 90, arrowParens avoid.
- Keep UI changes intentional and lightweight; avoid broad visual rewrites unless requested.
- Keep runtime logic defensive for flaky/slow CDN conditions.
- Preserve progressive loading behavior (lazy-load optional compilers/runtime pieces where possible).
- Do not introduce bundler-only assumptions into src/ runtime code.
- Prefer async/await over promise chains.
- Do not use IIFE, find another pattern instead.

## CDN and runtime expectations

- Keep dependency loading compatible with existing provider/fallback model in src/cdn.js.
- 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.

## Testing and validation expectations

- Run npm run lint after JavaScript edits.
- Run npm run build when touching scripts/, bootstrap, or CDN wiring.
- For UI behavior changes, validate manually through npm run dev in both render modes and at least one non-css style mode.

## Git workflow

- Keep changes focused to the smallest surface area.
- Update docs when behavior or developer workflow changes.
- Do not reformat unrelated files.

## Boundaries

Always:

- Keep changes localized to @knighted/develop.
- Preserve ESM compatibility and browser execution.
- Preserve CDN-first loading and fallback behavior.

Ask first:

- Adding or upgrading dependencies.
- Changing build output contract or import-map format.
- Changing public behavior documented in README/docs.

Never:

- Commit secrets or credentials.
- Edit generated output folders unless explicitly requested.
- Modify node_modules or lockfiles unless explicitly requested.
4 changes: 4 additions & 0 deletions docs/build-and-deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ npm run preview

## Notes

Related docs:

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

- In production, the preferred/default mode is import-map-based resolution (`window.__KNIGHTED_PRIMARY_CDN__ = "importMap"`).
- 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
110 changes: 110 additions & 0 deletions docs/code-mirror.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# CodeMirror Integration

This document defines how CodeMirror is integrated in @knighted/develop and what constraints must be preserved when changing editor behavior.

## Scope

CodeMirror is used for both authoring panels:

- Component panel (JSX source)
- Styles panel (CSS, CSS Modules, Less, Sass source)

The integration is CDN-first and must keep textarea fallback behavior.

## Integration Files

- `src/cdn.js`: CDN import keys and provider candidates
- `src/editor-codemirror.js`: shared CodeMirror runtime + editor factory
- `src/app.js`: editor initialization, fallback handling, and value wiring
- `src/styles.css`: editor host styling

## Runtime Model

The app initializes CodeMirror asynchronously.

- On success: both textareas are hidden and CodeMirror views are mounted.
- On failure: textareas remain active and the app keeps rendering normally.

This fallback is required. Editor failures must never block rendering.

## CDN Rules

CodeMirror packages are loaded with `importFromCdnWithFallback` and entries in `cdnImportSpecs`.

### Important: esm.sh specifier strategy

Use unversioned `esm` specifiers for the CodeMirror package group:

- `@codemirror/state`
- `@codemirror/view`
- `@codemirror/commands`
- `@codemirror/autocomplete`
- `@codemirror/language`
- `@codemirror/lang-javascript`
- `@codemirror/lang-css`

Reason: this lets esm.sh resolve one compatible dependency graph. Mixing pinned versions can load multiple `@codemirror/state` instances and trigger:

- `Unrecognized extension value in extension set ([object Object])`

Keep `jspmGa` candidates as fallback entries.

## Editor Behavior Baseline

`src/editor-codemirror.js` should continue to include these extensions:

- line numbers
- active line and gutter highlight
- bracket matching
- close brackets
- autocompletion
- syntax highlighting
- history keymap
- default keymap
- completion keymap
- close-bracket keymap
- `indentOnInput`
- tab size and indent unit

Language mapping should remain:

- component editor: `javascript-jsx`
- styles editor:
- `css` and `module` -> css language
- `less` -> less language
- `sass` -> sass language

## App Wiring Requirements

In `src/app.js`:

- Keep `getJsxSource()` and `getCssSource()` abstraction so both CodeMirror and textarea fallback paths work.
- Keep `initializeCodeEditors()` non-blocking (`void initializeCodeEditors()`).
- Keep style language reconfiguration on style mode change.
- Keep textarea input listeners in place for fallback mode.

## Validation Checklist

When modifying editor integration:

1. Run `npm run lint`.
2. Run `npm run dev` and verify:
- CodeMirror mounts in both panels.
- Textareas are hidden on success.
- Auto-close and indentation work while typing.
- Style mode change reconfigures language and still renders.
- Fallback path works if a CodeMirror import fails.
3. Run `npm run build` when CDN import keys are changed.

## Troubleshooting

If the UI still looks like plain textarea behavior:

1. Check for `.cm-editor` nodes in devtools.
2. Check whether `textarea.source-textarea--hidden` is present.
3. Check status text for editor fallback message.
4. Hard reload to clear cached CDN module responses.
5. Inspect console for duplicate-state error:
- `Unrecognized extension value in extension set ([object Object])`

If duplicate-state error returns, first verify `esm` CodeMirror specifiers in `src/cdn.js` are still unversioned for the full package group.
70 changes: 65 additions & 5 deletions src/app.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { cdnImports, importFromCdnWithFallback } from './cdn.js'
import { createCodeMirrorEditor } from './editor-codemirror.js'

const statusNode = document.getElementById('status')
const renderMode = document.getElementById('render-mode')
Expand Down Expand Up @@ -65,6 +66,10 @@ button:focus-visible {
jsxEditor.value = defaultJsx
cssEditor.value = defaultCss

let jsxCodeEditor = null
let cssCodeEditor = null
let getJsxSource = () => jsxEditor.value
let getCssSource = () => cssEditor.value
let scheduled = null
let reactRoot = null
let reactRuntime = null
Expand All @@ -85,6 +90,54 @@ const styleLabels = {
sass: 'Sass',
}

const getStyleEditorLanguage = mode => {
if (mode === 'less') return 'less'
if (mode === 'sass') return 'sass'
return 'css'
}

const createEditorHost = textarea => {
const host = document.createElement('div')
host.className = 'editor-host'
textarea.before(host)
return host
}

const initializeCodeEditors = async () => {
const jsxHost = createEditorHost(jsxEditor)
const cssHost = createEditorHost(cssEditor)

try {
const [nextJsxEditor, nextCssEditor] = await Promise.all([
createCodeMirrorEditor({
parent: jsxHost,
value: defaultJsx,
language: 'javascript-jsx',
onChange: maybeRender,
}),
createCodeMirrorEditor({
parent: cssHost,
value: defaultCss,
language: getStyleEditorLanguage(styleMode.value),
onChange: maybeRender,
}),
])

jsxCodeEditor = nextJsxEditor
cssCodeEditor = nextCssEditor
getJsxSource = () => jsxCodeEditor.getValue()
getCssSource = () => cssCodeEditor.getValue()

jsxEditor.classList.add('source-textarea--hidden')
cssEditor.classList.add('source-textarea--hidden')
} catch (error) {
jsxHost.remove()
cssHost.remove()
const message = error instanceof Error ? error.message : String(error)
setStatus(`Editor fallback: ${message}`)
}
}

const ensureCoreRuntime = async () => {
if (coreRuntime) return coreRuntime

Expand Down Expand Up @@ -423,7 +476,8 @@ const ensureLightningCssWasm = async () => {
const compileStyles = async () => {
const { cssFromSource } = await ensureCoreRuntime()
const dialect = styleMode.value
const cacheKey = `${dialect}\u0000${cssEditor.value}`
const cssSource = getCssSource()
const cacheKey = `${dialect}\u0000${cssSource}`
if (compiledStylesCache.key === cacheKey && compiledStylesCache.value) {
return compiledStylesCache.value
}
Expand All @@ -432,7 +486,7 @@ const compileStyles = async () => {
setStyleCompiling(shouldShowSpinner)

if (!shouldShowSpinner) {
const output = { css: cssEditor.value, moduleExports: null }
const output = { css: cssSource, moduleExports: null }
compiledStylesCache = {
key: cacheKey,
value: output,
Expand All @@ -459,7 +513,7 @@ const compileStyles = async () => {
options.lightningcss = await ensureLightningCssWasm()
}

const result = await cssFromSource(cssEditor.value, options)
const result = await cssFromSource(cssSource, options)
if (!result.ok) {
throw new Error(result.error.message)
}
Expand All @@ -486,7 +540,7 @@ const compileStyles = async () => {

const evaluateUserModule = async (helpers = {}) => {
const { jsx, transpileJsxSource } = await ensureCoreRuntime()
const userCode = jsxEditor.value
const userCode = getJsxSource()
.replace(/^\s*export\s+default\s+function\b/gm, '__defaultExport = function')
.replace(/^\s*export\s+default\s+class\b/gm, '__defaultExport = class')
.replace(/^\s*export\s+default\s+/gm, '__defaultExport = ')
Expand Down Expand Up @@ -633,7 +687,12 @@ const maybeRender = () => {
}

renderMode.addEventListener('change', maybeRender)
styleMode.addEventListener('change', maybeRender)
styleMode.addEventListener('change', () => {
if (cssCodeEditor) {
cssCodeEditor.setLanguage(getStyleEditorLanguage(styleMode.value))
}
maybeRender()
})
shadowToggle.addEventListener('change', maybeRender)
autoRenderToggle.addEventListener('change', () => {
if (autoRenderToggle.checked) {
Expand All @@ -646,4 +705,5 @@ cssEditor.addEventListener('input', maybeRender)

setStyleCompiling(false)
setCdnLoading(true)
void initializeCodeEditors()
renderPreview()
40 changes: 40 additions & 0 deletions src/cdn.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,46 @@ export const cdnImportSpecs = {
esm: '@parcel/css-wasm',
jspmGa: 'npm:@parcel/css-wasm',
},
codemirrorState: {
importMap: '@codemirror/state',
esm: '@codemirror/state',
jspmGa: 'npm:@codemirror/state@6.5.2',
},
codemirrorView: {
importMap: '@codemirror/view',
esm: '@codemirror/view',
jspmGa: 'npm:@codemirror/view@6.38.6',
},
codemirrorCommands: {
importMap: '@codemirror/commands',
esm: '@codemirror/commands',
jspmGa: 'npm:@codemirror/commands@6.10.0',
},
codemirrorAutocomplete: {
importMap: '@codemirror/autocomplete',
esm: '@codemirror/autocomplete',
jspmGa: 'npm:@codemirror/autocomplete@6.20.0',
},
codemirrorLanguage: {
importMap: '@codemirror/language',
esm: '@codemirror/language',
jspmGa: 'npm:@codemirror/language@6.11.3',
},
codemirrorLezerHighlight: {
importMap: '@lezer/highlight',
esm: '@lezer/highlight',
jspmGa: 'npm:@lezer/highlight@1.2.3',
},
codemirrorLangJavascript: {
importMap: '@codemirror/lang-javascript',
esm: '@codemirror/lang-javascript',
jspmGa: 'npm:@codemirror/lang-javascript@6.2.4',
},
codemirrorLangCss: {
importMap: '@codemirror/lang-css',
esm: '@codemirror/lang-css',
jspmGa: 'npm:@codemirror/lang-css@6.3.1',
},
}

const getProviderPriority = () => {
Expand Down
Loading