yx_generate_api_js/src/core/sync.js

153 lines
3.9 KiB
JavaScript

import fs from 'node:fs/promises'
import path from 'node:path'
export const syncExternalIndex = async ({
projectRoot,
outputDir,
externalIndexFile,
syncOptions,
}) => {
if (!externalIndexFile) {
throw new Error('externalIndexFile is required for sync')
}
const generatedIndexPath = path.join(outputDir, 'index.js')
const [generatedContent, targetContent] = await Promise.all([
fs.readFile(generatedIndexPath, 'utf8'),
readFileIfExists(externalIndexFile),
])
const nextContent = buildTargetFileContent({
targetContent,
generatedContent,
generatedIndexPath,
externalIndexFile,
exportFrom: syncOptions.exportFrom,
includeGeneratedIndexSnapshot: syncOptions.includeGeneratedIndexSnapshot,
snapshotTitle: syncOptions.snapshotTitle,
blockStart: syncOptions.blockStart,
blockEnd: syncOptions.blockEnd,
projectRoot,
})
if (normalizeLineEndings(nextContent) === normalizeLineEndings(targetContent)) {
console.log(`no changes: ${toProjectRelativePath(projectRoot, externalIndexFile)}`)
return
}
await fs.mkdir(path.dirname(externalIndexFile), { recursive: true })
await fs.writeFile(externalIndexFile, nextContent, 'utf8')
console.log(`synced: ${toProjectRelativePath(projectRoot, externalIndexFile)}`)
}
const buildTargetFileContent = ({
targetContent,
generatedContent,
generatedIndexPath,
externalIndexFile,
exportFrom,
includeGeneratedIndexSnapshot,
snapshotTitle,
blockStart,
blockEnd,
projectRoot,
}) => {
const lineEnding = targetContent.includes('\r\n') ? '\r\n' : '\n'
const normalizedTargetContent = normalizeLineEndings(targetContent)
const managedBlock = buildManagedBlock({
generatedContent,
generatedIndexPath,
externalIndexFile,
exportFrom,
includeGeneratedIndexSnapshot,
snapshotTitle,
blockStart,
blockEnd,
projectRoot,
})
const blockPattern = new RegExp(
`${escapeRegExp(blockStart)}[\\s\\S]*?${escapeRegExp(blockEnd)}`,
'm',
)
const trimmedTargetContent = normalizedTargetContent.trimEnd()
let nextContent = managedBlock
if (trimmedTargetContent) {
nextContent = blockPattern.test(normalizedTargetContent)
? trimmedTargetContent.replace(blockPattern, managedBlock)
: `${trimmedTargetContent}\n\n${managedBlock}`
}
return `${nextContent}\n`.replace(/\n/g, lineEnding)
}
const buildManagedBlock = ({
generatedContent,
generatedIndexPath,
externalIndexFile,
exportFrom,
includeGeneratedIndexSnapshot,
snapshotTitle,
blockStart,
blockEnd,
projectRoot,
}) => {
const lines = [
blockStart,
`// Synced from '${toProjectRelativePath(projectRoot, generatedIndexPath)}'. Do not edit manually.`,
]
if (includeGeneratedIndexSnapshot) {
lines.push(snapshotTitle, ...buildCommentLines(generatedContent))
}
lines.push(
`export * from '${exportFrom || buildExportFrom(externalIndexFile, path.dirname(generatedIndexPath))}'`,
)
lines.push(blockEnd)
return lines.join('\n')
}
const buildExportFrom = (externalIndexFile, generatedDir) => {
let relativeImportPath = path
.relative(path.dirname(externalIndexFile), generatedDir)
.replace(/\\/g, '/')
if (!relativeImportPath.startsWith('.')) {
relativeImportPath = `./${relativeImportPath}`
}
return relativeImportPath
}
const buildCommentLines = (content) => {
return normalizeLineEndings(content)
.trimEnd()
.split('\n')
.map((line) => (line ? `// ${line}` : '//'))
}
const readFileIfExists = async (targetPath) => {
try {
return await fs.readFile(targetPath, 'utf8')
} catch (error) {
if (error?.code === 'ENOENT') {
return ''
}
throw error
}
}
const normalizeLineEndings = (content) => {
return content.replace(/\r\n/g, '\n')
}
const escapeRegExp = (value) => {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
const toProjectRelativePath = (projectRoot, targetPath) => {
return path.relative(projectRoot, targetPath).replace(/\\/g, '/')
}