246 行
8.2 KiB
JavaScript
246 行
8.2 KiB
JavaScript
'use strict'
|
|
|
|
const crypto = require('node:crypto')
|
|
const fs = require('node:fs')
|
|
const path = require('node:path')
|
|
|
|
const GENERATOR_VERSION = 1
|
|
const GENERATED_DIRECTORY = '.xuqm/generated'
|
|
|
|
function readJson(file) {
|
|
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
}
|
|
|
|
function toImportSpecifier(fromDirectory, target) {
|
|
const relative = path.relative(fromDirectory, target).split(path.sep).join('/')
|
|
const specifier = relative.startsWith('.') ? relative : `./${relative}`
|
|
return specifier.replace(/\.(?:js|jsx|ts|tsx)$/, '')
|
|
}
|
|
|
|
function generatedEntryPath(moduleId) {
|
|
return `${GENERATED_DIRECTORY}/entries/${moduleId}.js`
|
|
}
|
|
|
|
function resolvedGenerationInput(projectRoot, config) {
|
|
const generation = config.entryGeneration
|
|
if (!generation) return null
|
|
|
|
const required = ['appComponent', 'startupRegistry', 'startupShell', 'debugPluginLoader']
|
|
const missing = required.filter(
|
|
key => typeof generation[key] !== 'string' || generation[key].trim() === '',
|
|
)
|
|
if (missing.length > 0) {
|
|
throw new Error(`entryGeneration 缺少字段:${missing.join(', ')}`)
|
|
}
|
|
|
|
const sourceFiles = {
|
|
appComponent: path.resolve(projectRoot, generation.appComponent),
|
|
startupRegistry: path.resolve(projectRoot, generation.startupRegistry),
|
|
startupShell: path.resolve(projectRoot, generation.startupShell),
|
|
debugPluginLoader: path.resolve(projectRoot, generation.debugPluginLoader),
|
|
...(generation.commonBootstrap
|
|
? { commonBootstrap: path.resolve(projectRoot, generation.commonBootstrap) }
|
|
: {}),
|
|
}
|
|
const missingSources = Object.entries(sourceFiles)
|
|
.filter(([, file]) => !fs.existsSync(file))
|
|
.map(([key, file]) => `${key}: ${path.relative(projectRoot, file)}`)
|
|
const appJson = path.resolve(projectRoot, generation.appJson ?? './app.json')
|
|
if (!fs.existsSync(appJson)) {
|
|
missingSources.push(`appJson: ${path.relative(projectRoot, appJson)}`)
|
|
}
|
|
if (missingSources.length > 0) {
|
|
throw new Error(`入口生成源文件不存在:\n- ${missingSources.join('\n- ')}`)
|
|
}
|
|
|
|
const generatedModules = config.modules
|
|
.filter(module => ['startup', 'common', 'app'].includes(module.type))
|
|
.map(module => ({ id: module.id, type: module.type }))
|
|
const debugPlugins = config.modules
|
|
.filter(module => module.type === 'buz')
|
|
.map(module => ({ id: module.id, entry: module.entry }))
|
|
|
|
return {
|
|
appJson,
|
|
debugPlugins,
|
|
generatedModules,
|
|
generation,
|
|
sourceFiles,
|
|
}
|
|
}
|
|
|
|
function expectedFiles(input) {
|
|
return [
|
|
...input.generatedModules.map(module => `entries/${module.id}.js`),
|
|
'entries/debugPlugins.js',
|
|
'manifest.json',
|
|
]
|
|
}
|
|
|
|
function generationHash(projectRoot, config, input) {
|
|
const hash = crypto.createHash('sha256')
|
|
hash.update(
|
|
JSON.stringify({
|
|
generatorVersion: GENERATOR_VERSION,
|
|
entryGeneration: config.entryGeneration,
|
|
modules: config.modules.map(module => ({
|
|
id: module.id,
|
|
type: module.type,
|
|
entry: module.entry,
|
|
})),
|
|
}),
|
|
)
|
|
// 生成入口只引用源码路径,不复制源码内容;业务源码保存不能造成生成目录反复写入。
|
|
hash.update(
|
|
JSON.stringify(
|
|
[input.appJson, ...Object.values(input.sourceFiles)]
|
|
.map(file => path.relative(projectRoot, file))
|
|
.sort(),
|
|
),
|
|
)
|
|
return hash.digest('hex')
|
|
}
|
|
|
|
function currentGenerationIsValid(target, inputHash, files) {
|
|
const manifestFile = path.join(target, 'manifest.json')
|
|
if (!fs.existsSync(manifestFile)) return false
|
|
try {
|
|
const manifest = readJson(manifestFile)
|
|
return (
|
|
manifest.generatorVersion === GENERATOR_VERSION &&
|
|
manifest.inputHash === inputHash &&
|
|
files.every(file => fs.existsSync(path.join(target, file)))
|
|
)
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
function writeGeneratedEntries(projectRoot, directory, config, input, inputHash) {
|
|
const entriesDirectory = path.join(directory, 'entries')
|
|
fs.mkdirSync(entriesDirectory, { recursive: true })
|
|
const importFromEntries = target => toImportSpecifier(entriesDirectory, target)
|
|
const byType = new Map(input.generatedModules.map(module => [module.type, module]))
|
|
|
|
const startup = byType.get('startup')
|
|
if (startup) {
|
|
fs.writeFileSync(
|
|
path.join(entriesDirectory, `${startup.id}.js`),
|
|
[
|
|
'// 此文件由 @xuqm/rn-update 自动生成,删除后会重建,请勿编辑。',
|
|
"import { AppRegistry } from 'react-native';",
|
|
`import StartupShell from ${JSON.stringify(importFromEntries(input.sourceFiles.startupShell))};`,
|
|
`const { name: appName } = require(${JSON.stringify(importFromEntries(input.appJson))});`,
|
|
'AppRegistry.registerComponent(appName, () => StartupShell);',
|
|
'',
|
|
].join('\n'),
|
|
)
|
|
}
|
|
|
|
const common = byType.get('common')
|
|
if (common) {
|
|
fs.writeFileSync(
|
|
path.join(entriesDirectory, `${common.id}.js`),
|
|
[
|
|
'// 此文件由 @xuqm/rn-update 自动生成,删除后会重建,请勿编辑。',
|
|
...(input.sourceFiles.commonBootstrap
|
|
? [`import ${JSON.stringify(importFromEntries(input.sourceFiles.commonBootstrap))};`]
|
|
: []),
|
|
'',
|
|
].join('\n'),
|
|
)
|
|
}
|
|
|
|
const app = byType.get('app')
|
|
if (app) {
|
|
fs.writeFileSync(
|
|
path.join(entriesDirectory, `${app.id}.js`),
|
|
[
|
|
'// 此文件由 @xuqm/rn-update 自动生成,删除后会重建,请勿编辑。',
|
|
`import App from ${JSON.stringify(importFromEntries(input.sourceFiles.appComponent))};`,
|
|
`import { registerAppComponent } from ${JSON.stringify(
|
|
importFromEntries(input.sourceFiles.startupRegistry),
|
|
)};`,
|
|
'registerAppComponent(App);',
|
|
'',
|
|
].join('\n'),
|
|
)
|
|
}
|
|
|
|
fs.writeFileSync(
|
|
path.join(entriesDirectory, 'debugPlugins.js'),
|
|
[
|
|
'// 此文件由 @xuqm/rn-update 自动生成,删除后会重建,请勿编辑。',
|
|
`import { registerDebugPluginLoader } from ${JSON.stringify(
|
|
importFromEntries(input.sourceFiles.debugPluginLoader),
|
|
)};`,
|
|
'',
|
|
...input.debugPlugins.flatMap(plugin => [
|
|
`registerDebugPluginLoader(${JSON.stringify(plugin.id)}, async () => {`,
|
|
` require(${JSON.stringify(importFromEntries(path.resolve(projectRoot, plugin.entry)))});`,
|
|
'});',
|
|
'',
|
|
]),
|
|
].join('\n'),
|
|
)
|
|
|
|
fs.writeFileSync(
|
|
path.join(directory, 'manifest.json'),
|
|
`${JSON.stringify(
|
|
{
|
|
schemaVersion: 1,
|
|
generatorVersion: GENERATOR_VERSION,
|
|
inputHash,
|
|
files: expectedFiles(input).filter(file => file !== 'manifest.json'),
|
|
},
|
|
null,
|
|
2,
|
|
)}\n`,
|
|
)
|
|
}
|
|
|
|
/**
|
|
* `.xuqm` 只保存可重建产物。生成采用临时目录替换,进程中断时不会暴露半套入口。
|
|
*/
|
|
function ensureGeneratedEntries(projectRoot = process.cwd(), configOverride) {
|
|
const configFile = path.join(projectRoot, 'xuqm.modules.json')
|
|
if (!fs.existsSync(configFile)) return { generated: false, reason: 'config-missing' }
|
|
const config = configOverride ?? readJson(configFile)
|
|
const input = resolvedGenerationInput(projectRoot, config)
|
|
if (!input) return { generated: false, reason: 'generation-disabled' }
|
|
|
|
const target = path.join(projectRoot, GENERATED_DIRECTORY)
|
|
const files = expectedFiles(input)
|
|
const inputHash = generationHash(projectRoot, config, input)
|
|
if (currentGenerationIsValid(target, inputHash, files)) {
|
|
return { generated: false, inputHash, target }
|
|
}
|
|
|
|
const parent = path.dirname(target)
|
|
const temporary = path.join(
|
|
parent,
|
|
`generated.tmp-${process.pid}-${crypto.randomBytes(4).toString('hex')}`,
|
|
)
|
|
const backup = path.join(parent, `generated.old-${process.pid}`)
|
|
fs.mkdirSync(parent, { recursive: true })
|
|
fs.rmSync(temporary, { force: true, recursive: true })
|
|
fs.rmSync(backup, { force: true, recursive: true })
|
|
try {
|
|
writeGeneratedEntries(projectRoot, temporary, config, input, inputHash)
|
|
if (fs.existsSync(target)) fs.renameSync(target, backup)
|
|
fs.renameSync(temporary, target)
|
|
fs.rmSync(backup, { force: true, recursive: true })
|
|
} catch (error) {
|
|
fs.rmSync(temporary, { force: true, recursive: true })
|
|
if (!fs.existsSync(target) && fs.existsSync(backup)) fs.renameSync(backup, target)
|
|
throw error
|
|
}
|
|
return { generated: true, inputHash, target }
|
|
}
|
|
|
|
module.exports = {
|
|
GENERATED_DIRECTORY,
|
|
ensureGeneratedEntries,
|
|
generatedEntryPath,
|
|
}
|