From 5753adf314b0d617613652ffcc774ce581fe914a Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Wed, 12 Aug 2026 08:20:43 -0500 Subject: [PATCH 1/3] externalize @solidjs/signals for solid 2 The v2 preset injects @solidjs/signals imports into compiled output after externals are collected, so the preview import map never gained an entry for it and bare imports failed to resolve. Externalizing it also pins its types to the solid-js version for type acquisition. Co-Authored-By: Claude Fable 5 --- packages/solid-repl/src/kernel/importMap.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/solid-repl/src/kernel/importMap.ts b/packages/solid-repl/src/kernel/importMap.ts index 16ee959..103c985 100644 --- a/packages/solid-repl/src/kernel/importMap.ts +++ b/packages/solid-repl/src/kernel/importMap.ts @@ -6,8 +6,9 @@ export interface ImportMapState { pinned: string[]; } -const EXTERNALIZED = ['solid-js', '@solidjs/web']; -const SOLID_FAMILY = [...EXTERNALIZED, '@solidjs/signals']; +// `@solidjs/signals` is externalized too: the v2 preset injects imports of it into +// compiled output after externals are collected, so it must always be in the map. +const EXTERNALIZED = ['solid-js', '@solidjs/web', '@solidjs/signals']; const isSolidV2 = (solidVersion: string | undefined) => !!solidVersion && parseInt(solidVersion, 10) >= 2; @@ -23,7 +24,7 @@ function moduleUrl(importee: string, solidVersion?: string) { const isV2 = isSolidV2(solidVersion); const target = solidWebAlias(importee, solidVersion) ?? importee; - const family = isV2 ? SOLID_FAMILY : ['solid-js']; + const family = isV2 ? EXTERNALIZED : ['solid-js']; const match = family.find((pkg) => target === pkg || target.startsWith(`${pkg}/`)); let url = 'https://esm.sh/'; From 8dd1192cf77710ef7501f1885ffb0aea71bc050d Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Wed, 12 Aug 2026 08:20:51 -0500 Subject: [PATCH 2/3] add auto-import completions to the typescript worker The codemirror lsp client never sends completionItem/resolve, so import edits are attached inline for a bounded, prefix-matched set of module-export entries, and the list is marked incomplete so the client re-queries as the word grows. Co-Authored-By: Claude Fable 5 --- .../src/components/editor/tsWorker.ts | 50 +++++++++++++++---- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/packages/solid-repl/src/components/editor/tsWorker.ts b/packages/solid-repl/src/components/editor/tsWorker.ts index 6412d56..3eb8f0e 100644 --- a/packages/solid-repl/src/components/editor/tsWorker.ts +++ b/packages/solid-repl/src/components/editor/tsWorker.ts @@ -139,19 +139,47 @@ const handleRequest = (method: string, params: any) => { case 'textDocument/completion': { const uri = params.textDocument.uri; - const { posToOffset } = positionConverters(env, uri); + const { posToOffset, offsetToPos } = positionConverters(env, uri); const offset = posToOffset(params.position); - const completions = env.languageService.getCompletionsAtPosition(uri, offset, {}); + const ls = env.languageService; + const completions = ls.getCompletionsAtPosition(uri, offset, { includeCompletionsForModuleExports: true }); if (!completions) return null; - return { - isIncomplete: !!completions.isIncomplete, - items: completions.entries.map((c) => ({ - label: c.name, - kind: completionItemKind[c.kind] ?? 1, - sortText: c.sortText, - data: { uri, offset, name: c.name, source: c.source }, - })), - }; + + const toItem = (c: ts.CompletionEntry) => ({ + label: c.name, + kind: completionItemKind[c.kind] ?? 1, + sortText: c.sortText, + data: { uri, offset, name: c.name, source: c.source }, + }); + + // Auto-import entries must carry their import edit up front (the client never sends + // completionItem/resolve), so only a bounded, prefix-matched set of them is resolved. + const start = completions.optionalReplacementSpan?.start ?? offset; + const prefix = (openDocs.get(uri) ?? '').slice(start, offset).toLowerCase(); + const autoImports = completions.entries + .filter( + (c) => + c.source && + c.hasAction && + prefix.length >= 2 && + c.name.toLowerCase().startsWith(prefix) && + // solid-js re-exports the signals APIs; its internal type paths aren't importable + !/@solidjs\/signals|solid-js\/types\//.test(c.source), + ) + .slice(0, 20) + .flatMap((c) => { + const details = ls.getCompletionEntryDetails(uri, offset, c.name, {}, c.source, undefined, c.data); + const change = details?.codeActions?.[0]?.changes.find((ch) => ch.fileName === uri); + const edits = change?.textChanges.map((tc) => ({ + range: { start: offsetToPos(tc.span.start), end: offsetToPos(tc.span.start + tc.span.length) }, + newText: tc.newText, + })); + return edits ? [{ ...toItem(c), additionalTextEdits: edits }] : []; + }); + + const plain = completions.entries.filter((c) => !c.source || !c.hasAction).map(toItem); + // Incomplete so the client re-queries as the word grows instead of filtering its first response. + return { isIncomplete: true, items: [...plain, ...autoImports] }; } case 'completionItem/resolve': { From 9b96bfc8cd984d9995bcee744b48706561d579fb Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Wed, 12 Aug 2026 08:20:51 -0500 Subject: [PATCH 3/3] prevent header title wrapping Co-Authored-By: Claude Fable 5 --- packages/playground/src/components/header.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/playground/src/components/header.tsx b/packages/playground/src/components/header.tsx index 13e1d38..4a00411 100644 --- a/packages/playground/src/components/header.tsx +++ b/packages/playground/src/components/header.tsx @@ -33,6 +33,7 @@ const titleStyles = css({ lineHeight: 0, letterSpacing: 'widest', textTransform: 'uppercase', + whiteSpace: 'nowrap', }); const menuButtonOnMobile = css({