Open
Description
问题
- 数据模型的TS提示,在含JS源码的情况下,问题多多
- 开发云函数时,缺乏对
event
事件入参的明确定义,譬如 定时器事件、HTTP事件
需求
需要一个能够支持云函数开发的Typescript模板并且该模板能有对应的提示词来辅助AI开发。
自有项目的部分Code分享
build ts
这里并没有使用常规的打包工具,而是使用bun
直接编译typescript
文件以及对js文件做后处理以支持 Nodejs18+
正常使用 ES Module
#!/usr/bin/env node
const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
/**
* 使用bun编译TypeScript文件
* 使用目录级别编译,保持模块引用关系和目录结构
*/
const sourceDir = 'functions';
const targetDir = 'dist';
function findFunctionDirs(dir) {
const functionDirs = [];
const items = fs.readdirSync(dir);
for (const item of items) {
const fullPath = path.join(dir, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory() && item !== 'node_modules' && item !== 'dist') {
// 检查是否包含TypeScript文件
if (hasTypeScriptFiles(fullPath)) {
functionDirs.push({
name: item,
path: fullPath,
outputPath: path.join(targetDir, item)
});
}
}
}
return functionDirs;
}
function hasTypeScriptFiles(dir) {
try {
const items = fs.readdirSync(dir);
for (const item of items) {
const fullPath = path.join(dir, item);
const stat = fs.statSync(fullPath);
if (stat.isFile() && (item.endsWith('.ts') || item.endsWith('.tsx'))) {
return true;
} else if (stat.isDirectory() && item !== 'node_modules' && item !== 'dist') {
if (hasTypeScriptFiles(fullPath)) {
return true;
}
}
}
} catch (error) {
// 忽略权限错误等
}
return false;
}
function ensureDir(dirPath) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
/**
* 后处理转译后的文件,添加 .mjs 扩展名到相对路径的 import 语句
* @param {string} dirPath 目录路径
* @param {string} functionOutputPath 函数输出根目录
*/
function postProcessImportsInDir(dirPath, functionOutputPath) {
const items = fs.readdirSync(dirPath);
for (const item of items) {
const fullPath = path.join(dirPath, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory() && item !== 'node_modules') {
postProcessImportsInDir(fullPath, functionOutputPath);
} else if (stat.isFile() && item.endsWith('.mjs')) {
postProcessImports(fullPath, functionOutputPath);
}
}
}
/**
* 检查目标路径是否存在对应的index文件
* @param {string} importPath 导入路径
* @param {string} currentFileDir 当前文件所在目录
* @param {string} functionOutputPath 函数输出目录
* @returns {string} 修正后的导入路径
*/
function resolveImportPath(importPath, currentFileDir, functionOutputPath) {
// 只处理相对路径
if (!importPath.startsWith('./') && !importPath.startsWith('../')) {
return importPath;
}
// 计算绝对路径
const absolutePath = path.resolve(currentFileDir, importPath);
// 检查是否存在对应的index.mjs文件
const indexPath = path.join(absolutePath, 'index.mjs');
if (fs.existsSync(indexPath)) {
// 如果存在index.mjs,则指向index.mjs
const indexRelativePath = path.relative(currentFileDir, indexPath);
return './' + indexRelativePath.replace(/\\/g, '/');
}
// 检查是否存在直接的.mjs文件
const directPath = absolutePath + '.mjs';
if (fs.existsSync(directPath)) {
return importPath + '.mjs';
}
// 默认添加.mjs扩展名
return importPath + '.mjs';
}
/**
* 后处理单个文件的import语句
* @param {string} filePath 文件路径
* @param {string} functionOutputPath 函数输出目录
*/
function postProcessImports(filePath, functionOutputPath) {
try {
let content = fs.readFileSync(filePath, 'utf8');
const currentFileDir = path.dirname(filePath);
// 1. 将 .js 扩展名替换为 .mjs(针对相对路径)
content = content.replace(
/import\s+([^'"]*)\s+from\s+['"](\.[^'"]*?)\.js['"]/g,
'import $1 from "$2.mjs"'
);
content = content.replace(
/export\s+([^'"]*)\s+from\s+['"](\.[^'"]*?)\.js['"]/g,
'export $1 from "$2.mjs"'
);
// 2. 智能处理没有扩展名的相对路径
content = content.replace(
/import\s+([^'"]*)\s+from\s+['"](\.[^'"]*?)(?<!\.mjs)(?<!\.ts)(?<!\.tsx)(?<!\.json)['"]/g,
(_, imports, importPath) => {
const resolvedPath = resolveImportPath(importPath, currentFileDir, functionOutputPath);
return `import ${imports} from "${resolvedPath}"`;
}
);
content = content.replace(
/export\s+([^'"]*)\s+from\s+['"](\.[^'"]*?)(?<!\.mjs)(?<!\.ts)(?<!\.tsx)(?<!\.json)['"]/g,
(_, exports, importPath) => {
const resolvedPath = resolveImportPath(importPath, currentFileDir, functionOutputPath);
return `export ${exports} from "${resolvedPath}"`;
}
);
fs.writeFileSync(filePath, content, 'utf8');
console.log(` ✓ Post-processed imports in ${filePath}`);
} catch (error) {
console.warn(` ⚠ Warning: Could not post-process imports in ${filePath}:`, error.message);
}
}
/**
* 查找目录中的所有TypeScript文件
* @param {string} dir 目录路径
* @param {string[]} files 文件列表
* @returns {string[]} TypeScript文件路径列表
*/
function findTsFilesInDir(dir, files = []) {
const items = fs.readdirSync(dir);
for (const item of items) {
const fullPath = path.join(dir, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory() && item !== 'node_modules' && item !== 'dist') {
findTsFilesInDir(fullPath, files);
} else if (stat.isFile() && (item.endsWith('.ts') || item.endsWith('.tsx'))) {
files.push(fullPath);
}
}
return files;
}
async function buildFunctionDir(functionDir) {
// 查找该函数目录中的所有TypeScript文件
const tsFiles = findTsFilesInDir(functionDir.path);
if (tsFiles.length === 0) {
console.log(`No TypeScript files found in ${functionDir.name}`);
return;
}
console.log(`Building function directory: ${functionDir.name} (${tsFiles.length} files)`);
// 并行编译该目录中的所有TypeScript文件
const buildPromises = tsFiles.map(tsFile => buildSingleFile(tsFile, functionDir));
try {
await Promise.all(buildPromises);
console.log(`✓ Built function directory: ${functionDir.name}`);
// 后处理:添加 .mjs 扩展名到相对路径的 import 语句
postProcessImportsInDir(functionDir.outputPath, functionDir.outputPath);
} catch (error) {
console.error(`✗ Failed to build function directory ${functionDir.name}:`, error.message);
throw error;
}
}
async function buildSingleFile(tsFile, functionDir) {
return new Promise((resolve, reject) => {
// 计算相对于函数目录的路径
const relativePath = path.relative(functionDir.path, tsFile);
const outputPath = path.join(functionDir.outputPath, relativePath.replace(/\.ts$/, '.mjs'));
const outputDir = path.dirname(outputPath);
// 确保输出目录存在
ensureDir(outputDir);
// 构建bun命令参数
const buildArgs = [
'build',
tsFile,
'--outfile',
outputPath,
'--target',
'node',
'--format',
'esm',
'--no-bundle' // 关键选项:只转译,不打包
];
// 使用bun build进行转译
const buildProcess = spawn('bun', buildArgs, {
stdio: 'pipe'
});
let stdout = '';
let stderr = '';
buildProcess.stdout.on('data', (data) => {
stdout += data.toString();
});
buildProcess.stderr.on('data', (data) => {
stderr += data.toString();
});
buildProcess.on('close', (code) => {
if (code === 0) {
console.log(` ✓ ${relativePath} -> ${path.relative(functionDir.outputPath, outputPath)}`);
resolve();
} else {
console.error(` ✗ Failed to build ${relativePath}:`);
if (stderr) console.error(stderr);
if (stdout) console.log(stdout);
reject(new Error(`Build failed for ${relativePath}`));
}
});
});
}
async function main() {
console.log('Building TypeScript files using directory-level compilation...');
// 确保目标目录存在
ensureDir(targetDir);
// 查找所有包含TypeScript文件的函数目录
const functionDirs = findFunctionDirs(sourceDir);
if (functionDirs.length === 0) {
console.log('No function directories with TypeScript files found.');
return;
}
console.log(`Found ${functionDirs.length} function directories:`);
functionDirs.forEach(dir => console.log(` - ${dir.name} (${dir.path})`));
// 并行编译所有函数目录
try {
await Promise.all(functionDirs.map(buildFunctionDir));
console.log('✓ All function directories built successfully!');
} catch (error) {
console.error('✗ Build failed:', error.message);
process.exit(1);
}
}
main().catch(console.error);
Copy Assets
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
/**
* 复制非TypeScript文件到dist目录
* 保持目录结构不变,排除node_modules
*/
const sourceDir = 'functions';
const targetDir = 'dist';
// 需要复制的文件扩展名
const copyExtensions = ['.json', '.yaml', '.yml', '.md', '.txt', '.js', '.mjs'];
// 需要排除的目录
const excludeDirs = ['node_modules', 'dist', '.git'];
function shouldCopyFile(filePath) {
const ext = path.extname(filePath).toLowerCase();
return copyExtensions.includes(ext);
}
function shouldSkipDir(dirName) {
return excludeDirs.includes(dirName);
}
function ensureDir(dirPath) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
function copyFile(src, dest) {
ensureDir(path.dirname(dest));
fs.copyFileSync(src, dest);
console.log(`Copied: ${src} -> ${dest}`);
}
function copyAssetsRecursive(srcDir, destDir) {
const items = fs.readdirSync(srcDir);
for (const item of items) {
const srcPath = path.join(srcDir, item);
const destPath = path.join(destDir, item);
const stat = fs.statSync(srcPath);
if (stat.isDirectory()) {
if (!shouldSkipDir(item)) {
ensureDir(destPath);
copyAssetsRecursive(srcPath, destPath);
}
} else if (stat.isFile()) {
if (shouldCopyFile(srcPath)) {
copyFile(srcPath, destPath);
}
}
}
}
// 确保目标目录存在
ensureDir(targetDir);
// 开始复制
console.log('Copying assets from functions to dist...');
copyAssetsRecursive(sourceDir, targetDir);
console.log('Asset copying completed.');
Global Types
可能有缺或不正确的地方
/**
* 全局云函数类型定义
* 这些类型在所有云函数中都可以直接使用,无需import
*/
declare global {
/**
* 云函数HTTP事件类型
* @template T body的类型,默认为string
* @template R queryStringParameters的类型,默认为Record<string, string>
* @template H headers的类型,默认为Record<string, string>
*/
interface CloudFunctionEvent<T = string, R = Record<string, string>, H = Record<string, string>> {
body: T
headers: H
httpMethod: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS" | string
isBase64Encoded: boolean
multiValueHeaders: Record<string, string[]>
path: string
queryStringParameters: R
requestContext: {
appId: string
envId: string
requestId: string
uin: string
}
}
/**
* 云函数定时任务事件类型
*/
interface CloudFunctionScheduleEvent {
TriggerName: string
}
/**
* 云函数上下文类型
*/
interface CloudFunctionContext {
requestId: string
functionName: string
functionVersion: string
memoryLimitInMB: number
timeRemaining: number
[key: string]: any
}
/**
* 通用API响应类型
* @template T 响应数据的类型
*/
interface ApiResponse<T = any> {
code: number
message?: string
data?: T
error?: string
timestamp?: string
}
/**
* 云函数主入口函数类型
* @template T 事件body的类型
* @template R 响应数据的类型
*/
type CloudFunctionMain<T = any, R = any> = (
event: CloudFunctionEvent<T>,
context: CloudFunctionContext
) => Promise<ApiResponse<R>>
/**
* 数据库查询结果类型
* @template T 数据项的类型
*/
interface DatabaseQueryResult<T = any> {
data: T[]
total?: number
limit?: number
offset?: number
}
/**
* 分页参数类型
*/
interface PaginationParams {
page?: number
pageSize?: number
limit?: number
offset?: number
}
/**
* 排序参数类型
*/
interface SortParams {
sortBy?: string
sortOrder?: 'asc' | 'desc' | 'ASC' | 'DESC'
}
/**
* 通用查询参数类型
*/
interface QueryParams extends PaginationParams, SortParams {
[key: string]: any
}
}
// 确保这个文件被视为模块
export { }
TSConfig
{
"compilerOptions": {
// Environment setup & latest features
"lib": [
"ESNext"
],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"allowJs": true,
// Bundler mode for better module resolution
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Output configuration (for reference, actual output handled by bun)
"outDir": "./dist",
"rootDir": "./functions",
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"forceConsistentCasingInFileNames": true,
// Module interop
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"isolatedModules": true,
// Development features
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": false,
"preserveConstEnums": true,
// Decorators (if needed)
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
// Type definitions
"types": [
"node",
"node-fetch"
]
},
"include": [
"functions/**/*",
"global.d.ts"
],
"exclude": [
"functions/**/node_modules",
"functions/**/dist",
"dist"
]
}