jingrow-ui/vite/doctypeInterfaceGenerator.js
jingrow c7bac1a7a0
Some checks failed
Publish on NPM / publish (push) Has been cancelled
Build and Deploy Storybook / build (push) Has been cancelled
Tests / test (push) Has been cancelled
initial commit
2025-10-24 00:40:30 +08:00

256 lines
7.2 KiB
JavaScript

import fs from 'fs/promises'
import path from 'path'
export class DocTypeInterfaceGenerator {
constructor(appsPath, appDoctypeMap, outputPath) {
this.appsPath = appsPath
this.appDoctypeMap = appDoctypeMap
this.outputPath = outputPath
this.processedDoctypes = new Set()
this.existingInterfaces = {}
this.updatedInterfaces = 0
this.jsonFileCache = new Map()
this.summary = {
processed: 0,
updated: 0,
skipped: 0,
notFound: 0,
details: [],
}
}
async generate() {
await this.loadExistingInterfaces()
const promises = []
for (const appName of Object.keys(this.appDoctypeMap)) {
for (const pagetypeName of this.appDoctypeMap[appName]) {
promises.push(this.processDoctype(appName, pagetypeName))
}
}
await Promise.all(promises)
this.printSummary()
if (this.updatedInterfaces > 0) {
const baseInterfaces = this.generateBaseInterfaces()
const interfacesString = [
baseInterfaces,
...Object.values(this.existingInterfaces),
].join('\n')
await fs.mkdir(path.dirname(this.outputPath), { recursive: true })
await fs.writeFile(this.outputPath, interfacesString)
}
}
printSummary() {
console.log('\nJingrow Type Generation Summary:')
console.log(`- Total processed: ${this.summary.processed} doctypes`)
console.log(`- Updated: ${this.summary.updated} interfaces`)
console.log(`- Skipped: ${this.summary.skipped} (no changes)`)
if (this.summary.notFound > 0) {
console.log(`- Not found: ${this.summary.notFound}`)
}
if (this.updatedInterfaces > 0) {
console.log(
`\nOutput file updated with ${this.updatedInterfaces} interface${this.updatedInterfaces === 1 ? '' : 's'}.`,
)
} else {
console.log('\nNo new schema changes.')
}
}
async loadExistingInterfaces() {
try {
const outputContent = await fs.readFile(this.outputPath, 'utf8')
const interfaceMatches = outputContent.match(
/\/\/ Last updated: [^\n]+\nexport interface\s+\w+\s+extends\s+\w+\s+{[^}]+}\n/g,
)
if (interfaceMatches) {
interfaceMatches.forEach((interfaceStr) => {
const match = interfaceStr.match(/export interface\s+(\w+)\s+extends/)
if (match) {
const interfaceName = match[1]
this.existingInterfaces[interfaceName] = interfaceStr
}
})
}
} catch (err) {
if (err.code !== 'ENOENT') {
throw err
}
}
}
async processDoctype(appName, pagetypeName) {
if (this.processedDoctypes.has(pagetypeName)) {
return
}
this.processedDoctypes.add(pagetypeName)
this.summary.processed++
const jsonFilePath = await this.findJsonFile(appName, pagetypeName)
if (!jsonFilePath) {
this.summary.notFound++
this.summary.details.push(`${pagetypeName}: not found`)
return
}
const jsonData = JSON.parse(await fs.readFile(jsonFilePath, 'utf8'))
const lastModified = jsonData.modified
const interfaceName = jsonData.name.replace(/\s+/g, '')
const existingInterface = this.existingInterfaces[interfaceName]
if (
existingInterface &&
existingInterface.includes(`// Last updated: ${lastModified}`)
) {
this.summary.skipped++
this.summary.details.push(`${pagetypeName}: skipped (no changes)`)
return
}
const fields = jsonData.fields
const typeMapping = {
Data: 'string',
'Text Editor': 'string',
Link: 'string',
Table: 'any[]',
'Table MultiSelect': 'any[]',
Percent: 'number',
Int: 'number',
Float: 'number',
Datetime: 'string', // "YYYY-MM-DD HH:MM:SS"
Date: 'string', // "YYYY-MM-DD"
Check: '0 | 1',
'Attach Image': 'string',
'Dynamic Link': 'string',
'Small Text': 'string',
Color: 'string',
Text: 'string',
Autocomplete: 'string',
Password: 'string',
Code: 'string',
'Read Only': 'string',
}
let interfaceString = `// Last updated: ${lastModified}\nexport interface ${interfaceName} extends ${jsonData.istable ? 'ChildDocType' : 'PageType'} {\n`
for (const field of fields) {
if (
[
'Section Break',
'Column Break',
'Tab Break',
'HTML',
'Button',
].includes(field.fieldtype)
) {
continue
}
let tsType = typeMapping[field.fieldtype] || 'any'
if (field.fieldtype === 'Select' && field.options) {
const options = field.options
.split('\n')
.map((option) => `'${option}'`)
.join(' | ')
tsType = options
} else if (
['Table', 'Table MultiSelect'].includes(field.fieldtype) &&
field.options
) {
const relatedDoctype = field.options
tsType = `${relatedDoctype.replace(/\s+/g, '')}[]`
await this.processDoctype(
appName,
relatedDoctype.toLowerCase().replace(/ /g, '_'),
)
}
let description = `/** ${field.label}: ${field.fieldtype}`
if (
['Table', 'Table MultiSelect', 'Link', 'Dynamic Link'].includes(
field.fieldtype,
) &&
field.options
) {
description += ` (${field.options})`
}
description += ' */'
let optional =
field.reqd ||
['Check', 'Table', 'Table MultiSelect'].includes(field.fieldtype)
? ''
: '?'
interfaceString += ` ${description}\n ${field.fieldname}${optional}: ${tsType};\n`
}
interfaceString += `}\n`
this.updatedInterfaces++
this.existingInterfaces[interfaceName] = interfaceString
this.summary.updated++
this.summary.details.push(`${pagetypeName}: updated`)
}
async findJsonFile(appName, pagetypeName) {
const cacheKey = `${appName}/${pagetypeName}`
if (this.jsonFileCache.has(cacheKey)) {
return this.jsonFileCache.get(cacheKey)
}
const targetPattern = path.join(
'pagetype',
pagetypeName,
`${pagetypeName}.json`,
)
let foundPath = null
const searchDirectory = async (directory) => {
const files = await fs.readdir(directory)
for (const file of files) {
const fullPath = path.join(directory, file)
try {
const stat = await fs.stat(fullPath)
if (stat.isDirectory()) {
await searchDirectory(fullPath)
} else if (fullPath.endsWith(targetPattern)) {
foundPath = fullPath
return
}
} catch (error) {
// Skip files/directories that can't be accessed (e.g., broken symlinks, permission denied)
if (error.code === 'ENOENT' || error.code === 'EACCES') {
continue
}
// Re-throw other unexpected errors
throw error
}
}
}
await searchDirectory(path.join(this.appsPath, appName))
this.jsonFileCache.set(cacheKey, foundPath)
return foundPath
}
generateBaseInterfaces() {
return `interface PageType {
name: string;
creation: string;
modified: string;
owner: string;
modified_by: string;
}
interface ChildDocType extends PageType {
parent?: string;
parentfield?: string;
parenttype?: string;
idx?: number;
}
`
}
}