Skip to content

Commit

Permalink
chore: wip
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbbreuer committed Oct 21, 2024
1 parent 05ba6d0 commit 382d80f
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 29 deletions.
4 changes: 3 additions & 1 deletion fixtures/input/example-0001.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

/**
Expand Down
6 changes: 4 additions & 2 deletions fixtures/output/example-0001.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ export declare const conf: {
};

export declare const someObject: {
someString: string;
someNumber: number;
someString: 'Stacks';
someNumber: 1000;
someBoolean: true;
someFalse: false;
};

/**
Expand Down
120 changes: 94 additions & 26 deletions src/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')) {
Expand All @@ -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++) {
Expand Down

0 comments on commit 382d80f

Please sign in to comment.