Skip to content

Commit 0dd4269

Browse files
feat: codemirror integration. (#3)
1 parent 301ff0d commit 0dd4269

8 files changed

Lines changed: 666 additions & 7 deletions

File tree

.oxlintrc.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@
1212
"no-console": "error",
1313
"no-unused-vars": "error",
1414
"no-shadow": "error",
15+
"no-restricted-syntax": [
16+
"error",
17+
{
18+
"selector": "CallExpression[callee.type='MemberExpression'][callee.property.name=/^(then|catch|finally)$/]",
19+
"message": "Prefer async/await over promise chains (.then/.catch/.finally)."
20+
}
21+
],
1522
"import/no-cycle": "error",
1623
"import/no-duplicates": "error",
1724
"import/no-self-import": "error",

AGENTS.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
---
2+
name: knighted-develop-agent
3+
description: Specialist coding agent for @knighted/develop (CDN-first browser playground for @knighted/jsx and @knighted/css).
4+
---
5+
6+
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.
7+
8+
## Commands (run early and often)
9+
10+
Repo root commands:
11+
12+
- Install: npm install
13+
- Dev server: npm run dev
14+
- Build prep + import map generation: npm run build
15+
- Build (esm primary CDN): npm run build:esm
16+
- Build (jspmGa primary CDN): npm run build:jspm
17+
- Build (importMap primary CDN): npm run build:importmap-mode
18+
- Preview dist output: npm run preview
19+
- Lint: npm run lint
20+
- Format write: npm run prettier
21+
22+
## Project knowledge
23+
24+
Tech stack:
25+
26+
- Node.js + npm
27+
- ESM only (type: module)
28+
- Browser-first runtime loaded from CDN
29+
- @knighted/jsx runtime (DOM + React paths)
30+
- @knighted/css browser compiler (CSS, Modules, Less, Sass)
31+
- jspm for import map generation
32+
33+
Repository structure:
34+
35+
- src/ - app UI, CDN loader, bootstrap, styles
36+
- scripts/ - build helper scripts for dist/import map preparation
37+
- docs/ - package-specific docs
38+
39+
## Code style and conventions
40+
41+
- Preserve current project formatting: single quotes, no semicolons, print width 90, arrowParens avoid.
42+
- Keep UI changes intentional and lightweight; avoid broad visual rewrites unless requested.
43+
- Keep runtime logic defensive for flaky/slow CDN conditions.
44+
- Preserve progressive loading behavior (lazy-load optional compilers/runtime pieces where possible).
45+
- Do not introduce bundler-only assumptions into src/ runtime code.
46+
- Prefer async/await over promise chains.
47+
- Do not use IIFE, find another pattern instead.
48+
49+
## CDN and runtime expectations
50+
51+
- Keep dependency loading compatible with existing provider/fallback model in src/cdn.js.
52+
- Prefer extending existing CDN import key patterns instead of ad hoc dynamic imports.
53+
- Maintain graceful fallback behavior when CDN modules fail to load.
54+
- Keep the app usable in local dev without requiring a local bundle step.
55+
56+
## Testing and validation expectations
57+
58+
- Run npm run lint after JavaScript edits.
59+
- Run npm run build when touching scripts/, bootstrap, or CDN wiring.
60+
- For UI behavior changes, validate manually through npm run dev in both render modes and at least one non-css style mode.
61+
62+
## Git workflow
63+
64+
- Keep changes focused to the smallest surface area.
65+
- Update docs when behavior or developer workflow changes.
66+
- Do not reformat unrelated files.
67+
68+
## Boundaries
69+
70+
Always:
71+
72+
- Keep changes localized to @knighted/develop.
73+
- Preserve ESM compatibility and browser execution.
74+
- Preserve CDN-first loading and fallback behavior.
75+
76+
Ask first:
77+
78+
- Adding or upgrading dependencies.
79+
- Changing build output contract or import-map format.
80+
- Changing public behavior documented in README/docs.
81+
82+
Never:
83+
84+
- Commit secrets or credentials.
85+
- Edit generated output folders unless explicitly requested.
86+
- Modify node_modules or lockfiles unless explicitly requested.

docs/build-and-deploy.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ npm run preview
8787

8888
## Notes
8989

90+
Related docs:
91+
92+
- `docs/code-mirror.md` for CodeMirror CDN integration rules, fallback behavior, and validation checklist.
93+
9094
- In production, the preferred/default mode is import-map-based resolution (`window.__KNIGHTED_PRIMARY_CDN__ = "importMap"`).
9195
- In `importMap` mode, runtime resolution is import-map first; if a specifier is missing from the generated map, runtime falls back through the CDN
9296
provider chain configured in `src/cdn.js`.

docs/code-mirror.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# CodeMirror Integration
2+
3+
This document defines how CodeMirror is integrated in @knighted/develop and what constraints must be preserved when changing editor behavior.
4+
5+
## Scope
6+
7+
CodeMirror is used for both authoring panels:
8+
9+
- Component panel (JSX source)
10+
- Styles panel (CSS, CSS Modules, Less, Sass source)
11+
12+
The integration is CDN-first and must keep textarea fallback behavior.
13+
14+
## Integration Files
15+
16+
- `src/cdn.js`: CDN import keys and provider candidates
17+
- `src/editor-codemirror.js`: shared CodeMirror runtime + editor factory
18+
- `src/app.js`: editor initialization, fallback handling, and value wiring
19+
- `src/styles.css`: editor host styling
20+
21+
## Runtime Model
22+
23+
The app initializes CodeMirror asynchronously.
24+
25+
- On success: both textareas are hidden and CodeMirror views are mounted.
26+
- On failure: textareas remain active and the app keeps rendering normally.
27+
28+
This fallback is required. Editor failures must never block rendering.
29+
30+
## CDN Rules
31+
32+
CodeMirror packages are loaded with `importFromCdnWithFallback` and entries in `cdnImportSpecs`.
33+
34+
### Important: esm.sh specifier strategy
35+
36+
Use unversioned `esm` specifiers for the CodeMirror package group:
37+
38+
- `@codemirror/state`
39+
- `@codemirror/view`
40+
- `@codemirror/commands`
41+
- `@codemirror/autocomplete`
42+
- `@codemirror/language`
43+
- `@codemirror/lang-javascript`
44+
- `@codemirror/lang-css`
45+
46+
Reason: this lets esm.sh resolve one compatible dependency graph. Mixing pinned versions can load multiple `@codemirror/state` instances and trigger:
47+
48+
- `Unrecognized extension value in extension set ([object Object])`
49+
50+
Keep `jspmGa` candidates as fallback entries.
51+
52+
## Editor Behavior Baseline
53+
54+
`src/editor-codemirror.js` should continue to include these extensions:
55+
56+
- line numbers
57+
- active line and gutter highlight
58+
- bracket matching
59+
- close brackets
60+
- autocompletion
61+
- syntax highlighting
62+
- history keymap
63+
- default keymap
64+
- completion keymap
65+
- close-bracket keymap
66+
- `indentOnInput`
67+
- tab size and indent unit
68+
69+
Language mapping should remain:
70+
71+
- component editor: `javascript-jsx`
72+
- styles editor:
73+
- `css` and `module` -> css language
74+
- `less` -> less language
75+
- `sass` -> sass language
76+
77+
## App Wiring Requirements
78+
79+
In `src/app.js`:
80+
81+
- Keep `getJsxSource()` and `getCssSource()` abstraction so both CodeMirror and textarea fallback paths work.
82+
- Keep `initializeCodeEditors()` non-blocking (`void initializeCodeEditors()`).
83+
- Keep style language reconfiguration on style mode change.
84+
- Keep textarea input listeners in place for fallback mode.
85+
86+
## Validation Checklist
87+
88+
When modifying editor integration:
89+
90+
1. Run `npm run lint`.
91+
2. Run `npm run dev` and verify:
92+
- CodeMirror mounts in both panels.
93+
- Textareas are hidden on success.
94+
- Auto-close and indentation work while typing.
95+
- Style mode change reconfigures language and still renders.
96+
- Fallback path works if a CodeMirror import fails.
97+
3. Run `npm run build` when CDN import keys are changed.
98+
99+
## Troubleshooting
100+
101+
If the UI still looks like plain textarea behavior:
102+
103+
1. Check for `.cm-editor` nodes in devtools.
104+
2. Check whether `textarea.source-textarea--hidden` is present.
105+
3. Check status text for editor fallback message.
106+
4. Hard reload to clear cached CDN module responses.
107+
5. Inspect console for duplicate-state error:
108+
- `Unrecognized extension value in extension set ([object Object])`
109+
110+
If duplicate-state error returns, first verify `esm` CodeMirror specifiers in `src/cdn.js` are still unversioned for the full package group.

src/app.js

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { cdnImports, importFromCdnWithFallback } from './cdn.js'
2+
import { createCodeMirrorEditor } from './editor-codemirror.js'
23

34
const statusNode = document.getElementById('status')
45
const renderMode = document.getElementById('render-mode')
@@ -65,6 +66,10 @@ button:focus-visible {
6566
jsxEditor.value = defaultJsx
6667
cssEditor.value = defaultCss
6768

69+
let jsxCodeEditor = null
70+
let cssCodeEditor = null
71+
let getJsxSource = () => jsxEditor.value
72+
let getCssSource = () => cssEditor.value
6873
let scheduled = null
6974
let reactRoot = null
7075
let reactRuntime = null
@@ -85,6 +90,54 @@ const styleLabels = {
8590
sass: 'Sass',
8691
}
8792

93+
const getStyleEditorLanguage = mode => {
94+
if (mode === 'less') return 'less'
95+
if (mode === 'sass') return 'sass'
96+
return 'css'
97+
}
98+
99+
const createEditorHost = textarea => {
100+
const host = document.createElement('div')
101+
host.className = 'editor-host'
102+
textarea.before(host)
103+
return host
104+
}
105+
106+
const initializeCodeEditors = async () => {
107+
const jsxHost = createEditorHost(jsxEditor)
108+
const cssHost = createEditorHost(cssEditor)
109+
110+
try {
111+
const [nextJsxEditor, nextCssEditor] = await Promise.all([
112+
createCodeMirrorEditor({
113+
parent: jsxHost,
114+
value: defaultJsx,
115+
language: 'javascript-jsx',
116+
onChange: maybeRender,
117+
}),
118+
createCodeMirrorEditor({
119+
parent: cssHost,
120+
value: defaultCss,
121+
language: getStyleEditorLanguage(styleMode.value),
122+
onChange: maybeRender,
123+
}),
124+
])
125+
126+
jsxCodeEditor = nextJsxEditor
127+
cssCodeEditor = nextCssEditor
128+
getJsxSource = () => jsxCodeEditor.getValue()
129+
getCssSource = () => cssCodeEditor.getValue()
130+
131+
jsxEditor.classList.add('source-textarea--hidden')
132+
cssEditor.classList.add('source-textarea--hidden')
133+
} catch (error) {
134+
jsxHost.remove()
135+
cssHost.remove()
136+
const message = error instanceof Error ? error.message : String(error)
137+
setStatus(`Editor fallback: ${message}`)
138+
}
139+
}
140+
88141
const ensureCoreRuntime = async () => {
89142
if (coreRuntime) return coreRuntime
90143

@@ -423,7 +476,8 @@ const ensureLightningCssWasm = async () => {
423476
const compileStyles = async () => {
424477
const { cssFromSource } = await ensureCoreRuntime()
425478
const dialect = styleMode.value
426-
const cacheKey = `${dialect}\u0000${cssEditor.value}`
479+
const cssSource = getCssSource()
480+
const cacheKey = `${dialect}\u0000${cssSource}`
427481
if (compiledStylesCache.key === cacheKey && compiledStylesCache.value) {
428482
return compiledStylesCache.value
429483
}
@@ -432,7 +486,7 @@ const compileStyles = async () => {
432486
setStyleCompiling(shouldShowSpinner)
433487

434488
if (!shouldShowSpinner) {
435-
const output = { css: cssEditor.value, moduleExports: null }
489+
const output = { css: cssSource, moduleExports: null }
436490
compiledStylesCache = {
437491
key: cacheKey,
438492
value: output,
@@ -459,7 +513,7 @@ const compileStyles = async () => {
459513
options.lightningcss = await ensureLightningCssWasm()
460514
}
461515

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

487541
const evaluateUserModule = async (helpers = {}) => {
488542
const { jsx, transpileJsxSource } = await ensureCoreRuntime()
489-
const userCode = jsxEditor.value
543+
const userCode = getJsxSource()
490544
.replace(/^\s*export\s+default\s+function\b/gm, '__defaultExport = function')
491545
.replace(/^\s*export\s+default\s+class\b/gm, '__defaultExport = class')
492546
.replace(/^\s*export\s+default\s+/gm, '__defaultExport = ')
@@ -633,7 +687,12 @@ const maybeRender = () => {
633687
}
634688

635689
renderMode.addEventListener('change', maybeRender)
636-
styleMode.addEventListener('change', maybeRender)
690+
styleMode.addEventListener('change', () => {
691+
if (cssCodeEditor) {
692+
cssCodeEditor.setLanguage(getStyleEditorLanguage(styleMode.value))
693+
}
694+
maybeRender()
695+
})
637696
shadowToggle.addEventListener('change', maybeRender)
638697
autoRenderToggle.addEventListener('change', () => {
639698
if (autoRenderToggle.checked) {
@@ -646,4 +705,5 @@ cssEditor.addEventListener('input', maybeRender)
646705

647706
setStyleCompiling(false)
648707
setCdnLoading(true)
708+
void initializeCodeEditors()
649709
renderPreview()

src/cdn.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,46 @@ export const cdnImportSpecs = {
9393
esm: '@parcel/css-wasm',
9494
jspmGa: 'npm:@parcel/css-wasm',
9595
},
96+
codemirrorState: {
97+
importMap: '@codemirror/state',
98+
esm: '@codemirror/state',
99+
jspmGa: 'npm:@codemirror/state@6.5.2',
100+
},
101+
codemirrorView: {
102+
importMap: '@codemirror/view',
103+
esm: '@codemirror/view',
104+
jspmGa: 'npm:@codemirror/view@6.38.6',
105+
},
106+
codemirrorCommands: {
107+
importMap: '@codemirror/commands',
108+
esm: '@codemirror/commands',
109+
jspmGa: 'npm:@codemirror/commands@6.10.0',
110+
},
111+
codemirrorAutocomplete: {
112+
importMap: '@codemirror/autocomplete',
113+
esm: '@codemirror/autocomplete',
114+
jspmGa: 'npm:@codemirror/autocomplete@6.20.0',
115+
},
116+
codemirrorLanguage: {
117+
importMap: '@codemirror/language',
118+
esm: '@codemirror/language',
119+
jspmGa: 'npm:@codemirror/language@6.11.3',
120+
},
121+
codemirrorLezerHighlight: {
122+
importMap: '@lezer/highlight',
123+
esm: '@lezer/highlight',
124+
jspmGa: 'npm:@lezer/highlight@1.2.3',
125+
},
126+
codemirrorLangJavascript: {
127+
importMap: '@codemirror/lang-javascript',
128+
esm: '@codemirror/lang-javascript',
129+
jspmGa: 'npm:@codemirror/lang-javascript@6.2.4',
130+
},
131+
codemirrorLangCss: {
132+
importMap: '@codemirror/lang-css',
133+
esm: '@codemirror/lang-css',
134+
jspmGa: 'npm:@codemirror/lang-css@6.3.1',
135+
},
96136
}
97137

98138
const getProviderPriority = () => {

0 commit comments

Comments
 (0)