@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
937 lines (856 loc) • 35.9 kB
JavaScript
import { parseImportsAndComments } from "../loaderUtils/index.mjs";
import { parseFunctionArguments, isTypeAssertion, isFunction, isGeneric, isArray, isArrowFunction, isObjectLiteral } from "./parseFunctionArguments.mjs";
/**
* Parse options for create* factory call parsing
*/
/**
* Helper function to extract string value from parser output, removing quotes if present
*/
function extractStringValue(value) {
if (typeof value !== 'string') {
return String(value);
}
// Remove surrounding quotes if present
const trimmed = value.trim();
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
return trimmed.slice(1, -1);
}
// Handle template literals
if (trimmed.startsWith('`') && trimmed.endsWith('`')) {
return trimmed.slice(1, -1);
}
return trimmed;
}
/**
* Helper function to recursively clean up structured data from parser for user consumption,
* removing quotes from strings and converting basic types
*/
function cleanStructuredData(data) {
// Check all structured data types first using the parser helpers
// Check for function calls
const functionCall = isFunction(data);
if (functionCall) {
// Build a function call string like "console.log('test')"
const argStr = functionCall.arguments.map(arg => {
if (Array.isArray(arg)) {
return arg.map(a => typeof a === 'string' ? a : String(a)).join(', ');
}
return typeof arg === 'string' ? arg : String(arg);
}).join(', ');
return `${functionCall.name}(${argStr})`;
}
// Check for generic structures
const generic = isGeneric(data);
if (generic) {
// Build a generic string like "Component<{ foo: string }>"
const genericsStr = generic.generics.map(g => typeof g === 'string' ? g : JSON.stringify(g)).join(', ');
if (generic.arguments && generic.arguments.length > 0) {
// Function with generics: Component<T>(args)
const argsStr = generic.arguments.map(p => typeof p === 'string' ? p : String(p)).join(', ');
return `${generic.name}<${genericsStr}>(${argsStr})`;
}
// Type with generics: Component<T>
return `${generic.name}<${genericsStr}>`;
}
// Check for type assertions
const typeAssertion = isTypeAssertion(data);
if (typeAssertion) {
const cleanedExpression = cleanStructuredData(typeAssertion.expression);
return `${cleanedExpression} as ${typeAssertion.type}`;
}
// Check for arrow functions
const arrowFunction = isArrowFunction(data);
if (arrowFunction) {
const argsStr = arrowFunction.args.map(p => typeof p === 'string' ? p : String(p)).join(', ');
if (arrowFunction.types) {
// Typed arrow function
const [inputType, outputType] = arrowFunction.types;
const returnValue = cleanStructuredData(arrowFunction.returnValue);
return `(${argsStr}: ${inputType}): ${outputType} => ${returnValue}`;
}
// Simple arrow function
const returnValue = cleanStructuredData(arrowFunction.returnValue);
return `(${argsStr}) => ${returnValue}`;
}
// Check for literal arrays
const arrayLiteral = isArray(data);
if (arrayLiteral) {
return arrayLiteral.items[0].map(cleanStructuredData);
}
// Check for object literals
const objectLiteral = isObjectLiteral(data);
if (objectLiteral) {
const cleaned = {};
for (const [key, value] of Object.entries(objectLiteral.properties)) {
cleaned[key] = cleanStructuredData(value);
}
return cleaned;
}
// Handle basic types after structured data checks
if (typeof data === 'string') {
// First extract string value (handle quotes)
const extracted = extractStringValue(data);
// Then try type conversion
if (extracted === 'true') {
return true;
}
if (extracted === 'false') {
return false;
}
// Check if it's a number (but be conservative about version strings like "1.0")
if (/^\d+(\.\d+)?$/.test(extracted)) {
const num = Number(extracted);
if (!Number.isNaN(num) && Number.isFinite(num)) {
// Don't convert simple version-like patterns (e.g., "1.0", "2.0", but convert "123.45")
if (extracted.includes('.')) {
// For decimals, only convert if it's not a simple version pattern
// Version patterns are typically single digit followed by .0 or simple patterns
if (!/^\d{1,2}\.0$/.test(extracted)) {
return num;
}
} else {
// Convert all integers
return num;
}
}
}
return extracted;
}
if (Array.isArray(data)) {
// Fallback for arrays that don't match structured patterns
return data.map(cleanStructuredData);
}
if (data && typeof data === 'object') {
// Fallback for objects that don't match structured patterns
const cleaned = {};
for (const [key, value] of Object.entries(data)) {
cleaned[key] = cleanStructuredData(value);
}
return cleaned;
}
return data;
}
/**
* Helper function to con );
}
// Throw error if the identifier is not found in imports
throw new Error(
`Invalid variants arguments in ${functionName} call in ${filePath}. ` +
`Component '${typeof structuredVariants === 'string' ? structuredVariants : JSON.stringify(structuredVariants)}' is not imported. Make sure to import it first.`,
);
}
/**
* Parse variants from object representation (new format)
*/
function parseVariantsObjectFromObject(obj, importMap, namedExportsMap, functionName, filePath) {
const demoImports = {};
const namedExports = {};
for (const [key, value] of Object.entries(obj)) {
// Handle both string values and structured values (arrays for TypeScript generics)
let importName;
// Use type discriminators to determine the structure type
const typeAssertion = isTypeAssertion(value);
if (typeAssertion) {
// This is a structured type assertion: ['as', 'React.FC<Props>', 'Component']
// Extract the actual component name from the expression
const {
expression
} = typeAssertion;
importName = typeof expression === 'string' ? expression : String(expression);
} else if (typeof value === 'string') {
// Simple string value - strip TypeScript type assertions and typeof expressions
let processedValue = value.trim();
const asIndex = processedValue.indexOf(' as ');
if (asIndex !== -1) {
processedValue = processedValue.substring(0, asIndex).trim();
}
// Handle typeof expressions
if (processedValue.startsWith('typeof ')) {
processedValue = processedValue.substring(7).trim();
}
importName = processedValue;
} else {
// Handle other structured types (functions, generics, arrays)
const functionCall = isFunction(value);
const generic = isGeneric(value);
const arrayLiteral = isArray(value);
if (functionCall) {
// Function call: ['Component', [...]]
importName = functionCall.name;
} else if (generic) {
// Generic: ['Component', [...], [...]]
importName = generic.name;
} else if (arrayLiteral) {
// Array literal: handle first element
const firstItem = arrayLiteral.items[0];
importName = typeof firstItem === 'string' ? firstItem : String(firstItem);
} else if (Array.isArray(value) && value.length > 0) {
// Fallback for unrecognized array structures
const componentExpression = String(value[0]);
const asIndex = componentExpression.indexOf(' as ');
importName = asIndex !== -1 ? componentExpression.substring(0, asIndex).trim() : componentExpression.trim();
} else {
// Final fallback - convert to string and extract
const valueStr = String(value);
const asIndex = valueStr.indexOf(' as ');
importName = asIndex !== -1 ? valueStr.substring(0, asIndex).trim() : valueStr.trim();
}
}
if (importMap.has(importName)) {
demoImports[key] = importMap.get(importName);
namedExports[key] = namedExportsMap.get(importName);
} else {
throw new Error(`Invalid variants argument in ${functionName} call in ${filePath}. ` + `Component '${importName}' is not imported. Make sure to import it first.`);
}
}
return {
variants: demoImports,
namedExports
};
}
/**
* Helper function to convert the new parseImportsAndComments format to a Map
* that maps import names to their resolved URLs
*/
function buildImportMap(importResult, allowExternalVariants) {
const importMap = new Map();
Object.values(importResult.relative).forEach(({
url,
names
}) => {
names.forEach(({
name,
alias
}) => {
// Use alias if available, otherwise use the original name
const nameToUse = alias || name;
importMap.set(nameToUse, url);
});
});
// Include external imports if allowExternalVariants is enabled
if (allowExternalVariants) {
Object.entries(importResult.externals).forEach(([modulePath, externalImport]) => {
if (externalImport && externalImport.names) {
externalImport.names.forEach(({
name,
alias
}) => {
// Use alias if available, otherwise use the original name
const nameToUse = alias || name;
importMap.set(nameToUse, modulePath);
});
}
});
}
return importMap;
}
/**
* Helper function to build a mapping from import aliases to their original named exports
*/
function buildNamedExportsMap(importResult, allowExternalVariants) {
const namedExportsMap = new Map();
Object.values(importResult.relative).forEach(({
names
}) => {
names.forEach(({
name,
alias,
type
}) => {
// Use alias if available, otherwise use the original name as key
const nameToUse = alias || name;
// Only map to the original export name for named imports
// Default imports should map to undefined since they don't have a specific named export
if (type === 'named') {
namedExportsMap.set(nameToUse, name);
} else {
namedExportsMap.set(nameToUse, undefined); // undefined for default/namespace imports
}
});
});
// Include external imports if allowExternalVariants is enabled
if (allowExternalVariants) {
Object.entries(importResult.externals).forEach(([, externalImport]) => {
if (externalImport && externalImport.names) {
externalImport.names.forEach(({
name,
alias,
type
}) => {
// Use alias if available, otherwise use the original name as key
const nameToUse = alias || name;
// Only map to the original export name for named imports
// Default imports should map to undefined since they don't have a specific named export
if (type === 'named') {
namedExportsMap.set(nameToUse, name);
} else {
namedExportsMap.set(nameToUse, undefined); // undefined for default/namespace imports
}
});
}
});
}
return namedExportsMap;
}
/**
* Parses a variants object using pre-parsed structured data
*/
function parseVariantsObjectFromStructured(structuredData, importMap, namedExportsMap, functionName, filePath) {
const demoImports = {};
const namedExports = {};
for (const item of structuredData) {
// If it's a string, process it directly
if (typeof item === 'string') {
const trimmedPart = item.trim();
// Check if this part contains a colon (key: value syntax)
const colonIndex = trimmedPart.indexOf(':');
if (colonIndex !== -1) {
// Handle "key: value" syntax
const key = trimmedPart.substring(0, colonIndex).trim();
const valueExpression = trimmedPart.substring(colonIndex + 1).trim();
// Strip TypeScript type assertions (e.g., "Component as React.ComponentType<...>" -> "Component")
const asIndex = valueExpression.indexOf(' as ');
const importName = asIndex !== -1 ? valueExpression.substring(0, asIndex).trim() : valueExpression;
if (importMap.has(importName)) {
demoImports[key] = importMap.get(importName);
namedExports[key] = namedExportsMap.get(importName);
} else {
throw new Error(`Invalid variants argument in ${functionName} call in ${filePath}. ` + `Component '${importName}' is not imported. Make sure to import it first.`);
}
} else {
// Handle shorthand syntax (just the component name)
const importName = trimmedPart;
if (importMap.has(importName)) {
demoImports[importName] = importMap.get(importName);
namedExports[importName] = namedExportsMap.get(importName);
} else {
throw new Error(`Invalid variants argument in ${functionName} call in ${filePath}. ` + `Component '${importName}' is not imported. Make sure to import it first.`);
}
}
}
// If it's an array (nested structure), we don't expect this in variants parsing
// but we could handle it if needed in the future
}
return {
variants: demoImports,
namedExports
};
}
/**
* Parses variants argument using pre-parsed structured data
*/
function parseVariantsArgumentFromStructured(structuredVariants, importMap, namedExportsMap, functionName, filePath) {
// If it's an object (Record<string, string>)
if (typeof structuredVariants === 'object' && !Array.isArray(structuredVariants)) {
// We have an object with key-value pairs
return parseVariantsObjectFromObject(structuredVariants, importMap, namedExportsMap, functionName, filePath);
}
// If it's an array (object literal parsed into structured data)
if (Array.isArray(structuredVariants)) {
// Parse the object contents using structured data
return parseVariantsObjectFromStructured(structuredVariants, importMap, namedExportsMap, functionName, filePath);
}
// If it's a single identifier string
if (typeof structuredVariants === 'string') {
let componentName = structuredVariants.trim();
// Handle TypeScript type assertions in single component syntax
const asIndex = componentName.indexOf(' as ');
if (asIndex !== -1) {
componentName = componentName.substring(0, asIndex).trim();
}
// Handle typeof expressions in single component syntax
if (componentName.startsWith('typeof ')) {
componentName = componentName.substring(7).trim();
}
if (importMap.has(componentName)) {
return {
variants: {
Default: importMap.get(componentName)
},
namedExports: {
Default: namedExportsMap.get(componentName)
}
};
}
// Throw error if the identifier is not found in imports
throw new Error(`Invalid variants argument in ${functionName} call in ${filePath}. ` + `Component '${componentName}' is not imported. Make sure to import it first.`);
}
// If we reach here, the structured data format is unexpected
throw new Error(`Unexpected structured variants format in ${functionName} call in ${filePath}. ` + `Expected string, array, or object but got: ${typeof structuredVariants}`);
}
/**
* Parse TypeScript generic definitions to extract variants mapping
* e.g., "{ VariantA: ComponentA, VariantB: ComponentB }" -> { VariantA: "ComponentA", VariantB: "ComponentB" }
* e.g., "Component" -> { Default: "Component" }
*/
function parseGenericDefinitions(genericContent) {
if (!genericContent.trim()) {
return {};
}
// Handle object literals directly (most common case)
const trimmed = genericContent.trim();
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
// Parse as object literal using the existing parser
const parsed = parseFunctionArguments(trimmed);
if (parsed.length === 1 && typeof parsed[0] === 'object' && !Array.isArray(parsed[0])) {
return parsed[0];
}
}
// Parse the generic content using the existing parser
const parsed = parseFunctionArguments(genericContent);
// If it's a single object, return it
if (parsed.length === 1 && typeof parsed[0] === 'object' && !Array.isArray(parsed[0])) {
return parsed[0];
}
// If it's a single string (single component), treat as Default variant
if (parsed.length === 1 && typeof parsed[0] === 'string') {
return {
Default: parsed[0]
};
}
// If it's multiple elements, try to interpret as an object
if (parsed.length > 1) {
const result = {};
parsed.forEach((item, index) => {
if (typeof item === 'string') {
result[`Variant${index + 1}`] = item;
} else if (typeof item === 'object' && item !== null) {
Object.assign(result, item);
}
});
return result;
}
return {};
}
/**
* Validates that a URL argument follows the expected convention
*/
function validateUrlArgument(url, functionName, filePath) {
const trimmedUrl = url.trim();
// Only accept import.meta.url
if (trimmedUrl === 'import.meta.url') {
return;
}
// For error messages, show the argument as parsed by parseFunctionArguments
// Simple string literals preserve their quotes, complex expressions are shown as parsed
const errorUrl = trimmedUrl;
throw new Error(`Invalid URL argument in ${functionName} call in ${filePath}. ` + `Expected 'import.meta.url' but got: ${errorUrl}`);
}
/**
* Validates that a variants argument is either an object mapping to imports or a single identifier
*/
function validateVariantsArgument(structuredVariants, functionName, filePath) {
if (!structuredVariants) {
throw new Error(`Invalid variants argument in ${functionName} call in ${filePath}. ` + `Expected an object mapping variant names to imports or a single component identifier.`);
}
// Check if it's a valid single identifier (string)
if (typeof structuredVariants === 'string') {
const trimmed = structuredVariants.trim();
if (!trimmed || !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(trimmed)) {
throw new Error(`Invalid variants argument in ${functionName} call in ${filePath}. ` + `Expected a valid component identifier, but got: "${trimmed}"`);
}
return; // Valid identifier
}
// Check if it's an array (object literal structure) or object (parsed key-value pairs)
if (Array.isArray(structuredVariants) || typeof structuredVariants === 'object' && structuredVariants !== null) {
return; // Valid object structure
}
throw new Error(`Invalid variants argument in ${functionName} call in ${filePath}. ` + `Expected an object mapping variant names to imports or a single component identifier, but got: ${typeof structuredVariants}`);
}
/**
* Parses a file to extract a single create* factory call and its variants and options
* Returns the parsed result with remaining content included
* Returns null if no create* call is found
*/
export async function parseCreateFactoryCall(code, filePath, parseOptions = {}, importsAndComments) {
// Find the first create* call in the code
const match = findFirstCreateFactoryCall(code, filePath, parseOptions);
// Return null if no create* call found
if (!match) {
return null;
}
// Check for multiple create* calls if allowMultipleFactories is false
if (!parseOptions.allowMultipleFactories) {
const secondMatch = findFirstCreateFactoryCall(code, filePath, parseOptions, match.functionEndIndex + 1);
if (secondMatch) {
throw new Error(`Multiple create* factory calls found in ${filePath}. Only one create* call per file is supported. Found 2 calls.`);
}
}
// Get import mappings from precomputed imports or parse them
importsAndComments = importsAndComments || (await parseImportsAndComments(code, filePath));
// Process the match using shared logic
const parsed = await processCreateFactoryMatch(match, code, filePath, parseOptions, importsAndComments);
// Calculate remaining content after the function call
const remaining = code.substring(match.functionEndIndex + 1);
const result = {
...parsed,
remaining,
importsAndComments // Include import data for reuse
};
return result;
}
/**
* Parses all create* factory calls in a file sequentially
* Returns a record of export names mapped to their parsed factory calls
*/
export async function parseAllCreateFactoryCalls(code, filePath, parseOptions = {}) {
const results = {};
let importsAndComments;
let searchIndex = 0;
// Process the code using single-pass approach
while (searchIndex < code.length) {
// Find the next create* call
const match = findFirstCreateFactoryCall(code, filePath, parseOptions, searchIndex);
if (!match) {
// No more create* calls found
break;
}
// Extract export name from the function call context
const beforeMatch = code.substring(0, match.functionStartIndex);
const exportMatch = beforeMatch.match(/export\s+const\s+(\w+)\s*=\s*$/m);
const exportName = exportMatch?.[1] || 'unknown';
// Get import mappings from precomputed imports or parse them
// eslint-disable-next-line no-await-in-loop
importsAndComments = importsAndComments || (await parseImportsAndComments(code, filePath));
// Process the match using shared logic
// eslint-disable-next-line no-await-in-loop
const parsedFactory = await processCreateFactoryMatch(match, code, filePath, parseOptions, importsAndComments);
results[exportName] = parsedFactory;
// Continue searching from after this function call
searchIndex = match.functionEndIndex + 1;
}
return results;
}
/**
* Processes a matched create* factory call into a ParsedCreateFactory object
* Handles all the common logic for validation, parsing, and transformation
*/
async function processCreateFactoryMatch(match, code, filePath, parseOptions, importsAndComments) {
const {
functionName,
fullMatch,
urlArg,
structuredVariants,
optionsStructured,
hasOptions,
argumentsStartIndex,
argumentsEndIndex
} = match;
const allowExternalVariants = parseOptions.allowExternalVariants || false;
const importMap = buildImportMap(importsAndComments, allowExternalVariants);
const namedExportsMap = buildNamedExportsMap(importsAndComments, allowExternalVariants);
const externals = importsAndComments.externals;
// Validate URL argument
validateUrlArgument(urlArg, functionName, filePath);
// Validate variants argument (skip when there is no variants argument)
const {
metadataOnly = false,
noVariants = false
} = parseOptions;
const skipVariants = metadataOnly || noVariants;
if (!skipVariants && structuredVariants !== undefined) {
validateVariantsArgument(structuredVariants, functionName, filePath);
}
// Extract URL (typically import.meta.url)
const url = urlArg.trim();
// Resolve variants using structured data (skip when there is no variants argument)
let variants;
let namedExports;
if (!skipVariants) {
if (structuredVariants !== undefined) {
// Use regular variants argument
const variantsResult = parseVariantsArgumentFromStructured(structuredVariants, importMap, namedExportsMap, functionName, filePath);
variants = variantsResult.variants;
namedExports = variantsResult.namedExports;
} else if (match.hasGenerics && match.structuredGenerics && Object.keys(match.structuredGenerics).length > 0) {
// Use generics as variants when no variants argument is provided and generics are not empty
const variantsResult = parseVariantsArgumentFromStructured(match.structuredGenerics, importMap, namedExportsMap, functionName, filePath);
variants = variantsResult.variants;
namedExports = variantsResult.namedExports;
}
}
// Parse options object
// Initialize with all options from structured data, then override specific fields
const options = optionsStructured && typeof optionsStructured === 'object' ? cleanStructuredData(optionsStructured) : {};
// Override with specific processing for known fields that need special handling
if (optionsStructured && typeof optionsStructured === 'object') {
if ('name' in optionsStructured) {
options.name = extractStringValue(optionsStructured.name);
}
if ('slug' in optionsStructured) {
options.slug = extractStringValue(optionsStructured.slug);
}
if ('skipPrecompute' in optionsStructured) {
const skipPrecomputeValue = optionsStructured.skipPrecompute;
if (skipPrecomputeValue === 'true' || skipPrecomputeValue === true) {
options.skipPrecompute = true;
} else if (skipPrecomputeValue === 'false' || skipPrecomputeValue === false) {
options.skipPrecompute = false;
}
}
// Handle precompute from structured data - clean for user consumption
if ('precompute' in optionsStructured) {
options.precompute = cleanStructuredData(optionsStructured.precompute);
}
}
// Transform externals from parseImportsAndComments format to simplified format
// Only include side-effect imports (where names array is empty)
const transformedExternals = {};
for (const [modulePath, externalImport] of Object.entries(externals)) {
// Only include side-effect imports (empty names array)
if (externalImport.names.length === 0) {
transformedExternals[modulePath] = []; // Empty array for side-effect imports
}
}
return {
functionName,
url,
variants,
namedExports,
options,
fullMatch,
hasOptions,
externals: transformedExternals,
argumentsStartIndex,
argumentsEndIndex,
// Add structured data for serialization - this preserves quotes for proper output
structuredUrl: urlArg,
structuredVariants,
structuredOptions: optionsStructured,
// Use original structured data, not cleaned options
hasGenerics: match.hasGenerics,
structuredGenerics: match.structuredGenerics
};
}
/**
* Finds the first create* factory call in code, starting from a given index
* Returns null if no create* call is found
*/
function findFirstCreateFactoryCall(code, filePath, parseOptions = {}, startIndex = 0) {
const createFunctionRegex = /\b(create\w*)\s*/g;
createFunctionRegex.lastIndex = startIndex;
const match = createFunctionRegex.exec(code);
if (!match) {
return null;
}
const functionName = match[1];
const matchStartIndex = match.index;
let currentIndex = match.index + match[0].length;
// Skip any whitespace after function name
while (currentIndex < code.length && /\s/.test(code[currentIndex])) {
currentIndex += 1;
}
if (currentIndex >= code.length) {
// No opening parenthesis found, try to find the next create* call
return findFirstCreateFactoryCall(code, filePath, parseOptions, match.index + match[0].length);
}
let genericContent = '';
let hasGenerics = false;
// Check if we have generics (starts with <)
if (code[currentIndex] === '<') {
hasGenerics = true;
let angleCount = 1;
let genericEndIndex = -1;
// Find the matching closing angle bracket, handling nesting
for (let i = currentIndex + 1; i < code.length; i += 1) {
const char = code[i];
if (char === '<') {
angleCount += 1;
} else if (char === '>') {
angleCount -= 1;
if (angleCount === 0) {
genericEndIndex = i;
break;
}
}
}
if (genericEndIndex === -1) {
// Unmatched angle brackets, try to find the next create* call
return findFirstCreateFactoryCall(code, filePath, parseOptions, match.index + match[0].length);
}
genericContent = code.substring(currentIndex + 1, genericEndIndex);
currentIndex = genericEndIndex + 1;
// Skip whitespace after generics
while (currentIndex < code.length && /\s/.test(code[currentIndex])) {
currentIndex += 1;
}
}
// Now look for the opening parenthesis
if (currentIndex >= code.length || code[currentIndex] !== '(') {
// No opening parenthesis found, try to find the next create* call
return findFirstCreateFactoryCall(code, filePath, parseOptions, match.index + match[0].length);
}
const parenIndex = currentIndex;
// Find the matching closing parenthesis
let parenCount = 0;
let endIndex = -1;
for (let i = parenIndex; i < code.length; i += 1) {
if (code[i] === '(') {
parenCount += 1;
} else if (code[i] === ')') {
parenCount -= 1;
if (parenCount === 0) {
endIndex = i;
break;
}
}
}
if (endIndex === -1) {
// Unmatched parentheses, try to find the next create* call
return findFirstCreateFactoryCall(code, filePath, parseOptions, match.index + match[0].length);
}
const fullMatch = code.substring(matchStartIndex, endIndex + 1);
const content = code.substring(parenIndex + 1, endIndex);
// Parse generic content if present
let structuredGenerics;
if (hasGenerics) {
// Parse the generic content as TypeScript type definitions
structuredGenerics = parseGenericDefinitions(genericContent);
}
// Split by commas at the top level, handling nested structures and comments
const structured = parseFunctionArguments(content);
// Validate the function follows the convention
const {
metadataOnly = false,
noVariants = false
} = parseOptions;
const skipVariants = metadataOnly || noVariants;
if (skipVariants) {
// No variants argument: expect 1-2 arguments (url, options?)
if (structured.length < 1 || structured.length > 2) {
throw new Error(`Invalid ${functionName} call in ${filePath}. ` + `Expected 1-2 arguments (url, options?) but got ${structured.length} arguments. ` + `For a no-variants factory, calls should follow: create*(url, options?)`);
}
} else if (hasGenerics && structured.length <= 2) {
// When generics are present AND we have 1-2 arguments, expect (url, options?)
if (structured.length < 1 || structured.length > 2) {
throw new Error(`Invalid ${functionName} call in ${filePath}. ` + `Expected 1-2 arguments (url, options?) but got ${structured.length} arguments. ` + `Functions with TypeScript generics should follow: create*<variants>(url, options?)`);
}
} else if (!hasGenerics && (structured.length < 2 || structured.length > 3)) {
// Normal mode: expect 2-3 arguments (url, variants, options?)
throw new Error(`Invalid ${functionName} call in ${filePath}. ` + `Expected 2-3 arguments (url, variants, options?) but got ${structured.length} arguments. ` + `Functions starting with 'create' must follow the convention: create*(url, variants, options?)`);
}
// Handle different argument patterns based on mode
if (skipVariants) {
// No variants argument: expect 1-2 arguments (url, options?)
if (structured.length === 1) {
const [urlArg] = structured;
return {
functionName,
fullMatch,
urlArg: typeof urlArg === 'string' ? urlArg.trim() : String(urlArg),
structuredVariants: hasGenerics ? structuredGenerics : undefined,
// Use generics as variants in metadata-only mode
optionsStructured: undefined,
hasOptions: false,
hasGenerics,
structuredGenerics,
functionStartIndex: matchStartIndex,
functionEndIndex: endIndex,
argumentsStartIndex: parenIndex + 1,
argumentsEndIndex: endIndex
};
}
if (structured.length === 2) {
const [urlArg, optionsStructured] = structured;
// Options should be an object
if (typeof optionsStructured === 'string' || !Array.isArray(optionsStructured) && typeof optionsStructured !== 'object') {
throw new Error(`Invalid options argument in ${functionName} call in ${filePath}. ` + `Expected an object but got: ${typeof optionsStructured === 'string' ? optionsStructured : JSON.stringify(optionsStructured)}`);
}
return {
functionName,
fullMatch,
urlArg: typeof urlArg === 'string' ? urlArg.trim() : String(urlArg),
structuredVariants: hasGenerics ? structuredGenerics : undefined,
// Use generics as variants in metadata-only mode
optionsStructured: typeof optionsStructured === 'object' && optionsStructured !== null ? optionsStructured : undefined,
hasOptions: true,
hasGenerics,
structuredGenerics,
functionStartIndex: matchStartIndex,
functionEndIndex: endIndex,
argumentsStartIndex: parenIndex + 1,
argumentsEndIndex: endIndex
};
}
} else if (!skipVariants && hasGenerics && structured.length === 1) {
// Generics-only mode (non-metadata): expect 1 argument (url) - use generics as variants
const [urlArg] = structured;
return {
functionName,
fullMatch,
urlArg: typeof urlArg === 'string' ? urlArg.trim() : String(urlArg),
structuredVariants: undefined,
// No explicit variants, will use generics later
optionsStructured: undefined,
hasOptions: false,
hasGenerics,
structuredGenerics,
functionStartIndex: matchStartIndex,
functionEndIndex: endIndex,
argumentsStartIndex: parenIndex + 1,
argumentsEndIndex: endIndex
};
} else if (!skipVariants && structured.length >= 2) {
// Normal mode: expect 2-3 arguments (url, variants, options?)
if (structured.length === 2) {
const [urlArg, secondArg] = structured;
if (hasGenerics) {
// With generics: 2 arguments means (url, options) - use generics as variants
return {
functionName,
fullMatch,
urlArg: typeof urlArg === 'string' ? urlArg.trim() : String(urlArg),
structuredVariants: undefined,
// Use generics
optionsStructured: typeof secondArg === 'object' && secondArg !== null ? secondArg : undefined,
hasOptions: true,
hasGenerics,
structuredGenerics,
functionStartIndex: matchStartIndex,
functionEndIndex: endIndex,
argumentsStartIndex: parenIndex + 1,
argumentsEndIndex: endIndex
};
}
// Without generics: 2 arguments means (url, variants) - use second arg as variants
return {
functionName,
fullMatch,
urlArg: typeof urlArg === 'string' ? urlArg.trim() : String(urlArg),
structuredVariants: secondArg,
optionsStructured: undefined,
hasOptions: false,
hasGenerics,
structuredGenerics,
functionStartIndex: matchStartIndex,
functionEndIndex: endIndex,
argumentsStartIndex: parenIndex + 1,
argumentsEndIndex: endIndex
};
}
if (structured.length === 3) {
const [urlArg, variantsStructured, optionsStructured] = structured;
// Options should be an object (Record<string, any>) or an empty object
if (typeof optionsStructured === 'string' || !Array.isArray(optionsStructured) && typeof optionsStructured !== 'object') {
throw new Error(`Invalid options argument in ${functionName} call in ${filePath}. ` + `Expected an object but got: ${typeof optionsStructured === 'string' ? optionsStructured : JSON.stringify(optionsStructured)}`);
}
return {
functionName,
fullMatch,
urlArg: typeof urlArg === 'string' ? urlArg.trim() : String(urlArg),
structuredVariants: variantsStructured,
optionsStructured: typeof optionsStructured === 'object' && optionsStructured !== null ? optionsStructured : undefined,
hasOptions: true,
// Options argument was provided
hasGenerics,
structuredGenerics,
functionStartIndex: matchStartIndex,
functionEndIndex: endIndex,
argumentsStartIndex: parenIndex + 1,
argumentsEndIndex: endIndex
};
}
}
// Should not reach here due to validation above
return null;
}