From 382d80f55e31ddbeedd5d872842e8d858372636f Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 21 Oct 2024 14:14:43 +0200 Subject: [PATCH] chore: wip --- fixtures/input/example-0001.ts | 4 +- fixtures/output/example-0001.d.ts | 6 +- src/extract.ts | 120 +++++++++++++++++++++++------- 3 files changed, 101 insertions(+), 29 deletions(-) diff --git a/fixtures/input/example-0001.ts b/fixtures/input/example-0001.ts index 57d84ad..47c231a 100644 --- a/fixtures/input/example-0001.ts +++ b/fixtures/input/example-0001.ts @@ -10,12 +10,14 @@ import { resolve } from 'node:path' */ export const conf: { [key: string]: string } = { apiUrl: 'https://api.stacksjs.org', - timeout: '5000', + timeout: '5000', // as string } export const someObject = { someString: 'Stacks', someNumber: 1000, + someBoolean: true, + someFalse: false, } /** diff --git a/fixtures/output/example-0001.d.ts b/fixtures/output/example-0001.d.ts index fedcf82..0916baa 100644 --- a/fixtures/output/example-0001.d.ts +++ b/fixtures/output/example-0001.d.ts @@ -10,8 +10,10 @@ export declare const conf: { }; export declare const someObject: { - someString: string; - someNumber: number; + someString: 'Stacks'; + someNumber: 1000; + someBoolean: true; + someFalse: false; }; /** diff --git a/src/extract.ts b/src/extract.ts index 171193c..ce18faf 100644 --- a/src/extract.ts +++ b/src/extract.ts @@ -22,7 +22,9 @@ export function generateDtsTypes(sourceCode: string): string { let lastCommentBlock = '' function processDeclaration(declaration: string): string { - const trimmed = declaration.trim() + // Remove comments + const declWithoutComments = declaration.replace(/\/\/.*$/gm, '').trim() + const trimmed = declWithoutComments // Handle imports if (trimmed.startsWith('import')) { @@ -38,40 +40,106 @@ export function generateDtsTypes(sourceCode: string): string { // Handle const declarations if (trimmed.startsWith('export const')) { - const [name, rest] = trimmed.split('=') - const type = name.split(':')[1]?.trim() || 'any' - return `export declare const ${name.split(':')[0].replace('export const', '').trim()}: ${type};` + const [name, rest] = trimmed.split('=').map(s => s.trim()) + const declaredType = name.includes(':') ? name.split(':')[1].trim() : null + + if (rest) { + // If we have a value, use it to infer the most specific type + if (rest.startsWith('{')) { + // For object literals, preserve the exact structure + const objectType = parseObjectLiteral(rest) + return `export declare const ${name.split(':')[0].replace('export const', '').trim()}: ${objectType};` + } + else { + // For primitive values, use the exact value as the type + const valueType = preserveValueType(rest) + return `export declare const ${name.split(':')[0].replace('export const', '').trim()}: ${valueType};` + } + } + else if (declaredType) { + // If no value but a declared type, use the declared type + return `export declare const ${name.split(':')[0].replace('export const', '').trim()}: ${declaredType};` + } + else { + // If no value and no declared type, default to 'any' + return `export declare const ${name.split(':')[0].replace('export const', '').trim()}: any;` + } } - // Handle interface declarations - if (trimmed.startsWith('export interface')) { - return trimmed.replace(/\s*\{\s*([^}]+)\}\s*$/, (_, content) => { - const formattedContent = content - .split(';') - .map(prop => prop.trim()) - .filter(Boolean) - .map(prop => ` ${prop};`) - .join('\n') - return ` {\n${formattedContent}\n}` - }).replace('export ', 'export declare ') + // Handle other declarations (interfaces, types, functions) + if (trimmed.startsWith('export')) { + return trimmed.endsWith(';') ? trimmed : `${trimmed};` } - // Handle type declarations - if (trimmed.startsWith('export type')) { - return `export declare ${trimmed.replace('export ', '')}` - } + return '' + } - // Handle function declarations - if (trimmed.includes('function')) { - return `export declare ${trimmed.replace('export ', '').split('{')[0].trim()};` + function preserveValueType(value: string): string { + value = value.trim() + if (value.startsWith('\'') || value.startsWith('"')) { + // Preserve string literals exactly as they appear in the source + // Ensure that the entire string is captured, including any special characters + const match = value.match(/^(['"])(.*)\1$/) + if (match) { + return `'${match[2]}'` // Return the content of the string, wrapped in single quotes + } + return 'string' // Fallback to string if the regex doesn't match + } + else if (value === 'true' || value === 'false') { + return value // Keep true and false as literal types } + else if (!Number.isNaN(Number(value))) { + return value // Keep numbers as literal types + } + else if (value.startsWith('[') && value.endsWith(']')) { + return 'any[]' // Generic array type + } + else { + return 'string' // Default to string for other cases + } + } - // Handle default exports - if (trimmed.startsWith('export default')) { - return `export default ${trimmed.replace('export default ', '')};` + function parseObjectLiteral(objectLiteral: string): string { + // Remove the opening and closing braces + const content = objectLiteral.slice(1, -1).trim() + + // Split the object literal into key-value pairs, respecting nested structures + const pairs = [] + let currentPair = '' + let nestLevel = 0 + let inQuotes = false + + for (const char of content) { + if (char === '{' && !inQuotes) + nestLevel++ + if (char === '}' && !inQuotes) + nestLevel-- + if (char === '"' || char === '\'') + inQuotes = !inQuotes + + if (char === ',' && nestLevel === 0 && !inQuotes) { + pairs.push(currentPair.trim()) + currentPair = '' + } + else { + currentPair += char + } } + if (currentPair) + pairs.push(currentPair.trim()) + + const parsedProperties = pairs.map((pair) => { + const [key, ...valueParts] = pair.split(':') + const value = valueParts.join(':').trim() // Rejoin in case the value contained a colon + + if (!key) + return null // Invalid pair + + const sanitizedValue = preserveValueType(value) + return ` ${key.trim()}: ${sanitizedValue};` + }).filter(Boolean) - return trimmed + return `{\n${parsedProperties.join('\n')}\n}` } for (let i = 0; i < lines.length; i++) {