diff --git a/eslint.config.mjs b/eslint.config.mjs index 26fc3bde7b9..6396de82bc3 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -361,6 +361,10 @@ export default [{ }, languageOptions: { + globals: { + globalThis: "readonly", + }, + parser: tseslint.parser, ecmaVersion: 6, sourceType: "module", diff --git a/packages/@react-aria/utils/src/domHelpers.ts b/packages/@react-aria/utils/src/domHelpers.ts index 7c661d5be85..59faecd3a50 100644 --- a/packages/@react-aria/utils/src/domHelpers.ts +++ b/packages/@react-aria/utils/src/domHelpers.ts @@ -3,8 +3,8 @@ export const getOwnerDocument = (el: Element | null | undefined): Document => { }; export const getOwnerWindow = ( - el: (Window & typeof global) | Element | null | undefined -): Window & typeof global => { + el: (Window & typeof globalThis) | Element | null | undefined +): Window & typeof globalThis => { if (el && 'window' in el && el.window === el) { return el; } diff --git a/packages/@react-aria/utils/src/useViewportSize.ts b/packages/@react-aria/utils/src/useViewportSize.ts index 30fc9d26385..ed6f6f765e5 100644 --- a/packages/@react-aria/utils/src/useViewportSize.ts +++ b/packages/@react-aria/utils/src/useViewportSize.ts @@ -26,6 +26,15 @@ export function useViewportSize(): ViewportSize { let [size, setSize] = useState(() => isSSR ? {width: 0, height: 0} : getViewportSize()); useEffect(() => { + let updateSize = (newSize: ViewportSize) => { + setSize(size => { + if (newSize.width === size.width && newSize.height === size.height) { + return size; + } + return newSize; + }); + }; + // Use visualViewport api to track available height even on iOS virtual keyboard opening let onResize = () => { // Ignore updates when zoomed. @@ -33,13 +42,7 @@ export function useViewportSize(): ViewportSize { return; } - setSize(size => { - let newSize = getViewportSize(); - if (newSize.width === size.width && newSize.height === size.height) { - return size; - } - return newSize; - }); + updateSize(getViewportSize()); }; // When closing the keyboard, iOS does not fire the visual viewport resize event until the animation is complete. @@ -54,18 +57,14 @@ export function useViewportSize(): ViewportSize { // Wait one frame to see if a new element gets focused. frame = requestAnimationFrame(() => { if (!document.activeElement || !willOpenKeyboard(document.activeElement)) { - setSize(size => { - let newSize = {width: window.innerWidth, height: window.innerHeight}; - if (newSize.width === size.width && newSize.height === size.height) { - return size; - } - return newSize; - }); + updateSize({width: window.innerWidth, height: window.innerHeight}); } }); } }; + updateSize(getViewportSize()); + window.addEventListener('blur', onBlur, true); if (!visualViewport) { diff --git a/packages/@react-aria/utils/test/useViewportSize.ssr.test.tsx b/packages/@react-aria/utils/test/useViewportSize.ssr.test.tsx index a5dcf31ae6c..e14deb7b3bc 100644 --- a/packages/@react-aria/utils/test/useViewportSize.ssr.test.tsx +++ b/packages/@react-aria/utils/test/useViewportSize.ssr.test.tsx @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {testSSR} from '@react-spectrum/test-utils-internal'; +import {screen, testSSR} from '@react-spectrum/test-utils-internal'; describe('useViewportSize SSR', () => { it('should render without errors', async () => { @@ -25,4 +25,21 @@ describe('useViewportSize SSR', () => { `); }); + + it('should update dimensions after hydration', async () => { + await testSSR(__filename, ` + import {useViewportSize} from '../src'; + + function Viewport() { + let size = useViewportSize(); + return
{size.width}x{size.height}
; + } + + + `, () => { + expect(screen.getByTestId('viewport')).toHaveTextContent('0x0'); + }); + + expect(screen.getByTestId('viewport')).not.toHaveTextContent('0x0'); + }); }); diff --git a/packages/@react-spectrum/s2/src/CoachMark.tsx b/packages/@react-spectrum/s2/src/CoachMark.tsx index 41229c198d1..d84dde75282 100644 --- a/packages/@react-spectrum/s2/src/CoachMark.tsx +++ b/packages/@react-spectrum/s2/src/CoachMark.tsx @@ -320,7 +320,7 @@ const actionButtonSize = { XL: 'L' } as const; -export const CoachMarkContext = createContext>({}); +export const CoachMarkContext = createContext, HTMLElement>>({}); export const CoachMark = forwardRef((props: CoachMarkProps, ref: ForwardedRef) => { let colorScheme = useContext(ColorSchemeContext); diff --git a/packages/dev/s2-docs/scripts/generateMarkdownDocs.mjs b/packages/dev/s2-docs/scripts/generateMarkdownDocs.mjs index f9915faf915..8abd1ec524e 100644 --- a/packages/dev/s2-docs/scripts/generateMarkdownDocs.mjs +++ b/packages/dev/s2-docs/scripts/generateMarkdownDocs.mjs @@ -60,6 +60,67 @@ const project = new Project({ skipAddingFilesFromTsConfig: true }); +const interfacePathCache = new Map(); +const interfaceTableCache = new Map(); +const classTableCache = new Map(); +const propTableCache = new Map(); +const descriptionCache = new Map(); +let tsFileIndex = null; + +function getTsFileIndex() { + if (tsFileIndex) { + return tsFileIndex; + } + + console.log('Building TypeScript file index...'); + const startTime = Date.now(); + + // Index files from component roots and packages directory + const patterns = [ + ...COMPONENT_SRC_ROOTS.map(r => path.posix.join(r, '**/*.{ts,tsx,d.ts}')), + path.posix.join(REPO_ROOT, 'packages/**/*.{ts,tsx,d.ts}') + ]; + + const files = glob.sync(patterns, { + absolute: true, + suppressErrors: true, + deep: 5, + ignore: ['**/node_modules/**', '**/*.test.*', '**/*.stories.*', '**/dist/**'] + }); + + // Build index: for each file, extract exported names for quick lookup + tsFileIndex = new Map(); + for (const filePath of files) { + try { + const content = fs.readFileSync(filePath, 'utf8'); + + // Extract interface/type/class/function/const names + const interfaceMatches = content.matchAll(/(?:export\s+)?interface\s+(\w+)/g); + const typeMatches = content.matchAll(/(?:export\s+)?type\s+(\w+)\s*[=<]/g); + const classMatches = content.matchAll(/(?:export\s+)?class\s+(\w+)/g); + const functionMatches = content.matchAll(/(?:export\s+)?function\s+(\w+)/g); + const constMatches = content.matchAll(/(?:export\s+)?const\s+(\w+)\s*[=:]/g); + + for (const match of [...interfaceMatches, ...typeMatches, ...classMatches, ...functionMatches, ...constMatches]) { + const name = match[1]; + if (!tsFileIndex.has(name)) { + tsFileIndex.set(name, []); + } + // Avoid duplicates for the same file + const existing = tsFileIndex.get(name); + if (!existing.includes(filePath)) { + existing.push(filePath); + } + } + } catch { + // Ignore files that can't be read + } + } + + console.log(`Built index with ${tsFileIndex.size} symbols in ${Date.now() - startTime}ms`); + return tsFileIndex; +} + /** * Clean type text by removing import statements and duplicate type parameters. */ @@ -71,6 +132,29 @@ function cleanTypeText(t) { return cleaned; } +/** + * Get type text from a declaration, preferring the type node (AST) over the resolved type. + */ +function getTypeText(decl, fallbackContext) { + // Try to get the type node first (preserves declaration order) + const typeNode = decl?.getTypeNode?.(); + if (typeNode) { + return cleanTypeText(typeNode.getText()); + } + + // Fall back to resolved type with context + const type = decl?.getType?.(); + if (type && fallbackContext) { + return cleanTypeText(type.getText(fallbackContext)); + } + + if (type) { + return cleanTypeText(type.getText()); + } + + return 'unknown'; +} + /** * Transform relative URLs to use .md extension instead of .html or no extension. * Preserves query params and hash fragments. @@ -335,50 +419,109 @@ function extractJSXText(node, file) { return ''; } -function resolveComponentPath(componentName, file) { - let roots = COMPONENT_SRC_ROOTS; +function getRootsForFile(file) { + if (file?.path) { + if (file.path.includes(path.join('pages', 'react-aria', 'internationalized'))) { + return [INTL_SRC_ROOT, S2_SRC_ROOT, RAC_SRC_ROOT]; + } else if (file.path.includes(path.join('pages', 'react-aria'))) { + return [RAC_SRC_ROOT, S2_SRC_ROOT, INTL_SRC_ROOT]; + } + } + return COMPONENT_SRC_ROOTS; +} + +function getCacheKey(name, file) { if (file?.path) { if (file.path.includes(path.join('pages', 'react-aria', 'internationalized'))) { - roots = [INTL_SRC_ROOT, S2_SRC_ROOT, RAC_SRC_ROOT]; + return `intl:${name}`; } else if (file.path.includes(path.join('pages', 'react-aria'))) { - roots = [RAC_SRC_ROOT, S2_SRC_ROOT, INTL_SRC_ROOT]; + return `rac:${name}`; + } else if (file.path.includes(path.join('pages', 's2'))) { + return `s2:${name}`; } } + return `default:${name}`; +} + +function resolveComponentPath(componentName, file) { + // Check unified cache first + const cacheKey = getCacheKey(componentName, file); + if (interfacePathCache.has(cacheKey)) { + return interfacePathCache.get(cacheKey); + } + + let roots = getRootsForFile(file); + // Fast path: check direct file paths first for (let root of roots) { for (let ext of ['tsx', 'ts']) { const candidate = path.join(root, `${componentName}.${ext}`); - if (fs.existsSync(candidate)) {return candidate;} + if (fs.existsSync(candidate)) { + interfacePathCache.set(cacheKey, candidate); + return candidate; + } } } - if (!global.__componentPathCache) { - global.__componentPathCache = new Map(); - } - if (global.__componentPathCache.has(componentName)) { - return global.__componentPathCache.get(componentName); + // Use pre-built index for fast lookup + const index = getTsFileIndex(); + const candidates = index.get(componentName); + + if (candidates && candidates.length > 0) { + // Prefer files in the priority roots + for (const root of roots) { + const match = candidates.find(p => p.startsWith(root)); + if (match) { + interfacePathCache.set(cacheKey, match); + return match; + } + } + + // Prefer .d.ts files over implementation files + const dtsMatch = candidates.find(p => p.endsWith('.d.ts')); + if (dtsMatch) { + interfacePathCache.set(cacheKey, dtsMatch); + return dtsMatch; + } + + // Prefer @react-types package for type lookups + const typesMatch = candidates.find(p => p.includes('@react-types')); + if (typesMatch) { + interfacePathCache.set(cacheKey, typesMatch); + return typesMatch; + } + + // Fall back to first match + interfacePathCache.set(cacheKey, candidates[0]); + return candidates[0]; } - const matches = glob.sync(roots.map(r => path.posix.join(r, `**/${componentName}.{ts,tsx}`)), { - absolute: true, - suppressErrors: true, - deep: 5 - }); - const resolved = matches[0] || null; - global.__componentPathCache.set(componentName, resolved); - return resolved; + interfacePathCache.set(cacheKey, null); + return null; } /** * Extract the leading JSDoc description comment placed immediately above the export for a component. */ function getComponentDescription(componentName, file) { + // Check cache first + const cacheKey = getCacheKey(componentName, file); + if (descriptionCache.has(cacheKey)) { + return descriptionCache.get(cacheKey); + } + const componentPath = resolveComponentPath(componentName, file); - if (!componentPath) {return null;} + if (!componentPath) { + descriptionCache.set(cacheKey, null); + return null; + } // Lazily add the source file to the ts-morph project. const source = project.addSourceFileAtPathIfExists(componentPath); - if (!source) {return null;} + if (!source) { + descriptionCache.set(cacheKey, null); + return null; + } // Try to find an exported declaration named exactly like the component. const exportedDecl = source.getExportedDeclarations().get(componentName)?.[0]; @@ -405,12 +548,14 @@ function getComponentDescription(componentName, file) { // If this is the direct node (not a parent), return its description immediately if (isDirectNode) { + descriptionCache.set(cacheKey, desc); return desc; } // Otherwise, check if the description mentions the component name const regex = new RegExp(`\\b${componentName}\\b`, 'i'); if (regex.test(desc)) { + descriptionCache.set(cacheKey, desc); return desc; } @@ -421,6 +566,7 @@ function getComponentDescription(componentName, file) { } if (typeof firstNodeDesc === 'string') { + descriptionCache.set(cacheKey, firstNodeDesc); return firstNodeDesc; } @@ -428,14 +574,12 @@ function getComponentDescription(componentName, file) { for (let doc of allJsDocs.reverse()) { const desc = doc.getDescription().trim(); if (desc && desc.toLowerCase().includes(componentName.toLowerCase())) { + descriptionCache.set(cacheKey, desc); return desc; } } - if (allJsDocs.length) { - return allJsDocs[0].getDescription().trim(); - } - + descriptionCache.set(cacheKey, null); return null; } @@ -443,53 +587,44 @@ function getComponentDescription(componentName, file) { * Build a markdown table of props for the given component by analyzing its interface. */ function generatePropTable(componentName, file) { + // Check cache first + const cacheKey = getCacheKey(componentName, file); + if (propTableCache.has(cacheKey)) { + return propTableCache.get(cacheKey); + } + const interfaceName = `${componentName}Props`; + + // Try to resolve via component path first, then interface path let componentPath = resolveComponentPath(componentName, file); - - // Fallback: deep search for the interface declaration if resolveComponentPath failed. if (!componentPath) { - let roots = COMPONENT_SRC_ROOTS; - if (file?.path) { - if (file.path.includes(path.join('pages', 'react-aria', 'internationalized'))) { - roots = [INTL_SRC_ROOT, S2_SRC_ROOT, RAC_SRC_ROOT]; - } else if (file.path.includes(path.join('pages', 'react-aria'))) { - roots = [RAC_SRC_ROOT, S2_SRC_ROOT, INTL_SRC_ROOT]; - } - } - const patterns = roots.map(r => path.posix.join(r, '**/*.{ts,tsx,d.ts}')); - // Also scan other packages if not found in component roots. - patterns.push(path.posix.join(REPO_ROOT, 'packages/**/*.{ts,tsx,d.ts}')); - - const matches = glob.sync(patterns, { - absolute: true, - suppressErrors: true, - deep: 5 - }).filter(p => { - try { - const txt = fs.readFileSync(p, 'utf8'); - return new RegExp(`(interface|type)\\s+${interfaceName}\\b`).test(txt) || new RegExp(`export\\s+(function|const|class)\\s+${componentName}\\b`).test(txt); - } catch { - return false; - } - }); - componentPath = matches[0] || null; + componentPath = resolveComponentPath(interfaceName, file); } - if (!componentPath) {return null;} + if (!componentPath) { + propTableCache.set(cacheKey, null); + return null; + } const source = project.addSourceFileAtPathIfExists(componentPath); - if (!source) {return null;} + if (!source) { + propTableCache.set(cacheKey, null); + return null; + } const iface = source.getInterface(interfaceName); - if (!iface) {return null;} + if (!iface) { + propTableCache.set(cacheKey, null); + return null; + } const propSymbols = iface.getType().getProperties(); const rows = propSymbols.map((sym) => { const name = sym.getName(); + const decl = sym.getDeclarations()?.[0]; const type = cleanTypeText(sym.getTypeAtLocation(iface).getText(iface)); - const decl = sym.getDeclarations()?.[0]; let description = ''; let defVal = ''; if (decl && typeof decl.getJsDocs === 'function') { @@ -506,7 +641,10 @@ function generatePropTable(componentName, file) { return {name, type, defVal, description}; }); - if (!rows.length) {return null;} + if (!rows.length) { + propTableCache.set(cacheKey, null); + return null; + } const header = '| Name | Type | Default | Description |\n|------|------|---------|-------------|'; const body = rows @@ -517,52 +655,43 @@ function generatePropTable(componentName, file) { }) .join('\n'); - return `${header}\n${body}`; + const result = `${header}\n${body}`; + propTableCache.set(cacheKey, result); + return result; } function generateInterfaceTable(interfaceName, file) { - // Attempt to resolve the file containing the interface. + // Check cache first + const cacheKey = getCacheKey(interfaceName, file); + if (interfaceTableCache.has(cacheKey)) { + return interfaceTableCache.get(cacheKey); + } + + // Use the unified path resolver which uses the pre-built index let ifacePath = resolveComponentPath(interfaceName, file); - // Fallback: deep search for interface declaration if resolveComponentPath failed. if (!ifacePath) { - let roots = COMPONENT_SRC_ROOTS; - if (file?.path) { - if (file.path.includes(path.join('pages', 'react-aria', 'internationalized'))) { - roots = [INTL_SRC_ROOT, S2_SRC_ROOT, RAC_SRC_ROOT]; - } else if (file.path.includes(path.join('pages', 'react-aria'))) { - roots = [RAC_SRC_ROOT, S2_SRC_ROOT, INTL_SRC_ROOT]; - } - } - const patterns = roots.map(r => path.posix.join(r, '**/*.{ts,tsx,d.ts}')); - // Also scan other packages if not found in component roots. - patterns.push(path.posix.join(REPO_ROOT, 'packages/**/*.{ts,tsx,d.ts}')); - - const matches = glob.sync(patterns, { - absolute: true, - suppressErrors: true, - deep: 5 - }).filter(p => { - try { - const txt = fs.readFileSync(p, 'utf8'); - return new RegExp(`(interface|type)\\s+${interfaceName}\\b`).test(txt); - } catch { - return false; - } - }); - ifacePath = matches[0] || null; + interfaceTableCache.set(cacheKey, null); + return null; } - if (!ifacePath) {return null;} - const source = project.addSourceFileAtPathIfExists(ifacePath); - if (!source) {return null;} + if (!source) { + interfaceTableCache.set(cacheKey, null); + return null; + } const ifaceDecl = source.getInterface(interfaceName); - if (!ifaceDecl) {return null;} + if (!ifaceDecl) { + interfaceTableCache.set(cacheKey, null); + return null; + } const propSymbols = ifaceDecl.getType().getProperties(); - if (!propSymbols.length) {return null;} + if (!propSymbols.length) { + interfaceTableCache.set(cacheKey, null); + return null; + } // Separate properties and methods const properties = []; @@ -608,12 +737,12 @@ function generateInterfaceTable(interfaceName, file) { if (callSignatures.length > 0) { const sig = callSignatures[0]; const params = sig.getParameters(); - const returnType = cleanTypeText(sig.getReturnType().getText()); + const returnType = cleanTypeText(sig.getReturnType().getText(ifaceDecl)); const paramStrs = params.map(p => { const pDecl = p.getDeclarations()?.[0]; const pName = p.getName(); - const pType = cleanTypeText(p.getDeclaredType().getText()); + const pType = getTypeText(pDecl, ifaceDecl); const pOptional = pDecl?.hasQuestionToken?.() ? '?' : ''; return `${pName}${pOptional}: ${pType}`; }); @@ -685,7 +814,9 @@ function generateInterfaceTable(interfaceName, file) { }); } - return sections.join('\n'); + const result = sections.join('\n'); + interfaceTableCache.set(cacheKey, result); + return result; } /** @@ -1808,6 +1939,13 @@ function remarkDocsComponentsToMarkdown() { // ### {typeName} newNodes.push({type: 'heading', depth: 3, children: [{type: 'text', value: typeName}]}); + // Try to generate function signature + const funcSig = generateFunctionSignature(typeName, file); + if (funcSig) { + const sigTree = unified().use(remarkParse).parse(funcSig); + newNodes.push(...sigTree.children); + } + const desc = getComponentDescription(typeName, file); if (desc) { newNodes.push({type: 'paragraph', children: [{type: 'text', value: desc}]}); @@ -1841,47 +1979,29 @@ function remarkDocsComponentsToMarkdown() { * Generate markdown documentation for a class, including its methods and properties. */ function generateClassAPITable(className, file) { - let classPath = resolveComponentPath(className, file); - - if (!classPath) { - // Fallback: deep search for class declaration - let roots = COMPONENT_SRC_ROOTS; - if (file?.path) { - if (file.path.includes(path.join('pages', 'react-aria', 'internationalized'))) { - roots = [INTL_SRC_ROOT, S2_SRC_ROOT, RAC_SRC_ROOT]; - } else if (file.path.includes(path.join('pages', 'react-aria'))) { - roots = [RAC_SRC_ROOT, S2_SRC_ROOT, INTL_SRC_ROOT]; - } - } - const patterns = roots.map(r => path.posix.join(r, '**/*.{ts,tsx,d.ts}')); - patterns.push(path.posix.join(REPO_ROOT, 'packages/**/*.{ts,tsx,d.ts}')); - - const matches = glob.sync(patterns, { - absolute: true, - suppressErrors: true, - deep: 5 - }).filter(p => { - try { - const txt = fs.readFileSync(p, 'utf8'); - return new RegExp(`class\\s+${className}\\b`).test(txt); - } catch { - return false; - } - }); - classPath = matches[0] || null; + // Check cache first + const cacheKey = getCacheKey(className, file); + if (classTableCache.has(cacheKey)) { + return classTableCache.get(cacheKey); } + // Use unified path resolver which uses the pre-built index + let classPath = resolveComponentPath(className, file); + if (!classPath) { + classTableCache.set(cacheKey, null); return null; } const source = project.addSourceFileAtPathIfExists(classPath); if (!source) { + classTableCache.set(cacheKey, null); return null; } const classDecl = source.getClass(className); if (!classDecl) { + classTableCache.set(cacheKey, null); return null; } @@ -1897,7 +2017,7 @@ function generateClassAPITable(className, file) { sections.push('### Constructor\n'); const rows = params.map(param => { const name = param.getName(); - const type = cleanTypeText(param.getType().getText(param)); + const type = getTypeText(param, param); let description = ''; const ctorDocs = ctor.getJsDocs(); @@ -1937,7 +2057,7 @@ function generateClassAPITable(className, file) { // Build method signature const paramStrs = params.map(p => { const pName = p.getName(); - const pType = cleanTypeText(p.getType().getText(p)); + const pType = getTypeText(p, p); const optional = p.hasQuestionToken() ? '?' : ''; return `${pName}${optional}: ${pType}`; }); @@ -1992,7 +2112,7 @@ function generateClassAPITable(className, file) { properties.forEach(prop => { const propName = prop.getName(); - const propType = cleanTypeText(prop.getType().getText(prop)); + const propType = getTypeText(prop, prop); let description = ''; const propDocs = prop.getJsDocs(); @@ -2004,7 +2124,9 @@ function generateClassAPITable(className, file) { }); } - return sections.length > 0 ? sections.join('\n') : null; + const result = sections.length > 0 ? sections.join('\n') : null; + classTableCache.set(cacheKey, result); + return result; } /** @@ -2013,24 +2135,11 @@ function generateClassAPITable(className, file) { function generateStateTable(renderPropsName, {showOptional = false, hideSelector = false} = {}, file) { // Attempt to resolve source file by stripping trailing "RenderProps" to get component name. let componentName = renderPropsName.replace(/RenderProps$/, ''); + + // Try component path first, then render props interface path let componentPath = resolveComponentPath(componentName, file); - - // If not found, fall back to searching all component roots. if (!componentPath) { - let roots = COMPONENT_SRC_ROOTS; - if (file?.path) { - if (file.path.includes(path.join('pages', 'react-aria', 'internationalized'))) { - roots = [INTL_SRC_ROOT, S2_SRC_ROOT, RAC_SRC_ROOT]; - } else if (file.path.includes(path.join('pages', 'react-aria'))) { - roots = [RAC_SRC_ROOT, S2_SRC_ROOT, INTL_SRC_ROOT]; - } - } - const matches = glob.sync(roots.map(r => path.posix.join(r, '**/*.{ts,tsx}')), { - absolute: true, - suppressErrors: true, - deep: 5 - }).filter(p => fs.readFileSync(p, 'utf8').includes(`interface ${renderPropsName}`)); - componentPath = matches[0] || null; + componentPath = resolveComponentPath(renderPropsName, file); } if (!componentPath) { @@ -2113,37 +2222,9 @@ function generateStateTable(renderPropsName, {showOptional = false, hideSelector * Looks at the first parameter type of the exported function with the given name. */ function generateFunctionOptionsTable(functionName, file) { - // Resolve the source file containing the function declaration. + // Use unified path resolver which uses the pre-built index let funcPath = resolveComponentPath(functionName, file); - if (!funcPath) { - // Fallback deep search similar to other helpers. - let roots = COMPONENT_SRC_ROOTS; - if (file?.path) { - if (file.path.includes(path.join('pages', 'react-aria', 'internationalized'))) { - roots = [INTL_SRC_ROOT, S2_SRC_ROOT, RAC_SRC_ROOT]; - } else if (file.path.includes(path.join('pages', 'react-aria'))) { - roots = [RAC_SRC_ROOT, S2_SRC_ROOT, INTL_SRC_ROOT]; - } - } - const patterns = roots.map(r => path.posix.join(r, '**/*.{ts,tsx,d.ts}')); - patterns.push(path.posix.join(REPO_ROOT, 'packages/**/*.{ts,tsx,d.ts}')); - - const matches = glob.sync(patterns, { - absolute: true, - suppressErrors: true, - deep: 5 - }).filter(p => { - try { - const txt = fs.readFileSync(p, 'utf8'); - return new RegExp(`(function|const)\\s+${functionName}\\b`).test(txt); - } catch { - return false; - } - }); - funcPath = matches[0] || null; - } - if (!funcPath) { return null; } @@ -2201,6 +2282,54 @@ function generateFunctionOptionsTable(functionName, file) { return null; } +/** + * Generate a function signature markdown. + * Returns the signature like: `functionName(param1: Type1, param2: Type2): ReturnType` + */ +function generateFunctionSignature(functionName, file) { + let funcPath = resolveComponentPath(functionName, file); + + if (!funcPath) { + return null; + } + + const source = project.addSourceFileAtPathIfExists(funcPath); + if (!source) { + return null; + } + + // Attempt to get an exported declaration for the function. + const exportedDecl = source.getExportedDeclarations().get(functionName)?.[0]; + const possibleDecls = [exportedDecl, source.getFunction(functionName), source.getVariableDeclaration(functionName)]; + + let funcDecl = possibleDecls.find(Boolean); + if (!funcDecl) { + return null; + } + + // Retrieve call signature via type to support arrow functions. + const type = funcDecl.getType?.(); + const callSig = type?.getCallSignatures?.()[0]; + if (!callSig) { + return null; + } + + const params = callSig.getParameters(); + const returnType = cleanTypeText(callSig.getReturnType().getText(funcDecl)); + + // Build parameter list + const paramStrs = params.map(paramSym => { + const paramDecl = paramSym.getDeclarations()?.[0]; + const pName = paramSym.getName(); + const pType = getTypeText(paramDecl, funcDecl); + const pOptional = paramDecl?.hasQuestionToken?.() ? '?' : ''; + return `${pName}${pOptional}: ${pType}`; + }); + + const signature = `${functionName}(${paramStrs.join(', ')}): ${returnType}`; + return `\`${signature}\``; +} + /** * Generate llms.txt file for a specific library. */ diff --git a/packages/react-aria-components/src/utils.tsx b/packages/react-aria-components/src/utils.tsx index 30c686ec3ec..903a80ad9de 100644 --- a/packages/react-aria-components/src/utils.tsx +++ b/packages/react-aria-components/src/utils.tsx @@ -20,7 +20,7 @@ interface SlottedValue { slots?: Record } -export type SlottedContextValue = SlottedValue | T | null | undefined; +export type SlottedContextValue = (SlottedValue & T) | null | undefined; export type ContextValue = SlottedContextValue>; type ProviderValue = [Context, T];