|
| 1 | +const fs = require('fs'); |
| 2 | +const path = require('path'); |
| 3 | +const { execSync } = require('child_process'); |
| 4 | +const ts = require('typescript'); |
| 5 | +const { PROJECT_ROOT } = require('./config'); |
| 6 | + |
| 7 | +const COMPILE_TS = false; |
| 8 | +const GENERATE_DTS = false; |
| 9 | + |
| 10 | + |
| 11 | +/** 编译选项,与根目录 tsconfig.json 中的 compilerOptions 保持一致 */ |
| 12 | +const COMPILER_OPTIONS = { |
| 13 | + target: ts.ScriptTarget.ES2015, |
| 14 | + module: ts.ModuleKind.ESNext, |
| 15 | + moduleResolution: ts.ModuleResolutionKind.NodeJs, |
| 16 | + esModuleInterop: true, |
| 17 | + allowSyntheticDefaultImports: true, |
| 18 | + experimentalDecorators: true, |
| 19 | + declaration: false, |
| 20 | + skipLibCheck: true, |
| 21 | + resolveJsonModule: true, |
| 22 | + allowJs: true, |
| 23 | + removeComments: false, |
| 24 | +}; |
| 25 | + |
| 26 | +/** |
| 27 | + * 将单个 .ts 文件编译为 .js 文件 |
| 28 | + * @param {string} inputFile 源 .ts 文件的绝对路径 |
| 29 | + * @param {string} rawOutputFile 目标文件的绝对路径(仍是 .ts 后缀,函数内部会改为 .js) |
| 30 | + * @returns {boolean|undefined} 成功返回 true,非 .ts 文件返回 undefined |
| 31 | + */ |
| 32 | +function processTs(inputFile, rawOutputFile) { |
| 33 | + if (!COMPILE_TS) return; |
| 34 | + |
| 35 | + // 只处理 .ts 文件(排除 .d.ts) |
| 36 | + if (!inputFile.endsWith('.ts') || inputFile.endsWith('.d.ts')) { |
| 37 | + return; |
| 38 | + } |
| 39 | + |
| 40 | + // 跳过纯类型定义文件(props.ts / type.ts / common.ts),这些文件仅提供类型声明,不需要编译为 JS |
| 41 | + const basename = path.basename(inputFile); |
| 42 | + if (basename === 'props.ts' || basename === 'type.ts' || basename === 'common.ts') { |
| 43 | + return; |
| 44 | + } |
| 45 | + |
| 46 | + try { |
| 47 | + const tsCode = fs.readFileSync(inputFile, 'utf8'); |
| 48 | + |
| 49 | + const result = ts.transpileModule(tsCode, { |
| 50 | + compilerOptions: COMPILER_OPTIONS, |
| 51 | + fileName: path.basename(inputFile), |
| 52 | + }); |
| 53 | + |
| 54 | + // 将 4 空格缩进转换为 2 空格缩进 |
| 55 | + const outputText = result.outputText.replace(/^( {4})+/gm, match => ' '.repeat(match.length / 4)); |
| 56 | + |
| 57 | + // 输出文件后缀改为 .js |
| 58 | + const outputFile = rawOutputFile.replace(/\.ts$/, '.js'); |
| 59 | + fs.mkdirSync(path.dirname(outputFile), { recursive: true }); |
| 60 | + fs.writeFileSync(outputFile, outputText); |
| 61 | + console.log(`✅ TS 编译完成: ${path.relative(PROJECT_ROOT, outputFile)}`); |
| 62 | + return true; |
| 63 | + } catch (err) { |
| 64 | + console.error(`❌ TS 编译失败: ${inputFile}`, err); |
| 65 | + return false; |
| 66 | + } |
| 67 | +} |
| 68 | + |
| 69 | +/** |
| 70 | + * 使用 tsc 命令行批量生成 .d.ts 声明文件 |
| 71 | + * @param {string} sourceDir 源码根目录(如 uniapp-components) |
| 72 | + * @param {string} targetDir 输出目录(如 dist) |
| 73 | + */ |
| 74 | +function generateDts(sourceDir, targetDir) { |
| 75 | + if (!GENERATE_DTS) return; |
| 76 | + // 收集 sourceDir 下所有 .ts 文件(排除 .d.ts、node_modules、_example) |
| 77 | + const tsFiles = collectTsFiles(sourceDir); |
| 78 | + |
| 79 | + if (tsFiles.length === 0) { |
| 80 | + console.log('⚠️ 未找到需要生成 .d.ts 的 .ts 文件'); |
| 81 | + return; |
| 82 | + } |
| 83 | + |
| 84 | + console.log(`\n📝 开始生成 .d.ts 声明文件,共 ${tsFiles.length} 个 .ts 文件...`); |
| 85 | + |
| 86 | + // 创建临时 tsconfig 文件,避免命令行参数过长 |
| 87 | + const tmpTsConfig = path.join(sourceDir, '.tsconfig.dts.tmp.json'); |
| 88 | + const tsconfigContent = { |
| 89 | + compilerOptions: { |
| 90 | + declaration: true, |
| 91 | + emitDeclarationOnly: true, |
| 92 | + skipLibCheck: true, |
| 93 | + target: 'ES2015', |
| 94 | + lib: ['ES2015', 'ES2016', 'ES2017', 'DOM'], |
| 95 | + module: 'ESNext', |
| 96 | + moduleResolution: 'node', |
| 97 | + experimentalDecorators: true, |
| 98 | + types: ['miniprogram-api-typings', '@dcloudio/types'], |
| 99 | + baseUrl: sourceDir, |
| 100 | + paths: { |
| 101 | + './superComponent': ['./superComponent.placeholder'], |
| 102 | + }, |
| 103 | + outDir: targetDir, |
| 104 | + rootDir: sourceDir, |
| 105 | + }, |
| 106 | + files: tsFiles, |
| 107 | + }; |
| 108 | + |
| 109 | + fs.writeFileSync(tmpTsConfig, JSON.stringify(tsconfigContent, null, 2)); |
| 110 | + |
| 111 | + const tscBin = path.resolve(PROJECT_ROOT, 'node_modules/.bin/tsc'); |
| 112 | + |
| 113 | + try { |
| 114 | + execSync(`"${tscBin}" --project "${tmpTsConfig}"`, { |
| 115 | + cwd: PROJECT_ROOT, |
| 116 | + stdio: 'pipe', |
| 117 | + }); |
| 118 | + console.log(`✅ .d.ts 声明文件生成完成,输出到 ${path.relative(PROJECT_ROOT, targetDir)}`); |
| 119 | + } catch (err) { |
| 120 | + const stdout = err.stdout ? err.stdout.toString() : ''; |
| 121 | + const stderr = err.stderr ? err.stderr.toString() : ''; |
| 122 | + const errorOutput = stdout || stderr; |
| 123 | + // tsc 有类型错误但默认 noEmitOnError=false,.d.ts 仍然会生成 |
| 124 | + // 仅打印警告,不中断流程 |
| 125 | + if (errorOutput) { |
| 126 | + console.warn(`⚠️ tsc 生成 .d.ts 时存在类型警告(不影响产物生成):\n${errorOutput}`); |
| 127 | + } |
| 128 | + console.log(`✅ .d.ts 声明文件生成完成(含警告),输出到 ${path.relative(PROJECT_ROOT, targetDir)}`); |
| 129 | + } finally { |
| 130 | + // 清理临时文件 |
| 131 | + try { |
| 132 | + fs.unlinkSync(tmpTsConfig); |
| 133 | + } catch (e) { /* ignore */ } |
| 134 | + } |
| 135 | +} |
| 136 | + |
| 137 | +/** |
| 138 | + * 递归收集目录下的所有 .ts 文件(排除 .d.ts、node_modules、_example) |
| 139 | + */ |
| 140 | +function collectTsFiles(dir) { |
| 141 | + const results = []; |
| 142 | + |
| 143 | + function walk(currentDir) { |
| 144 | + const entries = fs.readdirSync(currentDir, { withFileTypes: true }); |
| 145 | + for (const entry of entries) { |
| 146 | + const fullPath = path.join(currentDir, entry.name); |
| 147 | + if (entry.isDirectory()) { |
| 148 | + if (entry.name === 'node_modules' || entry.name === '_example') continue; |
| 149 | + walk(fullPath); |
| 150 | + } else if (entry.isFile() && entry.name.endsWith('.ts') && !entry.name.endsWith('.d.ts')) { |
| 151 | + results.push(fullPath); |
| 152 | + } |
| 153 | + } |
| 154 | + } |
| 155 | + |
| 156 | + walk(dir); |
| 157 | + return results; |
| 158 | +} |
| 159 | + |
| 160 | +module.exports = { |
| 161 | + processTs, |
| 162 | + generateDts, |
| 163 | +}; |
0 commit comments