diff --git a/package.json b/package.json index b2cb5e361..95a9cc509 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "format": "prettier . --write", "lint:fix": "eslint --flag unstable_native_nodejs_ts_config . --fix", "check:lint": "eslint --flag unstable_native_nodejs_ts_config .", - "check:types": "tsc --noEmit" + "check:types": "tsc --noEmit", + "sync:ref": "node scripts/extract-solid-ref.mjs" }, "dependencies": { "@kobalte/core": "^0.13.11", diff --git a/scripts/extract-solid-ref.mjs b/scripts/extract-solid-ref.mjs new file mode 100644 index 000000000..d31415945 --- /dev/null +++ b/scripts/extract-solid-ref.mjs @@ -0,0 +1,1170 @@ +#!/usr/bin/env node +import fs from "node:fs"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import ts from "typescript"; + +const REPO_URL = "https://github.com/solidjs/solid.git"; +const DEFAULT_SOURCE = "/tmp/solid-v2-source"; +const DEFAULT_OUT = "src/routes/v2/reference"; +const DEFAULT_REF = "next"; +const SPARSE_PATHS = [ + "packages/solid", + "packages/solid-web", + "packages/solid-signals", + "documentation/solid-2.0", +]; + +const ENTRYPOINTS = [ + { + packageName: "solid-js", + path: "packages/solid/src/index.ts", + }, + { + packageName: "@solidjs/web", + path: "packages/solid-web/src/index.ts", + }, + { + packageName: "@solidjs/signals", + path: "packages/solid-signals/src/index.ts", + }, +]; + +const GENERATED_MARKER = + "Generated by scripts/extract-solid-ref.mjs. Edit the source JSDoc or disposition map, then regenerate."; + +const TOP_LEVEL_ROUTE_ORDER = { + reactivity: "(1)reactivity", + stores: "(2)stores", + "lifecycle-actions": "(3)lifecycle-actions", + "components-context": "(4)components-context", + "components-jsx": "(5)components-jsx", + "rendering-ssr": "(6)rendering-ssr", + advanced: "(7)advanced", + types: "(8)types", +}; + +const ADVANCED_ROUTE_ORDER = { + "owner-introspection": "(1)owner-introspection", + "specialized-reactivity": "(2)specialized-reactivity", + "store-advanced": "(3)store-advanced", + "jsx-component-primitives": "(4)jsx-component-primitives", + "manual-hydration": "(5)manual-hydration", + "interop-async": "(6)interop-async", + "diagnostics-dev-hooks": "(7)diagnostics-dev-hooks", +}; + +const CANONICAL_ROUTES = { + createEffect: ["reactivity/create-effect.mdx", "Reactivity"], + createMemo: ["reactivity/create-memo.mdx", "Reactivity"], + createOptimistic: ["reactivity/create-optimistic.mdx", "Reactivity"], + createSignal: ["reactivity/create-signal.mdx", "Reactivity"], + flush: ["reactivity/flush.mdx", "Reactivity"], + isPending: ["reactivity/is-pending.mdx", "Reactivity"], + latest: ["reactivity/latest.mdx", "Reactivity"], + untrack: ["reactivity/untrack.mdx", "Reactivity"], + + createOptimisticStore: ["stores/create-optimistic-store.mdx", "Stores"], + createProjection: ["stores/create-projection.mdx", "Stores"], + createStore: ["stores/create-store.mdx", "Stores"], + merge: ["stores/merge.mdx", "Stores"], + omit: ["stores/omit.mdx", "Stores"], + reconcile: ["stores/reconcile.mdx", "Stores"], + + action: ["lifecycle-actions/action.mdx", "Lifecycle & Actions"], + onSettled: ["lifecycle-actions/on-settled.mdx", "Lifecycle & Actions"], + refresh: ["lifecycle-actions/refresh.mdx", "Lifecycle & Actions"], + + children: ["components-context/children.mdx", "Components & Context"], + createContext: [ + "components-context/create-context.mdx", + "Components & Context", + ], + createUniqueId: [ + "components-context/create-unique-id.mdx", + "Components & Context", + ], + dynamic: ["components-context/dynamic.mdx", "Components & Context"], + lazy: ["components-context/lazy.mdx", "Components & Context"], + useContext: ["components-context/use-context.mdx", "Components & Context"], + + Dynamic: ["components-jsx/dynamic.mdx", "Components (JSX)"], + Errored: ["components-jsx/errored.mdx", "Components (JSX)"], + For: ["components-jsx/for.mdx", "Components (JSX)"], + Loading: ["components-jsx/loading.mdx", "Components (JSX)"], + Match: ["components-jsx/switch-and-match.mdx", "Components (JSX)"], + Portal: ["components-jsx/portal.mdx", "Components (JSX)"], + Repeat: ["components-jsx/repeat.mdx", "Components (JSX)"], + Reveal: ["components-jsx/reveal.mdx", "Components (JSX)"], + Show: ["components-jsx/show.mdx", "Components (JSX)"], + Switch: ["components-jsx/switch-and-match.mdx", "Components (JSX)"], + + hydrate: ["rendering-ssr/hydrate.mdx", "Rendering & SSR"], + isDev: ["rendering-ssr/is-dev.mdx", "Rendering & SSR"], + isServer: ["rendering-ssr/is-server.mdx", "Rendering & SSR"], + render: ["rendering-ssr/render.mdx", "Rendering & SSR"], + renderToStream: ["rendering-ssr/render-to-stream.mdx", "Rendering & SSR"], + renderToString: ["rendering-ssr/render-to-string.mdx", "Rendering & SSR"], + renderToStringAsync: [ + "rendering-ssr/render-to-string-async.mdx", + "Rendering & SSR", + ], +}; + +const ADVANCED_ROUTES = { + createRoot: [ + "advanced/owner-introspection/create-root.mdx", + "Advanced / Owner & Introspection", + ], + getObserver: [ + "advanced/owner-introspection/get-observer.mdx", + "Advanced / Owner & Introspection", + ], + getOwner: [ + "advanced/owner-introspection/get-owner.mdx", + "Advanced / Owner & Introspection", + ], + isDisposed: [ + "advanced/owner-introspection/is-disposed.mdx", + "Advanced / Owner & Introspection", + ], + runWithOwner: [ + "advanced/owner-introspection/run-with-owner.mdx", + "Advanced / Owner & Introspection", + ], + + createReaction: [ + "advanced/specialized-reactivity/create-reaction.mdx", + "Advanced / Specialized Reactivity & Tracking", + ], + createRenderEffect: [ + "advanced/specialized-reactivity/create-render-effect.mdx", + "Advanced / Specialized Reactivity & Tracking", + ], + createTrackedEffect: [ + "advanced/specialized-reactivity/create-tracked-effect.mdx", + "Advanced / Specialized Reactivity & Tracking", + ], + isRefreshing: [ + "advanced/specialized-reactivity/is-refreshing.mdx", + "Advanced / Specialized Reactivity & Tracking", + ], + onCleanup: [ + "advanced/specialized-reactivity/on-cleanup.mdx", + "Advanced / Specialized Reactivity & Tracking", + ], + + deep: ["advanced/store-advanced/deep.mdx", "Advanced / Store Advanced"], + isWrappable: [ + "advanced/store-advanced/is-wrappable.mdx", + "Advanced / Store Advanced", + ], + snapshot: [ + "advanced/store-advanced/snapshot.mdx", + "Advanced / Store Advanced", + ], + storePath: [ + "advanced/store-advanced/store-path.mdx", + "Advanced / Store Advanced", + ], + + createErrorBoundary: [ + "advanced/jsx-component-primitives/create-error-boundary.mdx", + "Advanced / JSX Component Primitives", + ], + createLoadingBoundary: [ + "advanced/jsx-component-primitives/create-loading-boundary.mdx", + "Advanced / JSX Component Primitives", + ], + createRevealOrder: [ + "advanced/jsx-component-primitives/create-reveal-order.mdx", + "Advanced / JSX Component Primitives", + ], + mapArray: [ + "advanced/jsx-component-primitives/map-array.mdx", + "Advanced / JSX Component Primitives", + ], + repeat: [ + "advanced/jsx-component-primitives/repeat.mdx", + "Advanced / JSX Component Primitives", + ], + + Hydration: [ + "advanced/manual-hydration/hydration.mdx", + "Advanced / Manual Hydration", + ], + NoHydration: [ + "advanced/manual-hydration/no-hydration.mdx", + "Advanced / Manual Hydration", + ], + + enableExternalSource: [ + "advanced/interop-async/enable-external-source.mdx", + "Advanced / Interop & Async", + ], + flatten: ["advanced/interop-async/flatten.mdx", "Advanced / Interop & Async"], + NotReadyError: [ + "advanced/interop-async/not-ready-error.mdx", + "Advanced / Interop & Async", + ], + resolve: ["advanced/interop-async/resolve.mdx", "Advanced / Interop & Async"], + + DEV: [ + "advanced/diagnostics-dev-hooks/dev.mdx", + "Advanced / Diagnostics & Dev Hooks", + ], +}; + +const STANDALONE_TYPE_ROUTES = { + Accessor: ["types/reactive-types.mdx", "Types"], + Setter: ["types/reactive-types.mdx", "Types"], + Signal: ["types/reactive-types.mdx", "Types"], + + Component: ["types/component-types.mdx", "Types"], + ParentComponent: ["types/component-types.mdx", "Types"], + VoidComponent: ["types/component-types.mdx", "Types"], + FlowComponent: ["types/component-types.mdx", "Types"], + ParentProps: ["types/component-types.mdx", "Types"], + VoidProps: ["types/component-types.mdx", "Types"], + FlowProps: ["types/component-types.mdx", "Types"], + ValidComponent: ["types/component-types.mdx", "Types"], + ComponentProps: ["types/component-types.mdx", "Types"], + Ref: ["types/component-types.mdx", "Types"], + + Context: ["types/context-types.mdx", "Types"], + ContextProviderComponent: ["types/context-types.mdx", "Types"], + + Store: ["types/store-types.mdx", "Types"], + StoreNode: ["types/store-types.mdx", "Types"], + SolidStore: ["types/store-types.mdx", "Types"], + + Owner: ["types/owner.mdx", "Types"], + + JSX: ["types/jsx-types.mdx", "Types"], + JSXElement: ["types/jsx-types.mdx", "Types"], +}; + +const FOLD_INTO = { + ArrayFilterFn: "storePath", + ChildrenReturn: "children", + ComputeFunction: "createEffect", + ContextNotFoundError: "useContext", + ContextRecord: "createContext", + CustomPartial: "storePath", + Dev: "DEV", + DevHooks: "DEV", + DiagnosticCapture: "DEV", + DiagnosticCode: "DEV", + DiagnosticEvent: "DEV", + DiagnosticKind: "DEV", + Diagnostics: "DEV", + DiagnosticSeverity: "DEV", + DynamicProps: "Dynamic", + EffectBundle: "createEffect", + EffectFunction: "createEffect", + EffectOptions: "createEffect", + ExternalSource: "enableExternalSource", + ExternalSourceConfig: "enableExternalSource", + ExternalSourceFactory: "enableExternalSource", + HydrationProjectionOptions: "createProjection", + Maybe: "mapArray", + Merge: "merge", + MemoOptions: ["createMemo", "createSignal"], + NoOwnerError: "getOwner", + NoInfer: "createSignal", + NotWrappable: "isWrappable", + Omit: "omit", + Part: "storePath", + PathSetter: "storePath", + ProjectionOptions: ["createProjection", "createStore"], + PatchOp: "createStore", + Refreshable: ["createOptimisticStore", "createProjection", "createStore"], + RevealOrder: "Reveal", + RevealProps: "Reveal", + ResolvedChildren: "children", + ResolvedJSXElement: "children", + MatchProps: "Switch", + SignalOptions: ["createSignal", "createMemo"], + StoreOptions: "createStore", + StorePathRange: "storePath", + StoreSetter: "createStore", + isEqual: ["createSignal", "createMemo"], +}; + +const HIDDEN_EXPORTS = new Set([ + "$DEVCOMP", + "$PROXY", + "$REFRESH", + "$TRACK", + "SUPPORTS_PROXY", + "Namespaces", + "SVGElements", + "MathMLElements", + "addEventListener", + "clearSnapshots", + "createComponent", + "createDeepProxy", + "createOwner", + "enableHydration", + "enforceLoadingBoundary", + "escape", + "getContext", + "getNextChildId", + "getNextElement", + "HydrationContext", + "insert", + "IQueue", + "markSnapshotScope", + "mergeProps", + "NoHydrateContext", + "peekNextChildId", + "releaseSnapshotScope", + "resolveSSRNode", + "setSnapshotCapture", + "setContext", + "sharedConfig", + "spread", + "SSRTemplateObject", + "ssr", + "ssrAttribute", + "ssrClassList", + "ssrElement", + "ssrHandleError", + "ssrHydrationKey", + "ssrRunInScope", + "ssrStyle", + "template", +]); + +const ENTRY_CALLOUTS = { + Dynamic: + "> `` is the JSX convenience wrapper for `dynamic()`. Prefer `dynamic()` when you need a reusable stable component reference.", + createRoot: + "> `render()` creates the root for normal app code. Reach for `createRoot` in tests, libraries, or non-render entry points that need to host a reactive scope.", + dynamic: + "> `dynamic()` is the canonical API: it returns a stable component identity and composes with `lazy()` and routing. Use `` when the choice happens at a JSX callsite.", + onCleanup: + "> For component setup and teardown in Solid 2.0, prefer `onSettled` and return a cleanup function. Keep `onCleanup` for advanced owner and custom primitive internals.", +}; + +const scriptPath = fileURLToPath(import.meta.url); +const repoRoot = path.resolve(path.dirname(scriptPath), ".."); + +const args = parseArgs(process.argv.slice(2)); +const sourceDir = path.resolve(args.source ?? DEFAULT_SOURCE); +const sourceRef = args["source-ref"] ?? DEFAULT_REF; +const outDir = path.resolve(repoRoot, args.out ?? DEFAULT_OUT); +const dryRun = Boolean(args["dry-run"]); +const skipFetch = Boolean(args["skip-fetch"]); + +if (args.help) { + printHelp(); + process.exit(0); +} + +if (!skipFetch) { + ensureSourceCheckout(sourceDir, sourceRef); +} + +const sourceSha = git(["-C", sourceDir, "rev-parse", "HEAD"]).trim(); +const program = createProgram(sourceDir); +const checker = program.getTypeChecker(); +const reference = collectEntries(program, checker, sourceDir); +const files = buildOutputFiles(reference, { + sourceDir, + sourceRef, + sourceSha, +}); + +if (dryRun) { + printDryRun(files, reference, sourceSha); +} else { + writeFiles(files); + console.log( + `Wrote ${files.length} reference file(s) to ${path.relative(repoRoot, outDir)}` + ); + console.log(`Solid source SHA: ${sourceSha}`); +} + +function parseArgs(rawArgs) { + const parsed = {}; + for (let i = 0; i < rawArgs.length; i++) { + const arg = rawArgs[i]; + if (!arg.startsWith("--")) { + throw new Error(`Unexpected argument: ${arg}`); + } + const key = arg.slice(2); + if (["dry-run", "skip-fetch", "help"].includes(key)) { + parsed[key] = true; + continue; + } + const next = rawArgs[++i]; + if (!next || next.startsWith("--")) { + throw new Error(`Missing value for --${key}`); + } + parsed[key] = next; + } + return parsed; +} + +function printHelp() { + console.log(`Usage: +node scripts/extract-solid-ref.mjs [options] + +Options: + --source-ref Solid source ref to fetch. Defaults to "next". + --source Local solidjs/solid checkout. Defaults to /tmp/solid-v2-source. + --out Output directory. Defaults to src/routes/v2/reference. + --dry-run Print planned files without writing MDX. + --skip-fetch Use the current local checkout as-is. + --help Show this help. +`); +} + +function ensureSourceCheckout(dir, ref) { + if (!fs.existsSync(dir)) { + git(["clone", "--filter=blob:none", "--sparse", REPO_URL, dir]); + } else if (!fs.existsSync(path.join(dir, ".git"))) { + throw new Error(`${dir} exists but is not a git checkout`); + } + + git(["-C", dir, "sparse-checkout", "set", ...SPARSE_PATHS]); + git(["-C", dir, "fetch", "origin", ref, "--depth=1"]); + git(["-C", dir, "checkout", "--detach", "FETCH_HEAD"]); +} + +function git(args) { + return execFileSync("git", args, { + encoding: "utf8", + stdio: ["ignore", "pipe", "inherit"], + }); +} + +function createProgram(root) { + const options = { + allowJs: false, + allowSyntheticDefaultImports: true, + esModuleInterop: true, + jsx: ts.JsxEmit.Preserve, + module: ts.ModuleKind.NodeNext, + moduleResolution: ts.ModuleResolutionKind.NodeNext, + noEmit: true, + skipLibCheck: true, + target: ts.ScriptTarget.ES2022, + baseUrl: root, + paths: { + "solid-js": ["packages/solid/src/index.ts"], + "@solidjs/signals": ["packages/solid-signals/src/index.ts"], + "@solidjs/web": ["packages/solid-web/src/index.ts"], + }, + }; + + const rootNames = [ + ...ENTRYPOINTS.map((entry) => path.join(root, entry.path)), + ...collectTsFiles(path.join(root, "packages/solid/src")), + ...collectTsFiles(path.join(root, "packages/solid-web/src")), + ...collectTsFiles(path.join(root, "packages/solid-signals/src")), + ]; + + const host = ts.createCompilerHost(options); + const originalResolve = ts.resolveModuleName; + host.resolveModuleNames = (moduleNames, containingFile) => + moduleNames.map((moduleName) => { + const alias = resolveAlias(root, moduleName); + if (alias) { + return { + resolvedFileName: alias, + extension: ts.Extension.Ts, + }; + } + + if (moduleName.startsWith(".")) { + const local = resolveLocalModule(containingFile, moduleName); + if (local) { + return { + resolvedFileName: local, + extension: ts.Extension.Ts, + }; + } + } + + return originalResolve(moduleName, containingFile, options, host) + .resolvedModule; + }); + + return ts.createProgram([...new Set(rootNames)], options, host); +} + +function resolveAlias(root, moduleName) { + const aliases = { + "solid-js": "packages/solid/src/index.ts", + "@solidjs/signals": "packages/solid-signals/src/index.ts", + "@solidjs/web": "packages/solid-web/src/index.ts", + }; + return aliases[moduleName] ? path.join(root, aliases[moduleName]) : undefined; +} + +function resolveLocalModule(containingFile, moduleName) { + const base = path.resolve(path.dirname(containingFile), moduleName); + const candidates = [ + base, + base.replace(/\.js$/, ".ts"), + `${base}.ts`, + path.join(base, "index.ts"), + ]; + return candidates.find((candidate) => fs.existsSync(candidate)); +} + +function collectTsFiles(dir) { + if (!fs.existsSync(dir)) return []; + const files = []; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...collectTsFiles(fullPath)); + } else if ( + entry.isFile() && + fullPath.endsWith(".ts") && + !fullPath.endsWith(".d.ts") + ) { + files.push(fullPath); + } + } + return files; +} + +function collectEntries(program, checker, root) { + const entries = []; + const foldedEntries = []; + const byName = new Map(); + const foldedByName = new Map(); + + for (const entrypoint of ENTRYPOINTS) { + const sourceFile = program.getSourceFile(path.join(root, entrypoint.path)); + if (!sourceFile) { + throw new Error(`Entrypoint not found: ${entrypoint.path}`); + } + + const moduleSymbol = checker.getSymbolAtLocation(sourceFile); + if (!moduleSymbol) continue; + + for (const exportedSymbol of checker.getExportsOfModule(moduleSymbol)) { + const name = exportedSymbol.getName(); + if (shouldSkipName(name)) continue; + + const symbol = resolveSymbol(checker, exportedSymbol); + const declarations = declarationList(symbol).filter((declaration) => + isSourceDeclaration(root, declaration) + ); + if (!declarations.length) continue; + + const docs = getDocs(declarations); + const disposition = getDisposition(name, docs); + if (disposition.type === "hidden") { + continue; + } + if (disposition.type === "missing") { + throw new Error( + `Missing reference disposition for exported symbol "${name}". Add it to CANONICAL_ROUTES, ADVANCED_ROUTES, STANDALONE_TYPE_ROUTES, FOLD_INTO, HIDDEN_EXPORTS, or mark it @internal upstream.` + ); + } + const primary = selectPrimaryDeclaration(declarations); + const entry = createEntry({ + name, + entrypoint, + primary, + root, + declarations, + docs, + checker, + exportedSymbol, + symbol, + disposition, + }); + + if (disposition.type === "fold") { + const existing = foldedByName.get(name); + if (existing) { + mergeDuplicateEntry(existing, entry); + } else { + foldedByName.set(name, entry); + foldedEntries.push(entry); + } + continue; + } + + const existing = byName.get(name); + if (existing) { + mergeDuplicateEntry(existing, entry); + } else { + byName.set(name, entry); + entries.push(entry); + } + } + } + + return { + pages: entries.sort( + (a, b) => + a.route.outputPath.localeCompare(b.route.outputPath) || + a.name.localeCompare(b.name) + ), + folds: foldedEntries.sort((a, b) => a.name.localeCompare(b.name)), + }; +} + +function createEntry({ + name, + entrypoint, + primary, + root, + declarations, + docs, + checker, + exportedSymbol, + symbol, + disposition, +}) { + const sourceFilePath = primary.getSourceFile().fileName; + const sourcePath = relativePath(root, sourceFilePath); + const line = + primary.getSourceFile().getLineAndCharacterOfPosition(primary.getStart()) + .line + 1; + return { + name, + packageName: entrypoint.packageName, + kind: getKind(primary), + sourcePath, + line, + docs, + signature: getSignature(name, declarations, checker), + route: disposition.route, + foldTargets: disposition.targets ?? [], + aliases: getExportAliases(name, exportedSymbol, symbol), + }; +} + +function mergeDuplicateEntry(existing, incoming) { + if (!existing.docs.summary && incoming.docs.summary) { + existing.kind = incoming.kind; + existing.sourcePath = incoming.sourcePath; + existing.line = incoming.line; + existing.docs = incoming.docs; + existing.signature = incoming.signature; + existing.aliases = incoming.aliases; + } +} + +function shouldSkipName(name) { + return false; +} + +function resolveSymbol(checker, symbol) { + if ((symbol.flags & ts.SymbolFlags.Alias) !== 0) { + try { + return checker.getAliasedSymbol(symbol); + } catch { + return symbol; + } + } + return symbol; +} + +function declarationList(symbol) { + return symbol.declarations ?? []; +} + +function isSourceDeclaration(root, declaration) { + const fileName = declaration.getSourceFile().fileName; + return fileName.startsWith(path.join(root, "packages")); +} + +function selectPrimaryDeclaration(declarations) { + const withDocs = declarations.find( + (declaration) => getDocs([declaration]).summary + ); + return withDocs ?? declarations[0]; +} + +function getKind(declaration) { + if ( + ts.isInterfaceDeclaration(declaration) || + ts.isTypeAliasDeclaration(declaration) || + ts.isTypeParameterDeclaration(declaration) + ) { + return "type"; + } + if (ts.isModuleDeclaration(declaration)) { + return "namespace"; + } + return "value"; +} + +function getDocs(declarations) { + const result = { + summary: "", + params: [], + returns: "", + examples: [], + deprecated: "", + internal: false, + remarks: "", + description: "", + }; + + for (const declaration of declarations) { + const docs = declaration.jsDoc ?? []; + for (const doc of docs) { + const summary = normalizeMarkdown(renderComment(doc.comment)); + if (summary) result.summary = summary; + + for (const tag of doc.tags ?? []) { + const tagName = tag.tagName.getText(); + const comment = normalizeMarkdown(renderComment(tag.comment)); + if (tagName === "param" && ts.isJSDocParameterTag(tag)) { + result.params.push({ + name: tag.name.getText(), + text: comment, + }); + } else if (tagName === "returns" || tagName === "return") { + result.returns = comment; + } else if (tagName === "example") { + if (comment) result.examples.push(comment); + } else if (tagName === "deprecated") { + result.deprecated = comment || "Deprecated."; + } else if (tagName === "internal") { + result.internal = true; + } else if (tagName === "remarks") { + result.remarks = comment; + } else if (tagName === "description") { + result.description = comment; + } + } + } + } + + return result; +} + +function renderComment(comment) { + if (!comment) return ""; + if (typeof comment === "string") return comment; + return comment.map((part) => renderCommentPart(part)).join(""); +} + +function renderCommentPart(part) { + if ( + ts.isJSDocLink?.(part) || + ts.isJSDocLinkCode?.(part) || + ts.isJSDocLinkPlain?.(part) + ) { + return renderDocLink(part); + } + if (typeof part.text === "string") return part.text; + return part.getText?.() ?? ""; +} + +function renderDocLink(part) { + const name = part.name?.getText?.(); + const text = part.text?.trim(); + const label = text || name; + return label ? `\`${label}\`` : ""; +} + +function normalizeMarkdown(value) { + return String(value) + .replace(/\r\n/g, "\n") + .replace(/^\s+\*\s/gm, "") + .trim(); +} + +function getSignature(name, declarations, checker) { + const functionDeclarations = declarations.filter(ts.isFunctionDeclaration); + if (functionDeclarations.length) { + const overloads = functionDeclarations.filter( + (declaration) => !declaration.body + ); + const targets = overloads.length ? overloads : functionDeclarations; + return targets + .map((declaration) => cleanSignature(signatureFromNode(declaration))) + .join("\n"); + } + + const classDeclaration = declarations.find(ts.isClassDeclaration); + if (classDeclaration) + return cleanSignature(signatureFromNode(classDeclaration)); + + const interfaceDeclaration = declarations.find(ts.isInterfaceDeclaration); + if (interfaceDeclaration) + return cleanSignature(signatureFromNode(interfaceDeclaration)); + + const typeAlias = declarations.find(ts.isTypeAliasDeclaration); + if (typeAlias) return cleanSignature(signatureFromNode(typeAlias)); + + const variableDeclaration = declarations.find(ts.isVariableDeclaration); + if (variableDeclaration) { + const type = checker.typeToString( + checker.getTypeOfSymbolAtLocation( + checker.getSymbolAtLocation(variableDeclaration.name) ?? + checker.getTypeAtLocation(variableDeclaration).symbol, + variableDeclaration + ), + variableDeclaration, + ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.UseFullyQualifiedType + ); + return `const ${name}: ${type};`; + } + + const enumDeclaration = declarations.find(ts.isEnumDeclaration); + if (enumDeclaration) + return cleanSignature(signatureFromNode(enumDeclaration)); + + const primary = declarations[0]; + return cleanSignature(signatureFromNode(primary)); +} + +function signatureFromNode(node) { + const sourceFile = node.getSourceFile(); + if (ts.isFunctionDeclaration(node) && node.body) { + return `${sourceFile.text.slice(node.getStart(sourceFile), node.body.getStart(sourceFile)).trim()};`; + } + const text = node.getText(sourceFile); + const withoutDocs = text.replace(/^\/\*\*[\s\S]*?\*\/\s*/g, ""); + return withoutDocs; +} + +function cleanSignature(value) { + return value + .replace(/\r\n/g, "\n") + .replace(/^export\s+/gm, "") + .replace(/^declare\s+/gm, "") + .replace(/\n{3,}/g, "\n\n") + .trim() + .replace(/;?$/, ";"); +} + +function getExportAliases(name, exportedSymbol, resolvedSymbol) { + return exportedSymbol === resolvedSymbol || + exportedSymbol.getName() === resolvedSymbol.getName() + ? [] + : [resolvedSymbol.getName()].filter((alias) => alias !== name); +} + +function getDisposition(name, docs) { + if (docs.internal || HIDDEN_EXPORTS.has(name) || name.startsWith("$")) { + return { type: "hidden" }; + } + + if (FOLD_INTO[name]) { + return { type: "fold", targets: normalizeTargets(FOLD_INTO[name]) }; + } + + const route = getRouteForSymbol(name); + if (route) { + return { type: route.visibility, route }; + } + + return { type: "missing" }; +} + +function normalizeTargets(target) { + return Array.isArray(target) ? target : [target]; +} + +function getRouteForSymbol(name) { + for (const [type, routes] of [ + ["canonical", CANONICAL_ROUTES], + ["advanced", ADVANCED_ROUTES], + ["type", STANDALONE_TYPE_ROUTES], + ]) { + const mapped = routes[name]; + if (!mapped) continue; + return { + path: mapped[0], + outputPath: orderRoutePath(mapped[0]), + category: mapped[1], + mapped: true, + visibility: type, + }; + } +} + +function orderRoutePath(routePath) { + const parts = routePath.split("/"); + return parts + .map((part, index) => { + if (index === 0) return TOP_LEVEL_ROUTE_ORDER[part] ?? part; + if (parts[0] === "advanced" && index === 1) { + return ADVANCED_ROUTE_ORDER[part] ?? part; + } + return part; + }) + .join("/"); +} + +function buildOutputFiles(reference, source) { + const grouped = new Map(); + for (const entry of reference.pages) { + const routePath = entry.route.outputPath; + const current = grouped.get(routePath); + if (!current) { + grouped.set(routePath, [entry]); + } else { + current.push(entry); + } + } + + const foldsByRoute = collectFoldsByRoute(reference.folds); + + return [...grouped.entries()].map(([routePath, routeEntries]) => { + const primary = routeEntries[0]; + return { + routePath, + fullPath: path.join(outDir, routePath), + content: renderMdx( + routeEntries, + primary.route.category, + source, + foldsByRoute.get(routePath) ?? [] + ), + }; + }); +} + +function collectFoldsByRoute(folds) { + const byRoute = new Map(); + for (const entry of folds) { + for (const target of entry.foldTargets) { + const route = getRouteForSymbol(target); + if (!route) { + throw new Error( + `Fold target "${target}" for "${entry.name}" does not have a reference route.` + ); + } + const current = byRoute.get(route.outputPath); + if (!current) { + byRoute.set(route.outputPath, [entry]); + } else if (!current.some((existing) => existing.name === entry.name)) { + current.push(entry); + } + } + } + for (const entries of byRoute.values()) { + entries.sort((a, b) => a.name.localeCompare(b.name)); + } + return byRoute; +} + +function renderMdx(entries, category, source, foldedEntries = []) { + const primary = entries[0]; + const description = + escapeMdxText(primary.docs.summary.split("\n\n")[0]) || + `${primary.name} API reference.`; + const title = + entries.length === 1 + ? primary.name + : entries.map((entry) => entry.name).join(" / "); + const frontmatter = [ + "---", + `title: ${yamlString(title)}`, + `category: ${yamlString(category)}`, + 'version: "2.0"', + `description: ${yamlString(description)}`, + `source_repo: "solidjs/solid"`, + `source_ref: ${yamlString(source.sourceRef)}`, + `source_sha: ${yamlString(source.sourceSha)}`, + `source_path: ${yamlString(primary.sourcePath)}`, + "---", + ].join("\n"); + + const body = entries + .map((entry) => renderEntry(entry, source)) + .join("\n\n---\n\n"); + const relatedTypes = foldedEntries.length + ? `\n\n${renderRelatedTypes(foldedEntries)}` + : ""; + return `${frontmatter} + +{/* ${GENERATED_MARKER} */} + +${body}${relatedTypes} +`; +} + +function renderEntry(entry) { + const blocks = []; + blocks.push( + escapeMdxText(entry.docs.summary) || `${entry.name} API reference.` + ); + + if (ENTRY_CALLOUTS[entry.name]) { + blocks.push(ENTRY_CALLOUTS[entry.name]); + } + + if (entry.docs.deprecated) { + blocks.push(`> Deprecated: ${escapeMdxText(entry.docs.deprecated)}`); + } + + blocks.push(`## Import + +\`\`\`ts +${entry.kind === "type" || entry.kind === "namespace" ? "import type" : "import"} { ${entry.name} } from "${entry.packageName}"; +\`\`\``); + + blocks.push(`## Type signature + +\`\`\`ts +${entry.signature} +\`\`\``); + + if (entry.docs.params.length) { + blocks.push(`## Parameters + +${entry.docs.params.map((param) => `### \`${param.name}\`\n\n${escapeMdxText(param.text) || "No description provided."}`).join("\n\n")}`); + } + + if (entry.docs.returns) { + blocks.push(`## Return value + +${escapeMdxText(entry.docs.returns)}`); + } + + if (entry.docs.remarks) { + blocks.push(`## Remarks + +${escapeMdxText(entry.docs.remarks)}`); + } + + if (entry.docs.examples.length) { + blocks.push(`## Examples + +${entry.docs.examples.map(formatExample).join("\n\n")}`); + } + + return blocks.join("\n\n"); +} + +function renderRelatedTypes(entries) { + return `## Related types + +${entries.map(renderRelatedType).join("\n\n")}`; +} + +function renderRelatedType(entry) { + const blocks = [`### \`${entry.name}\``]; + const summary = escapeMdxText(entry.docs.summary); + if (summary) blocks.push(summary); + blocks.push(`\`\`\`ts +${entry.signature} +\`\`\``); + if (entry.docs.remarks) { + blocks.push(escapeMdxText(entry.docs.remarks)); + } + return blocks.join("\n\n"); +} + +function formatExample(example) { + if (example.includes("```")) return example; + return `\`\`\`ts +${example} +\`\`\``; +} + +function escapeMdxText(value) { + return splitFencedCode(value) + .map((part) => + part.fence ? part.text : escapeAnglesOutsideInlineCode(part.text) + ) + .join(""); +} + +function splitFencedCode(value) { + const parts = []; + const pattern = /```[\s\S]*?```/g; + let lastIndex = 0; + let match; + while ((match = pattern.exec(value))) { + if (match.index > lastIndex) + parts.push({ fence: false, text: value.slice(lastIndex, match.index) }); + parts.push({ fence: true, text: match[0] }); + lastIndex = pattern.lastIndex; + } + if (lastIndex < value.length) + parts.push({ fence: false, text: value.slice(lastIndex) }); + return parts; +} + +function escapeAnglesOutsideInlineCode(value) { + return value + .split(/(`[^`]*`)/g) + .map((part) => + part.startsWith("`") + ? part + : part.replace( + /<([A-Za-z][^>\n]*)>/g, + (_match, inner) => `<${inner}>` + ) + ) + .join(""); +} + +function writeFiles(files) { + cleanGeneratedFiles(outDir); + for (const file of files) { + fs.mkdirSync(path.dirname(file.fullPath), { recursive: true }); + fs.writeFileSync(file.fullPath, file.content); + } +} + +function cleanGeneratedFiles(dir) { + if (!fs.existsSync(dir)) return; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + cleanGeneratedFiles(fullPath); + if (fs.readdirSync(fullPath).length === 0) fs.rmdirSync(fullPath); + continue; + } + if (!entry.isFile() || !fullPath.endsWith(".mdx")) continue; + const content = fs.readFileSync(fullPath, "utf8"); + if (content.includes("Generated by scripts/extract-solid-")) { + fs.unlinkSync(fullPath); + } + } +} + +function printDryRun(files, reference, sourceSha) { + const visibility = reference.pages.reduce((counts, entry) => { + counts[entry.route.visibility] = (counts[entry.route.visibility] ?? 0) + 1; + return counts; + }, {}); + console.log(`Solid source SHA: ${sourceSha}`); + console.log( + `Entries: ${reference.pages.length} (${Object.entries(visibility) + .map(([key, value]) => `${value} ${key}`) + .join(", ")})` + ); + console.log(`Folded: ${reference.folds.length}`); + console.log(`Files: ${files.length}`); + for (const file of files) { + console.log(path.relative(repoRoot, file.fullPath)); + } +} + +function relativePath(root, filePath) { + return path.relative(root, filePath).split(path.sep).join("/"); +} + +function slugify(value) { + return value + .replace(/^\$/, "dollar-") + .replace(/([a-z0-9])([A-Z])/g, "$1-$2") + .replace(/[^a-zA-Z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") + .toLowerCase(); +} + +function yamlString(value) { + return JSON.stringify(value); +} diff --git a/src/routes/v2/reference/(1)reactivity/create-effect.mdx b/src/routes/v2/reference/(1)reactivity/create-effect.mdx new file mode 100644 index 000000000..cb8fc9416 --- /dev/null +++ b/src/routes/v2/reference/(1)reactivity/create-effect.mdx @@ -0,0 +1,148 @@ +--- +title: "createEffect" +category: "Reactivity" +version: "2.0" +description: "Creates a reactive effect with **separate compute and effect phases**." +source_repo: "solidjs/solid" +source_ref: "next" +source_sha: "a93a216ef35bd053949493506942884fb24e7684" +source_path: "packages/solid-signals/src/signals.ts" +--- + +{/* Generated by scripts/extract-solid-ref.mjs. Edit the source JSDoc or disposition map, then regenerate. */} + +Creates a reactive effect with **separate compute and effect phases**. + +- `compute(prev)` runs reactively — _put all reactive reads here_. The + returned value is passed to `effect` and is also the new "previous" value + for the next run. +- `effect(next, prev?)` runs imperatively (untracked) after the queue + flushes. _Put DOM writes / fetch / logging / subscriptions here._ It may + return a cleanup function which runs before the next effect or on + disposal. + +Reactive reads inside `effect` will _not_ re-trigger this effect — that's +intentional. If you need a single-phase tracked effect, use +`createTrackedEffect` (with the tradeoffs noted there). + +Pass an `EffectBundle` (`{ effect, error }`) instead of a plain function to +intercept errors thrown from the compute or effect phases. + +```typescript +createEffect(compute, effectFn | { effect, error }, options?: EffectOptions); +``` + +> Deprecated: `createEffect(compute)` (single argument) is no longer supported. +> Pass a separate effect function as the second argument: +> `createEffect(compute, effect)`. See [MISSING_EFFECT_FN]. + +- For a side effect that reacts to changes, split the work: + `createEffect(() => signal(), value => doWork(value))`. +- For a derived value, use `createMemo(() => signal())`. +- For a one-shot side effect at construction time, just call the function. + +## Import + +```ts +import { createEffect } from "solid-js"; +``` + +## Type signature + +```ts +function createEffect( + compute: ComputeFunction, T>, + effectFn: EffectFunction, T> | EffectBundle, T>, + options?: EffectOptions +): void; +function createEffect( + compute: ComputeFunction, T> +): never; +``` + +## Parameters + +### `compute` + +a function that receives its previous value and returns a new value used to react on a computation + +### `effectFn` + +a function that receives the new value and is used to perform side effects (return a cleanup function), or an `EffectBundle` with `effect` and `error` handlers + +### `options` + +`EffectOptions` -- name, defer, schedule + +## Examples + +```ts +const [count, setCount] = createSignal(0); + +createEffect( + () => count(), // compute: tracks `count` + (value) => console.log(value) // effect: side effect +); + +setCount(1); // logs 1 after the next flush +``` + +```ts +createEffect( + () => userId(), + (id) => { + const ctrl = new AbortController(); + fetch(`/users/${id}`, { signal: ctrl.signal }); + return () => ctrl.abort(); // cleanup before next run / disposal + } +); +``` + +## Related types + +### `ComputeFunction` + +```ts +type ComputeFunction = ( + v: Prev +) => PromiseLike | AsyncIterable | Next; +``` + +### `EffectBundle` + +```ts +type EffectBundle = { + effect: EffectFunction; + error: (err: unknown, cleanup: () => void) => void; +}; +``` + +### `EffectFunction` + +```ts +type EffectFunction = ( + v: Next, + p?: Prev +) => (() => void) | void; +``` + +### `EffectOptions` + +Options for effect primitives that support deferring/scheduling their initial run (`createEffect`, `createRenderEffect`, `createReaction`). + +```ts +interface EffectOptions extends BaseEffectOptions { + /** When true, defers the initial effect execution until the next change */ + defer?: boolean; + /** + * When true, enqueues the initial effect callback through the effect queue instead of running + * it synchronously at creation. Lets the initial run participate in transitions -- if any + * source throws `NotReadyError` during the compute phase, the callback is held until the + * transition settles. + * + * Primarily for render effects that need transition-aware initial mounts (e.g. the root + * `insert()` in `render()`). + */ + schedule?: boolean; +} +``` diff --git a/src/routes/v2/reference/(1)reactivity/create-memo.mdx b/src/routes/v2/reference/(1)reactivity/create-memo.mdx new file mode 100644 index 000000000..897d590c1 --- /dev/null +++ b/src/routes/v2/reference/(1)reactivity/create-memo.mdx @@ -0,0 +1,127 @@ +--- +title: "createMemo" +category: "Reactivity" +version: "2.0" +description: "Creates a readonly derived reactive memoized signal." +source_repo: "solidjs/solid" +source_ref: "next" +source_sha: "a93a216ef35bd053949493506942884fb24e7684" +source_path: "packages/solid-signals/src/signals.ts" +--- + +{/* Generated by scripts/extract-solid-ref.mjs. Edit the source JSDoc or disposition map, then regenerate. */} + +Creates a readonly derived reactive memoized signal. + +```typescript +const value = createMemo(compute, options?: MemoOptions); +``` + +## Import + +```ts +import { createMemo } from "solid-js"; +``` + +## Type signature + +```ts +function createMemo( + compute: ComputeFunction, T>, + options?: MemoOptions +): Accessor; +``` + +## Parameters + +### `compute` + +a function that receives its previous value and returns a new value used to react on a computation + +### `options` + +`MemoOptions` -- id, name, equals, unobserved, lazy + +## Examples + +```ts +const [first, setFirst] = createSignal("Ada"); +const [last, setLast] = createSignal("Lovelace"); + +const fullName = createMemo(() => `${first()} ${last()}`); + +fullName(); // "Ada Lovelace" +``` + +```ts +// Async memo — reads surface as pending inside +const user = createMemo(async () => { + const res = await fetch(`/users/${id()}`); + return res.json(); +}); +``` + +## Related types + +### `isEqual` + +```ts +function isEqual(a: T, b: T): boolean; +``` + +### `MemoOptions` + +Options for read-only memos created with `createMemo`. +Also used in combination with `SignalOptions` for writable memos +(`createSignal(fn)` / `createOptimistic(fn)`). + +```ts +interface MemoOptions { + /** Stable identifier for the owner hierarchy */ + id?: string; + /** Debug name (dev mode only) */ + name?: string; + /** When true, the owner is invisible to the ID scheme -- inherits parent ID and doesn't consume a childCount slot */ + transparent?: boolean; + /** + * Custom equality function, or `false` to always notify subscribers. + * Defaults to reference equality (`isEqual`). Pass a comparator (e.g. + * `(a, b) => a.id === b.id`) for value-based equality, or `false` to + * notify on every recompute regardless of equality. + */ + equals?: false | ((prev: T, next: T) => boolean); + /** Callback invoked when the computed loses all subscribers */ + unobserved?: () => void; + /** + * When true, defers the initial computation until the value is first read, + * **and** opts the memo into autodisposal — once it has no remaining + * subscribers it is torn down and recomputed from scratch on the next read. + * Use it for compute-on-demand values that should not retain state across + * idle periods. Non-lazy owned memos live for their owner's lifetime and + * never autodispose. + */ + lazy?: boolean; +} +``` + +### `SignalOptions` + +Options for plain signals created with `createSignal(value)` or `createOptimistic(value)`. + +```ts +interface SignalOptions { + /** Debug name (dev mode only) */ + name?: string; + /** + * Custom equality function, or `false` to always notify subscribers. + * Defaults to reference equality (`isEqual`). Pass a comparator (e.g. + * `(a, b) => a.id === b.id`) for value-based equality, or `false` to + * notify on every write regardless of equality. + */ + equals?: false | ((prev: T, next: T) => boolean); + /** Suppress dev-mode warnings when writing inside an owned scope */ + ownedWrite?: boolean; + /** Callback invoked when the signal loses all subscribers */ + unobserved?: () => void; +} +``` diff --git a/src/routes/v2/reference/(1)reactivity/create-optimistic.mdx b/src/routes/v2/reference/(1)reactivity/create-optimistic.mdx new file mode 100644 index 000000000..9b9731b7b --- /dev/null +++ b/src/routes/v2/reference/(1)reactivity/create-optimistic.mdx @@ -0,0 +1,72 @@ +--- +title: "createOptimistic" +category: "Reactivity" +version: "2.0" +description: "Creates an optimistic signal that can be used to optimistically update a value\nand then revert it back to the previous value at end of transition." +source_repo: "solidjs/solid" +source_ref: "next" +source_sha: "a93a216ef35bd053949493506942884fb24e7684" +source_path: "packages/solid-signals/src/signals.ts" +--- + +{/* Generated by scripts/extract-solid-ref.mjs. Edit the source JSDoc or disposition map, then regenerate. */} + +Creates an optimistic signal that can be used to optimistically update a value +and then revert it back to the previous value at end of transition. + +When called with a plain value, creates an optimistic signal with `SignalOptions` (name, equals, ownedWrite, unobserved). +When called with a function, creates a writable optimistic memo with `SignalOptions & MemoOptions` (adds id, lazy). + +```typescript +// Plain optimistic signal +const [state, setState] = createOptimistic(value, options?: SignalOptions); +// Writable optimistic memo (function overload) +const [state, setState] = createOptimistic(fn, options?: SignalOptions & MemoOptions); +``` + +## Import + +```ts +import { createOptimistic } from "solid-js"; +``` + +## Type signature + +```ts +function createOptimistic(): Signal; +function createOptimistic( + value: Exclude, + options?: SignalOptions +): Signal; +function createOptimistic( + fn: ComputeFunction, + options?: SignalOptions & MemoOptions +): Signal; +``` + +## Parameters + +### `value` + +initial value of the signal; if empty, the signal's type will automatically extended with undefined + +### `options` + +optional object with a name for debugging purposes and equals, a comparator function for the previous and next value to allow fine-grained control over the reactivity + +## Return value + +`[state: Accessor, setState: Setter]` + +## Examples + +```ts +const [todos, setTodos] = createOptimistic(initialTodos); + +const addTodo = action(function* (text: string) { + const tempId = crypto.randomUUID(); + setTodos((t) => [...t, { id: tempId, text, pending: true }]); // optimistic + const saved = yield api.createTodo(text); + setTodos((t) => t.map((x) => (x.id === tempId ? saved : x))); // reconcile +}); +``` diff --git a/src/routes/v2/reference/(1)reactivity/create-signal.mdx b/src/routes/v2/reference/(1)reactivity/create-signal.mdx new file mode 100644 index 000000000..e2dd82094 --- /dev/null +++ b/src/routes/v2/reference/(1)reactivity/create-signal.mdx @@ -0,0 +1,146 @@ +--- +title: "createSignal" +category: "Reactivity" +version: "2.0" +description: "Creates a simple reactive state with a getter and setter." +source_repo: "solidjs/solid" +source_ref: "next" +source_sha: "a93a216ef35bd053949493506942884fb24e7684" +source_path: "packages/solid-signals/src/signals.ts" +--- + +{/* Generated by scripts/extract-solid-ref.mjs. Edit the source JSDoc or disposition map, then regenerate. */} + +Creates a simple reactive state with a getter and setter. + +When called with a plain value, creates a signal with `SignalOptions` (name, equals, ownedWrite, unobserved). +When called with a function, creates a writable memo with `SignalOptions & MemoOptions` (adds id, lazy). + +```typescript +// Plain signal +const [state, setState] = createSignal(value, options?: SignalOptions); +// Writable memo (function overload) +const [state, setState] = createSignal(fn, initialValue?, options?: SignalOptions & MemoOptions); +``` + +## Import + +```ts +import { createSignal } from "solid-js"; +``` + +## Type signature + +```ts +function createSignal(): Signal; +function createSignal( + value: Exclude, + options?: SignalOptions +): Signal; +function createSignal( + fn: ComputeFunction, + options?: SignalOptions & MemoOptions +): Signal; +``` + +## Parameters + +### `value` + +initial value of the state; if empty, the state's type will automatically extended with undefined + +### `options` + +optional object with a name for debugging purposes and equals, a comparator function for the previous and next value to allow fine-grained control over the reactivity + +## Return value + +`[state: Accessor, setState: Setter]` + +## Examples + +```ts +const [count, setCount] = createSignal(0); + +count(); // 0 +setCount(1); // explicit value +setCount((c) => c + 1); // updater +``` + +```ts +// Writable memo: starts as `fn()`, can be locally overwritten by setter. +const [user, setUser] = createSignal(() => fetchUser(userId())); + +setUser({ ...user(), name: "Alice" }); // optimistic local edit +``` + +## Related types + +### `isEqual` + +```ts +function isEqual(a: T, b: T): boolean; +``` + +### `MemoOptions` + +Options for read-only memos created with `createMemo`. +Also used in combination with `SignalOptions` for writable memos +(`createSignal(fn)` / `createOptimistic(fn)`). + +```ts +interface MemoOptions { + /** Stable identifier for the owner hierarchy */ + id?: string; + /** Debug name (dev mode only) */ + name?: string; + /** When true, the owner is invisible to the ID scheme -- inherits parent ID and doesn't consume a childCount slot */ + transparent?: boolean; + /** + * Custom equality function, or `false` to always notify subscribers. + * Defaults to reference equality (`isEqual`). Pass a comparator (e.g. + * `(a, b) => a.id === b.id`) for value-based equality, or `false` to + * notify on every recompute regardless of equality. + */ + equals?: false | ((prev: T, next: T) => boolean); + /** Callback invoked when the computed loses all subscribers */ + unobserved?: () => void; + /** + * When true, defers the initial computation until the value is first read, + * **and** opts the memo into autodisposal — once it has no remaining + * subscribers it is torn down and recomputed from scratch on the next read. + * Use it for compute-on-demand values that should not retain state across + * idle periods. Non-lazy owned memos live for their owner's lifetime and + * never autodispose. + */ + lazy?: boolean; +} +``` + +### `NoInfer` + +```ts +type NoInfer = [T][T extends any ? 0 : never]; +``` + +### `SignalOptions` + +Options for plain signals created with `createSignal(value)` or `createOptimistic(value)`. + +```ts +interface SignalOptions { + /** Debug name (dev mode only) */ + name?: string; + /** + * Custom equality function, or `false` to always notify subscribers. + * Defaults to reference equality (`isEqual`). Pass a comparator (e.g. + * `(a, b) => a.id === b.id`) for value-based equality, or `false` to + * notify on every write regardless of equality. + */ + equals?: false | ((prev: T, next: T) => boolean); + /** Suppress dev-mode warnings when writing inside an owned scope */ + ownedWrite?: boolean; + /** Callback invoked when the signal loses all subscribers */ + unobserved?: () => void; +} +``` diff --git a/src/routes/v2/reference/(1)reactivity/flush.mdx b/src/routes/v2/reference/(1)reactivity/flush.mdx new file mode 100644 index 000000000..2a1a325c8 --- /dev/null +++ b/src/routes/v2/reference/(1)reactivity/flush.mdx @@ -0,0 +1,43 @@ +--- +title: "flush" +category: "Reactivity" +version: "2.0" +description: "Synchronously processes the pending reactive queue: runs every scheduled\nmemo/effect/computation that has dirty inputs, until the graph is settled." +source_repo: "solidjs/solid" +source_ref: "next" +source_sha: "a93a216ef35bd053949493506942884fb24e7684" +source_path: "packages/solid-signals/src/core/scheduler.ts" +--- + +{/* Generated by scripts/extract-solid-ref.mjs. Edit the source JSDoc or disposition map, then regenerate. */} + +Synchronously processes the pending reactive queue: runs every scheduled +memo/effect/computation that has dirty inputs, until the graph is settled. + +Reactive updates are normally batched onto the microtask queue, so multiple +writes in a row collapse into a single update pass. Call `flush()` when you +need to _observe_ the result of those writes synchronously — most commonly +in tests, but also at the boundary of imperative integration code. + +## Import + +```ts +import { flush } from "solid-js"; +``` + +## Type signature + +```ts +function flush(): void; +``` + +## Examples + +```ts +const [count, setCount] = createSignal(0); +const doubled = createMemo(() => count() * 2); + +setCount(5); +flush(); +expect(doubled()).toBe(10); +``` diff --git a/src/routes/v2/reference/(1)reactivity/is-pending.mdx b/src/routes/v2/reference/(1)reactivity/is-pending.mdx new file mode 100644 index 000000000..0eb3d1ec4 --- /dev/null +++ b/src/routes/v2/reference/(1)reactivity/is-pending.mdx @@ -0,0 +1,39 @@ +--- +title: "isPending" +category: "Reactivity" +version: "2.0" +description: "Returns `true` if any reactive read inside `fn` is currently in a pending\n(async, not-yet-settled) state. Does not subscribe — pair with a tracked\nmemo if you want to react to pending status changes." +source_repo: "solidjs/solid" +source_ref: "next" +source_sha: "a93a216ef35bd053949493506942884fb24e7684" +source_path: "packages/solid-signals/src/core/core.ts" +--- + +{/* Generated by scripts/extract-solid-ref.mjs. Edit the source JSDoc or disposition map, then regenerate. */} + +Returns `true` if any reactive read inside `fn` is currently in a pending +(async, not-yet-settled) state. Does not subscribe — pair with a tracked +memo if you want to react to pending status changes. + +Useful for showing inline transition indicators alongside the previous +value (rather than swapping to a `` fallback). + +## Import + +```ts +import { isPending } from "solid-js"; +``` + +## Type signature + +```ts +function isPending(fn: () => any): boolean; +``` + +## Examples + +```tsx +const pending = createMemo(() => isPending(() => user())); + +; +``` diff --git a/src/routes/v2/reference/(1)reactivity/latest.mdx b/src/routes/v2/reference/(1)reactivity/latest.mdx new file mode 100644 index 000000000..5933774f4 --- /dev/null +++ b/src/routes/v2/reference/(1)reactivity/latest.mdx @@ -0,0 +1,41 @@ +--- +title: "latest" +category: "Reactivity" +version: "2.0" +description: "Reads reactive expressions while bypassing any pending async overlay — i.e.\nalways returns the most-recently-committed value, even when newer reads\ninside `fn` are still in flight." +source_repo: "solidjs/solid" +source_ref: "next" +source_sha: "a93a216ef35bd053949493506942884fb24e7684" +source_path: "packages/solid-signals/src/core/core.ts" +--- + +{/* Generated by scripts/extract-solid-ref.mjs. Edit the source JSDoc or disposition map, then regenerate. */} + +Reads reactive expressions while bypassing any pending async overlay — i.e. +always returns the most-recently-committed value, even when newer reads +inside `fn` are still in flight. + +Useful inside a `` boundary's children when you want to keep +showing the previous resolved data instead of the fallback while the next +value loads. + +## Import + +```ts +import { latest } from "solid-js"; +``` + +## Type signature + +```ts +function latest(fn: () => T): T; +``` + +## Examples + +```tsx +}> + {/* During a transition, render the previous user instead of skeleton: *\/} + user())} /> + +``` diff --git a/src/routes/v2/reference/(1)reactivity/untrack.mdx b/src/routes/v2/reference/(1)reactivity/untrack.mdx new file mode 100644 index 000000000..b2067acd1 --- /dev/null +++ b/src/routes/v2/reference/(1)reactivity/untrack.mdx @@ -0,0 +1,46 @@ +--- +title: "untrack" +category: "Reactivity" +version: "2.0" +description: "Runs `fn` outside of any reactive tracking — reads inside `fn` will not\nsubscribe the current scope. Returns whatever `fn` returns." +source_repo: "solidjs/solid" +source_ref: "next" +source_sha: "a93a216ef35bd053949493506942884fb24e7684" +source_path: "packages/solid-signals/src/core/core.ts" +--- + +{/* Generated by scripts/extract-solid-ref.mjs. Edit the source JSDoc or disposition map, then regenerate. */} + +Runs `fn` outside of any reactive tracking — reads inside `fn` will not +subscribe the current scope. Returns whatever `fn` returns. + +Use `untrack` inside a memo or effect when you need to read a signal once +without making the surrounding computation depend on its future changes. + +Pass a `strictReadLabel` string to enable a dev-mode warning: any reactive +read inside `fn` that isn't inside a nested tracking scope will log a +warning naming the label. + +## Import + +```ts +import { untrack } from "solid-js"; +``` + +## Type signature + +```ts +function untrack(fn: () => T, strictReadLabel?: string | false): T; +``` + +## Examples + +```ts +createEffect( + () => trigger(), // tracks `trigger` only + () => { + const snapshot = untrack(() => state); // read once, untracked + log(snapshot); + } +); +``` diff --git a/src/routes/v2/reference/(2)stores/create-optimistic-store.mdx b/src/routes/v2/reference/(2)stores/create-optimistic-store.mdx new file mode 100644 index 000000000..2e6564012 --- /dev/null +++ b/src/routes/v2/reference/(2)stores/create-optimistic-store.mdx @@ -0,0 +1,91 @@ +--- +title: "createOptimisticStore" +category: "Stores" +version: "2.0" +description: "The store equivalent of `createOptimistic`. Writes inside an `action`\ntransition are tentative — they show up immediately but auto-revert (or\nreconcile to the action's resolved value) once the transition finishes." +source_repo: "solidjs/solid" +source_ref: "next" +source_sha: "a93a216ef35bd053949493506942884fb24e7684" +source_path: "packages/solid-signals/src/store/optimistic.ts" +--- + +{/* Generated by scripts/extract-solid-ref.mjs. Edit the source JSDoc or disposition map, then regenerate. */} + +The store equivalent of `createOptimistic`. Writes inside an `action` +transition are tentative — they show up immediately but auto-revert (or +reconcile to the action's resolved value) once the transition finishes. + +Use this for optimistic UI on collection-shaped data. For single-value +optimistic state, prefer `createOptimistic`. + +- Plain form: `createOptimisticStore(initialValue)`. +- Derived form: `createOptimisticStore(fn, seed, options?)` — a projection + store whose authoritative value is recomputed by `fn` and whose + optimistic overlay reverts after each transition. + +`options.key` defaults to `"id"`; specify it only when your data uses a +different identity field (e.g. `{ key: "uuid" }` or `{ key: t => t.slug }`). +Restating the default just adds noise. + +## Import + +```ts +import { createOptimisticStore } from "solid-js"; +``` + +## Type signature + +```ts +function createOptimisticStore( + store: NoFn | Store> +): [get: Store, set: StoreSetter]; +function createOptimisticStore( + fn: (store: T) => void | T | Promise | AsyncIterable, + store: Partial | Store>, + options?: ProjectionOptions +): [get: Refreshable>, set: StoreSetter]; +``` + +## Return value + +`[store: Store, setStore: StoreSetter]` + +## Examples + +```ts +const [todos, setTodos] = createOptimisticStore([]); + +// Mutation: optimistic add, then in-place reconcile to the saved row. +const addTodo = action(function* (text: string) { + const tempId = crypto.randomUUID(); + setTodos((t) => { + t.push({ id: tempId, text, pending: true }); + }); + const saved = yield api.createTodo(text); + setTodos((t) => { + const i = t.findIndex((x) => x.id === tempId); + if (i >= 0) t[i] = saved; + }); +}); + +// Return form: filter is the natural shape for removal. +const removeTodo = action(function* (id: string) { + setTodos((t) => t.filter((x) => x.id !== id)); + yield api.removeTodo(id); +}); +``` + +## Related types + +### `Refreshable` + +Brand applied to derived/projected stores indicating they participate in +the `refresh()` re-run protocol. Use this alias instead of inlining +`T & { [$REFRESH]: any }` so that user-defined hooks that wrap +`createOptimisticStore` / `createProjection` / projection-form +`createStore` can have their return types inferred without leaking the +internal `$REFRESH` symbol into public type signatures (TS4058). + +```ts +type Refreshable = T & { readonly [$REFRESH]: any }; +``` diff --git a/src/routes/v2/reference/(2)stores/create-projection.mdx b/src/routes/v2/reference/(2)stores/create-projection.mdx new file mode 100644 index 000000000..7d61d97fc --- /dev/null +++ b/src/routes/v2/reference/(2)stores/create-projection.mdx @@ -0,0 +1,105 @@ +--- +title: "createProjection" +category: "Stores" +version: "2.0" +description: "Creates a derived (projected) store. Like `createMemo` but for stores: the\nderive function receives a mutable draft and either mutates it in place\n(canonical) or returns a new value. Either way the result is reconciled\nagainst the previous draft by `options.key` (default `\"id\"`), so surviving\nitems keep their proxy identity — only added/removed items are\ncreated/disposed." +source_repo: "solidjs/solid" +source_ref: "next" +source_sha: "a93a216ef35bd053949493506942884fb24e7684" +source_path: "packages/solid-signals/src/store/projection.ts" +--- + +{/* Generated by scripts/extract-solid-ref.mjs. Edit the source JSDoc or disposition map, then regenerate. */} + +Creates a derived (projected) store. Like `createMemo` but for stores: the +derive function receives a mutable draft and either mutates it in place +(canonical) or returns a new value. Either way the result is reconciled +against the previous draft by `options.key` (default `"id"`), so surviving +items keep their proxy identity — only added/removed items are +created/disposed. + +Returns the projected store directly (no setter — reads only). + +Use this when you want the structural-sharing / per-property tracking +behaviour of a store on top of a derived computation. For simple read-only +derivations, `createMemo` is lighter. + +## Import + +```ts +import { createProjection } from "solid-js"; +``` + +## Type signature + +```ts +function createProjection( + fn: (draft: T) => void | T | Promise | AsyncIterable, + seed: Partial, + options?: ProjectionOptions +): Refreshable>; +``` + +## Parameters + +### `fn` + +receives the current draft; mutate it in place or return new +data. Return is convenient for filter/derive shapes where mutation is +awkward. + +### `seed` + +the backing store value to wrap and reconcile into + +### `options` + +`ProjectionOptions` — `name`, `key`. `key` defaults to +`"id"`; specify it only when your data uses a different identity field +(e.g. `{ key: "uuid" }` or `{ key: u => u.slug }`). + +## Examples + +```ts +// Mutation form — update individual fields on the draft. +const summary = createProjection<{ total: number; active: number }>( + (draft) => { + draft.total = users().length; + draft.active = users().filter((u) => u.active).length; + }, + { total: 0, active: 0 } +); + +// Return form — produce a derived collection. Reconciled by `id` so each +// surviving user keeps the same store identity across recomputes. +const activeUsers = createProjection( + () => allUsers().filter((u) => u.active), + [] +); +``` + +## Related types + +### `ProjectionOptions` + +Options for derived/projected stores created with `createStore(fn)`, `createProjection`, or `createOptimisticStore(fn)`. + +```ts +interface ProjectionOptions extends StoreOptions { + /** Key property name or function for reconciliation identity */ + key?: string | ((item: NonNullable) => any); +} +``` + +### `Refreshable` + +Brand applied to derived/projected stores indicating they participate in +the `refresh()` re-run protocol. Use this alias instead of inlining +`T & { [$REFRESH]: any }` so that user-defined hooks that wrap +`createOptimisticStore` / `createProjection` / projection-form +`createStore` can have their return types inferred without leaking the +internal `$REFRESH` symbol into public type signatures (TS4058). + +```ts +type Refreshable = T & { readonly [$REFRESH]: any }; +``` diff --git a/src/routes/v2/reference/(2)stores/create-store.mdx b/src/routes/v2/reference/(2)stores/create-store.mdx new file mode 100644 index 000000000..18585673f --- /dev/null +++ b/src/routes/v2/reference/(2)stores/create-store.mdx @@ -0,0 +1,146 @@ +--- +title: "createStore" +category: "Stores" +version: "2.0" +description: "Creates a deeply-reactive store backed by a Proxy. Reads track each property\naccessed; only the parts that change trigger updates." +source_repo: "solidjs/solid" +source_ref: "next" +source_sha: "a93a216ef35bd053949493506942884fb24e7684" +source_path: "packages/solid-signals/src/store/store.ts" +--- + +{/* Generated by scripts/extract-solid-ref.mjs. Edit the source JSDoc or disposition map, then regenerate. */} + +Creates a deeply-reactive store backed by a Proxy. Reads track each property +accessed; only the parts that change trigger updates. + +Store properties hold **plain values**, not accessors. The proxy already +tracks reads per-property — wrapping a value in `() => state.foo` produces +a getter that _won't_ track when called, which looks like a reactivity bug +but is just a category error. If you have a signal-shaped piece of state, +make it a property of the store (`{ foo: 1 }`) rather than nesting an +accessor inside (`{ foo: () => signal() }`). + +The setter takes a **draft-mutating** function — mutate the draft in place +(canonical). The callback may also return a new value: arrays are replaced +by index (length adjusted), objects are shallow-diffed at the top level +(keys present in the returned value are written, missing keys deleted). Use +the return form for shapes where mutation is awkward — most commonly +removing items via `filter`. The setter does **not** do keyed reconciliation; +for that, use the derived/projection form (or `createProjection`). + +- Plain form: `createStore(initialValue)` — wraps a value in a reactive + proxy. +- Derived form: `createStore(fn, seed, options?)` — a _projection store_ + whose contents are computed by `fn(draft)`. `fn` may be sync, async, or + an `AsyncIterable`; the projection's result reconciles against the + existing store by `options.key` (default `"id"`) for stable identity. + +## Import + +```ts +import { createStore } from "solid-js"; +``` + +## Type signature + +```ts +function createStore( + store: NoFn | Store> +): [get: Store, set: StoreSetter]; +function createStore( + fn: (store: T) => void | T | Promise | AsyncIterable, + store: Partial | Store>, + options?: ProjectionOptions +): [get: Refreshable>, set: StoreSetter]; +``` + +## Return value + +`[store: Store, setStore: StoreSetter]` + +## Examples + +```ts +const [state, setState] = createStore({ + user: { name: "Ada", age: 36 }, + todos: [] as { id: string; text: string; done: boolean }[], +}); + +// Canonical: mutate the draft in place. +setState((s) => { + s.user.age = 37; +}); +setState((s) => { + s.todos.push({ id: "1", text: "x", done: false }); +}); + +// Return form: reach for it when mutation is awkward. +setState((s) => s.todos.filter((t) => !t.done)); // remove items +setState((s) => ({ ...s, user: { name: "Grace", age: 85 } })); // shallow replace +``` + +```ts +// Derived store — auto-fetches & reconciles by `id`. +const [users] = createStore( + async () => fetch("/users").then((r) => r.json()), + [] as User[] +); +``` + +## Related types + +### `ProjectionOptions` + +Options for derived/projected stores created with `createStore(fn)`, `createProjection`, or `createOptimisticStore(fn)`. + +```ts +interface ProjectionOptions extends StoreOptions { + /** Key property name or function for reconciliation identity */ + key?: string | ((item: NonNullable) => any); +} +``` + +### `Refreshable` + +Brand applied to derived/projected stores indicating they participate in +the `refresh()` re-run protocol. Use this alias instead of inlining +`T & { [$REFRESH]: any }` so that user-defined hooks that wrap +`createOptimisticStore` / `createProjection` / projection-form +`createStore` can have their return types inferred without leaking the +internal `$REFRESH` symbol into public type signatures (TS4058). + +```ts +type Refreshable = T & { readonly [$REFRESH]: any }; +``` + +### `StoreOptions` + +Base options for store primitives. + +```ts +interface StoreOptions { + /** Debug name (dev mode only) */ + name?: string; +} +``` + +### `StoreSetter` + +A store setter. The callback receives a writable **draft** of the store. + +- **Mutate in place (canonical):** `s.foo = 1`, `s.list.push(x)`, + `s.list.splice(i, 1)`. This is the default form for most updates. +- **Return a new value:** for shapes where mutation is awkward, most + commonly removing items (`s => s.list.filter(...)`). Arrays are replaced + by index (length adjusted); objects are shallow-diffed at the top level + (keys present in the returned value are written, missing keys deleted). + +The setter does **not** perform keyed reconciliation. If you need surviving +items to keep their store identity across full-array replacement, use the +projection form — `createStore(fn, seed, { key })` or `createProjection` — +whose derive function reconciles its return by `options.key`. + +```ts +type StoreSetter = (fn: (state: T) => T | void) => void; +``` diff --git a/src/routes/v2/reference/(2)stores/merge.mdx b/src/routes/v2/reference/(2)stores/merge.mdx new file mode 100644 index 000000000..21df02b71 --- /dev/null +++ b/src/routes/v2/reference/(2)stores/merge.mdx @@ -0,0 +1,56 @@ +--- +title: "merge" +category: "Stores" +version: "2.0" +description: "Merges multiple props-like objects into a single proxy that *preserves\nreactivity*. Reads are forwarded to the right-most source that defines the\nproperty, so later sources override earlier ones (like `Object.assign`)." +source_repo: "solidjs/solid" +source_ref: "next" +source_sha: "a93a216ef35bd053949493506942884fb24e7684" +source_path: "packages/solid-signals/src/store/utils.ts" +--- + +{/* Generated by scripts/extract-solid-ref.mjs. Edit the source JSDoc or disposition map, then regenerate. */} + +Merges multiple props-like objects into a single proxy that _preserves +reactivity_. Reads are forwarded to the right-most source that defines the +property, so later sources override earlier ones (like `Object.assign`). + +Function arguments are treated as memo-backed sources — useful for passing +derived defaults whose computation should track reactively. + +Use this in component bodies to merge defaults / overrides without losing +Solid's per-property tracking. + +## Import + +```ts +import { merge } from "solid-js"; +``` + +## Type signature + +```ts +function merge(...sources: T): Merge; +``` + +## Examples + +```tsx +function Button(_props: { label: string; type?: string; disabled?: boolean }) { + const props = merge({ type: "button", disabled: false }, _props); + + return ( + + ); +} +``` + +## Related types + +### `Merge` + +```ts +type Merge = Simplify<_Merge>; +``` diff --git a/src/routes/v2/reference/(2)stores/omit.mdx b/src/routes/v2/reference/(2)stores/omit.mdx new file mode 100644 index 000000000..1eeff412d --- /dev/null +++ b/src/routes/v2/reference/(2)stores/omit.mdx @@ -0,0 +1,68 @@ +--- +title: "omit" +category: "Stores" +version: "2.0" +description: "Returns a reactive proxy of `props` with the listed keys hidden. Tracking\non the remaining keys is preserved." +source_repo: "solidjs/solid" +source_ref: "next" +source_sha: "a93a216ef35bd053949493506942884fb24e7684" +source_path: "packages/solid-signals/src/store/utils.ts" +--- + +{/* Generated by scripts/extract-solid-ref.mjs. Edit the source JSDoc or disposition map, then regenerate. */} + +Returns a reactive proxy of `props` with the listed keys hidden. Tracking +on the remaining keys is preserved. + +Use it to forward "rest" props to a child element while pulling out the +keys your component handles itself — the equivalent of `splitProps(p, ["a","b"])[1]`. + +## Import + +```ts +import { omit } from "solid-js"; +``` + +## Type signature + +```ts +function omit, K extends readonly (keyof T)[]>( + props: T, + ...keys: K +): Omit; +``` + +## Examples + +```tsx +function Input( + props: { + label: string; + value: string; + onInput: (v: string) => void; + } & JSX.HTMLAttributes +) { + const rest = omit(props, "label", "value", "onInput"); + + return ( + + ); +} +``` + +## Related types + +### `Omit` + +```ts +type Omit = { + [P in keyof T as Exclude]: T[P]; +}; +``` diff --git a/src/routes/v2/reference/(2)stores/reconcile.mdx b/src/routes/v2/reference/(2)stores/reconcile.mdx new file mode 100644 index 000000000..de9e837cf --- /dev/null +++ b/src/routes/v2/reference/(2)stores/reconcile.mdx @@ -0,0 +1,56 @@ +--- +title: "reconcile" +category: "Stores" +version: "2.0" +description: "Returns a draft-mutating function that smart-merges `value` into a store,\npreserving the identity of items whose `key` field matches between old and\nnew states. Useful when applying server payloads or full-replacement data\nonto an existing store without losing fine-grained reactivity." +source_repo: "solidjs/solid" +source_ref: "next" +source_sha: "a93a216ef35bd053949493506942884fb24e7684" +source_path: "packages/solid-signals/src/store/reconcile.ts" +--- + +{/* Generated by scripts/extract-solid-ref.mjs. Edit the source JSDoc or disposition map, then regenerate. */} + +Returns a draft-mutating function that smart-merges `value` into a store, +preserving the identity of items whose `key` field matches between old and +new states. Useful when applying server payloads or full-replacement data +onto an existing store without losing fine-grained reactivity. + +Items with the same key are updated in place (only changed properties +trigger updates). Items added or removed update the corresponding signals. + +## Import + +```ts +import { reconcile } from "solid-js"; +``` + +## Type signature + +```ts +function reconcile( + value: T, + key: string | ((item: NonNullable) => any) +); +``` + +## Parameters + +### `value` + +the next state to merge in + +### `key` + +property name (string) or extractor function for stable identity + +## Examples + +```ts +const [todos, setTodos] = createStore([]); + +async function refresh() { + const fresh = await api.getTodos(); + setTodos(reconcile(fresh, "id")); // diff-merge by `id` +} +``` diff --git a/src/routes/v2/reference/(3)lifecycle-actions/action.mdx b/src/routes/v2/reference/(3)lifecycle-actions/action.mdx new file mode 100644 index 000000000..a8cbea56d --- /dev/null +++ b/src/routes/v2/reference/(3)lifecycle-actions/action.mdx @@ -0,0 +1,62 @@ +--- +title: "action" +category: "Lifecycle & Actions" +version: "2.0" +description: "Wraps a generator function so each invocation runs as a single transaction\n(a \"transition\") that batches every signal/store write between yields. The\nsurrounding UI sees one atomic update per yielded step; nothing is committed\nuntil the action either completes or the next `yield` resolves." +source_repo: "solidjs/solid" +source_ref: "next" +source_sha: "a93a216ef35bd053949493506942884fb24e7684" +source_path: "packages/solid-signals/src/core/action.ts" +--- + +{/* Generated by scripts/extract-solid-ref.mjs. Edit the source JSDoc or disposition map, then regenerate. */} + +Wraps a generator function so each invocation runs as a single transaction +(a "transition") that batches every signal/store write between yields. The +surrounding UI sees one atomic update per yielded step; nothing is committed +until the action either completes or the next `yield` resolves. + +Yield promises (or any awaitable) inside the generator — the action waits +for each before continuing, but the writes you made beforehand are already +visible (or held by `` if optimistic). Yield bare values for +synchronous batched steps. + +Each call returns a `Promise` that resolves with the generator's return +value, or rejects if it throws. Pair with `createOptimistic` / +`createOptimisticStore` to apply tentative writes that auto-revert if the +action fails. + +## Import + +```ts +import { action } from "solid-js"; +``` + +## Type signature + +```ts +function action( + genFn: (...args: Args) => Generator | AsyncGenerator +); +``` + +## Examples + +```ts +const [todos, setTodos] = createOptimisticStore([]); + +const addTodo = action(function* (text: string) { + const tempId = crypto.randomUUID(); + setTodos((t) => { + t.push({ id: tempId, text, pending: true }); + }); // optimistic + const saved = yield api.createTodo(text); // network round-trip + setTodos((t) => { + const i = t.findIndex((x) => x.id === tempId); + if (i >= 0) t[i] = saved; + }); + return saved; +}); + +await addTodo("buy milk"); +``` diff --git a/src/routes/v2/reference/(3)lifecycle-actions/on-settled.mdx b/src/routes/v2/reference/(3)lifecycle-actions/on-settled.mdx new file mode 100644 index 000000000..cbfa90111 --- /dev/null +++ b/src/routes/v2/reference/(3)lifecycle-actions/on-settled.mdx @@ -0,0 +1,104 @@ +--- +title: "onSettled" +category: "Lifecycle & Actions" +version: "2.0" +description: "Schedules `callback` to run **once** after the reactive graph has fully\nsettled — i.e. once every pending async read inside the current owner has\nresolved and the queue has flushed. Each call registers a single fire; it\ndoes not create an ongoing subscription." +source_repo: "solidjs/solid" +source_ref: "next" +source_sha: "a93a216ef35bd053949493506942884fb24e7684" +source_path: "packages/solid-signals/src/signals.ts" +--- + +{/* Generated by scripts/extract-solid-ref.mjs. Edit the source JSDoc or disposition map, then regenerate. */} + +Schedules `callback` to run **once** after the reactive graph has fully +settled — i.e. once every pending async read inside the current owner has +resolved and the queue has flushed. Each call registers a single fire; it +does not create an ongoing subscription. + +The canonical lifecycle primitive in 2.0. Three main usages: + +- **Component-level setup-and-teardown** _(the most common shape)_: run + setup after the component's first stable render and **return a cleanup + function** to dispose it on owner disposal. This is the replacement for + the 1.x `onMount` + `onCleanup` pairing — setup and teardown live in one + block, and `onCleanup` is no longer the right tool for component + bodies. (`onMount` no longer exists in 2.0.) +- **Post-settle "ready" hook:** run once after a component's first stable + render — analytics ping, focus, scroll-into-view, etc. No cleanup needed. +- **Inside an event handler:** schedule work to run after the action / + transition triggered by the event has completed. + +Reactive reads inside the callback are _not_ tracked — to react to +subsequent settles, register a new `onSettled` each time. + +`onCleanup` is **not** allowed inside the callback — return a cleanup +function instead. The returned cleanup runs on owner disposal. + +## Import + +```ts +import { onSettled } from "solid-js"; +``` + +## Type signature + +```ts +function onSettled(callback: () => void | (() => void)): void; +``` + +## Parameters + +### `callback` + +Function to run; may return a cleanup function that fires +on owner disposal + +## Examples + +```tsx +// Component-level setup + teardown — replaces onMount + onCleanup. +// Subscribe to an external source on mount, unsubscribe on dispose. +function useViewportWidth() { + const [width, setWidth] = createSignal(window.innerWidth); + onSettled(() => { + const onResize = () => setWidth(window.innerWidth); + window.addEventListener("resize", onResize); + return () => window.removeEventListener("resize", onResize); + }); + return width; +} +``` + +```tsx +// Post-settle "ready" hook — no cleanup needed. +function Dashboard() { + const data = createMemo(async () => fetchData()); + + onSettled(() => { + analytics.track("dashboard.ready"); + }); + + return ( + }> +
{data()}
+
+ ); +} +``` + +```tsx +// Event-handler — runs after the action settles. +function SaveButton() { + const save = action(function* () { + yield api.save(); + }); + + const handleClick = () => { + save(); + onSettled(() => toast("Saved!")); + }; + + return ; +} +``` diff --git a/src/routes/v2/reference/(3)lifecycle-actions/refresh.mdx b/src/routes/v2/reference/(3)lifecycle-actions/refresh.mdx new file mode 100644 index 000000000..b34e35a64 --- /dev/null +++ b/src/routes/v2/reference/(3)lifecycle-actions/refresh.mdx @@ -0,0 +1,46 @@ +--- +title: "refresh" +category: "Lifecycle & Actions" +version: "2.0" +description: "Forces a reactive source to re-execute, even if its inputs haven't changed." +source_repo: "solidjs/solid" +source_ref: "next" +source_sha: "a93a216ef35bd053949493506942884fb24e7684" +source_path: "packages/solid-signals/src/core/core.ts" +--- + +{/* Generated by scripts/extract-solid-ref.mjs. Edit the source JSDoc or disposition map, then regenerate. */} + +Forces a reactive source to re-execute, even if its inputs haven't changed. + +Two forms: + +- `refresh(memo)` — pass an accessor (memo / signal getter) and its + underlying computation reruns. +- `refresh(store)` — pass a _projected_ store created from + `createStore(fn, ...)` or `createProjection(...)` and the projection + recomputes. + +Use it to invalidate cached async values (e.g. force a re-fetch) without +tearing the consumer down. + +## Import + +```ts +import { refresh } from "solid-js"; +``` + +## Type signature + +```ts +function refresh(fn: (() => T) | Refreshable): T; +``` + +## Examples + +```ts +const user = createMemo(async () => fetch(`/users/${id()}`).then(r => r.json())); + +// Re-fetch on demand + +``` diff --git a/src/routes/v2/reference/(4)components-context/children.mdx b/src/routes/v2/reference/(4)components-context/children.mdx new file mode 100644 index 000000000..86c10d538 --- /dev/null +++ b/src/routes/v2/reference/(4)components-context/children.mdx @@ -0,0 +1,75 @@ +--- +title: "children" +category: "Components & Context" +version: "2.0" +description: "Resolves a `children` accessor and exposes the result as an accessor with\na `.toArray()` helper. Use this when a component needs to inspect or\niterate over its children rather than just render them through." +source_repo: "solidjs/solid" +source_ref: "next" +source_sha: "a93a216ef35bd053949493506942884fb24e7684" +source_path: "packages/solid/src/client/core.ts" +--- + +{/* Generated by scripts/extract-solid-ref.mjs. Edit the source JSDoc or disposition map, then regenerate. */} + +Resolves a `children` accessor and exposes the result as an accessor with +a `.toArray()` helper. Use this when a component needs to inspect or +iterate over its children rather than just render them through. + +## Import + +```ts +import { children } from "solid-js"; +``` + +## Type signature + +```ts +function children(fn: Accessor): ChildrenReturn; +``` + +## Parameters + +### `fn` + +an accessor for the children + +## Return value + +an accessor of the resolved children, with `.toArray()` for iteration + +## Examples + +```tsx +function List(props: { children: JSX.Element }) { + const items = children(() => props.children); + return ( +
    + {items.toArray().map((item) => ( +
  • {item}
  • + ))} +
+ ); +} +``` + +## Related types + +### `ChildrenReturn` + +```ts +type ChildrenReturn = Accessor & { + toArray: () => ResolvedJSXElement[]; +}; +``` + +### `ResolvedChildren` + +```ts +type ResolvedChildren = ResolvedJSXElement | ResolvedJSXElement[]; +``` + +### `ResolvedJSXElement` + +```ts +type ResolvedJSXElement = Exclude; +``` diff --git a/src/routes/v2/reference/(4)components-context/create-context.mdx b/src/routes/v2/reference/(4)components-context/create-context.mdx new file mode 100644 index 000000000..22a3d6455 --- /dev/null +++ b/src/routes/v2/reference/(4)components-context/create-context.mdx @@ -0,0 +1,106 @@ +--- +title: "createContext" +category: "Components & Context" +version: "2.0" +description: "Creates a Context for sharing state with descendants of a Provider in the\ncomponent tree." +source_repo: "solidjs/solid" +source_ref: "next" +source_sha: "a93a216ef35bd053949493506942884fb24e7684" +source_path: "packages/solid/src/client/core.ts" +--- + +{/* Generated by scripts/extract-solid-ref.mjs. Edit the source JSDoc or disposition map, then regenerate. */} + +Creates a Context for sharing state with descendants of a Provider in the +component tree. + +The returned `Context` is itself a provider component — pass it a `value` +prop to scope a value to its children. Read it inside descendants with +`useContext`. + +Two forms: + +- **`createContext()`** (default-less, the canonical form). Reading via + `useContext` outside an enclosing Provider throws `ContextNotFoundError`. + Use this for everything that carries reactive state — signals, stores, + `[state, actions]` tuples, services. The Provider is mandatory by + construction; the throw makes a missing Provider a loud bug instead of a + silent no-op. The annotation `` is required because there is no value + to infer from. +- **`createContext(defaultValue)`** (default form). Reserved for the + narrow case of contexts whose value is a primitive with a meaningful + static fallback (theme, locale, frozen config). Outside any Provider, + `useContext` returns `defaultValue`. + +If you want truly app-wide state, **don't use Context** — a module-scope +signal/store _is_ a global. Context is for scoping state to a subtree; +that's why a Provider is required. + +## Import + +```ts +import { createContext } from "solid-js"; +``` + +## Type signature + +```ts +function createContext( + defaultValue?: T, + options?: EffectOptions +): Context; +``` + +## Parameters + +### `defaultValue` + +optional default; only meaningful for primitive +fallbacks. Omit for any context carrying reactive state. + +### `options` + +`{ name }` for debugging in development + +## Return value + +a context object that doubles as its own provider component + +## Examples + +```tsx +// Reactive payload — default-less, throws if no Provider. +type TodosCtx = readonly [Store, TodoActions]; +const TodosContext = createContext(); + +function App() { + return ( + + + + ); +} + +function TodoList() { + const [todos, { addTodo }] = useContext(TodosContext); // typed as TodosCtx + // ... +} +``` + +```tsx +// Primitive default — falls back to "light" outside a Provider. +const ThemeContext = createContext<"light" | "dark">("light"); + +function Button() { + const theme = useContext(ThemeContext); // "light" | "dark" + return ; +} +``` + +## Related types + +### `ContextRecord` + +```ts +type ContextRecord = Record; +``` diff --git a/src/routes/v2/reference/(4)components-context/create-unique-id.mdx b/src/routes/v2/reference/(4)components-context/create-unique-id.mdx new file mode 100644 index 000000000..87bbb9450 --- /dev/null +++ b/src/routes/v2/reference/(4)components-context/create-unique-id.mdx @@ -0,0 +1,42 @@ +--- +title: "createUniqueId" +category: "Components & Context" +version: "2.0" +description: "Returns a stable id string that matches between server-rendered and\nclient-hydrated trees. Use it for `