mirror of
https://github.com/motajs/template.git
synced 2026-11-01 22:24:31 +08:00
init: 2.B
This commit is contained in:
commit
6bf32174d8
610 files changed
+151444
No files matched your search
@@ -0,0 +1,682 @@
|
||||
import { build, loadConfigFromFile, mergeConfig, UserConfig } from 'vite';
|
||||
import legacy from '@vitejs/plugin-legacy';
|
||||
import { resolve } from 'path';
|
||||
import { copy, emptyDir, ensureDir, pathExists } from 'fs-extra';
|
||||
import { OutputAsset, OutputChunk, RollupOutput } from 'rollup';
|
||||
import Fontmin from 'fontmin';
|
||||
import { readdir, readFile, rmdir, stat, writeFile } from 'fs/promises';
|
||||
import { transformAsync } from '@babel/core';
|
||||
import archiver from 'archiver';
|
||||
import { createWriteStream } from 'fs';
|
||||
import { zip } from 'compressing';
|
||||
import { RequiredData, RequiredIconsData, ResourceType } from './types';
|
||||
import { splitResource, SplittedResource } from './build-resource';
|
||||
import { formatSize } from './utils';
|
||||
|
||||
const DEBUG_REPLAY = false;
|
||||
|
||||
const ansi = {
|
||||
clear: '\x1b[2J\x1b[0f'
|
||||
};
|
||||
|
||||
// 资源分离步骤的单包大小,默认 2M,可以自行调整
|
||||
const RESOUCE_SIZE = 2 * 2 ** 20;
|
||||
|
||||
const distDir = resolve(process.cwd(), 'dist');
|
||||
const tempDir = resolve(process.cwd(), '_temp');
|
||||
|
||||
/**
|
||||
* 构建游戏代码
|
||||
* @param entry 入口文件
|
||||
*/
|
||||
async function buildClient(outDir: string) {
|
||||
const configFile = resolve(process.cwd(), 'vite.config.ts');
|
||||
const config = await loadConfigFromFile(
|
||||
{ command: 'build', mode: 'production' },
|
||||
configFile
|
||||
);
|
||||
const resolved = mergeConfig(config?.config ?? {}, {
|
||||
plugins: [
|
||||
legacy({
|
||||
targets: [
|
||||
'Chrome >= 56',
|
||||
'Firefox >= 51',
|
||||
'Edge >= 79',
|
||||
'Safari >= 15',
|
||||
'Opera >= 43'
|
||||
],
|
||||
polyfills: true,
|
||||
renderModernChunks: false
|
||||
})
|
||||
],
|
||||
build: {
|
||||
outDir,
|
||||
copyPublicDir: true,
|
||||
rollupOptions: {
|
||||
external: ['@wasm-audio-decoders/opus-ml'],
|
||||
output: {
|
||||
format: 'es',
|
||||
entryFileNames: '[name].[hash].js',
|
||||
chunkFileNames: 'chunks/[name].[hash].js',
|
||||
assetFileNames: 'assets/[name].[hash][extname]',
|
||||
manualChunks: {
|
||||
antdv: ['ant-design-vue', '@ant-design/icons-vue'],
|
||||
audio: [
|
||||
'codec-parser',
|
||||
'opus-decoder',
|
||||
'ogg-opus-decoder',
|
||||
'@wasm-audio-decoders/ogg-vorbis'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} satisfies UserConfig);
|
||||
|
||||
return build({
|
||||
...resolved,
|
||||
configFile: false
|
||||
});
|
||||
}
|
||||
|
||||
async function buildData(outDir: string, entry: string) {
|
||||
const configFile = resolve(process.cwd(), 'vite.config.ts');
|
||||
const config = await loadConfigFromFile(
|
||||
{ command: 'build', mode: 'production' },
|
||||
configFile
|
||||
);
|
||||
const resolved = mergeConfig(config?.config ?? {}, {
|
||||
build: {
|
||||
outDir,
|
||||
copyPublicDir: false,
|
||||
lib: {
|
||||
entry,
|
||||
name: 'ProcessData',
|
||||
fileName: 'data',
|
||||
formats: ['iife']
|
||||
},
|
||||
minify: !DEBUG_REPLAY
|
||||
}
|
||||
} satisfies UserConfig);
|
||||
|
||||
return build({
|
||||
...resolved,
|
||||
configFile: false
|
||||
});
|
||||
}
|
||||
|
||||
const enum ProgressStatus {
|
||||
Success,
|
||||
Fail,
|
||||
Working,
|
||||
Warn
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出构建步骤
|
||||
* @param step 步骤数
|
||||
* @param final 最后一步的状态
|
||||
*/
|
||||
function logProgress(step: number, final: ProgressStatus) {
|
||||
const list = [
|
||||
`1. 构建前准备`,
|
||||
`2. 构建客户端代码`,
|
||||
`3. 构建数据端代码`,
|
||||
`4. 压缩 main.js`,
|
||||
`5. 压缩字体`,
|
||||
`6. 资源分块`,
|
||||
`7. 最后处理`,
|
||||
`8. 压缩为 zip`
|
||||
];
|
||||
|
||||
const str = list.reduce((prev, curr, idx) => {
|
||||
if (idx > step) {
|
||||
return prev;
|
||||
} else if (idx === step) {
|
||||
switch (final) {
|
||||
case ProgressStatus.Fail:
|
||||
return prev + `❌ ${curr}\r\n错误信息:\r\n`;
|
||||
case ProgressStatus.Success:
|
||||
return prev + `✅ ${curr}\r\n`;
|
||||
case ProgressStatus.Working:
|
||||
return prev + `🔄 ${curr}\r\n`;
|
||||
case ProgressStatus.Warn:
|
||||
return prev + `⚠️ ${curr}\r\n`;
|
||||
}
|
||||
} else {
|
||||
return prev + `✅ ${curr}\r\n`;
|
||||
}
|
||||
}, '');
|
||||
|
||||
process.stdout.write(ansi.clear);
|
||||
process.stdout.write(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取规范化文件名
|
||||
* @param output Rollup 输出
|
||||
* @param client 是否是客户端代码
|
||||
*/
|
||||
function getFileName(output: OutputChunk | OutputAsset, client: boolean) {
|
||||
const name = output.fileName;
|
||||
if (name.startsWith('index-legacy') && client) {
|
||||
return 'main';
|
||||
}
|
||||
if (name.startsWith('data') && !client) {
|
||||
return 'main';
|
||||
}
|
||||
if (name.startsWith('index.html') && client) {
|
||||
return 'index';
|
||||
}
|
||||
const index = name.indexOf('-legacy');
|
||||
const unhash = name.slice(0, index);
|
||||
return unhash;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件大小
|
||||
* @param output Rollup 输出
|
||||
*/
|
||||
function getFileSize(output: OutputChunk | OutputAsset) {
|
||||
if (output.type === 'asset') {
|
||||
if (typeof output.source === 'string') {
|
||||
return Buffer.byteLength(output.source);
|
||||
} else {
|
||||
return output.source.byteLength;
|
||||
}
|
||||
} else {
|
||||
return Buffer.byteLength(output.code);
|
||||
}
|
||||
}
|
||||
|
||||
const enum ClientDataLevel {
|
||||
Error,
|
||||
Suspect,
|
||||
Pass
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查客户端与数据端文件大小,并给出数据端引用客户端的可能性
|
||||
* @param client 客户端文件大小
|
||||
* @param data 数据端文件大小
|
||||
*/
|
||||
function checkClientData(
|
||||
client: Map<string, number>,
|
||||
data: Map<string, number>
|
||||
) {
|
||||
let error = false;
|
||||
let warn = false;
|
||||
|
||||
const clientMain = client.get('main');
|
||||
const dataMain = data.get('main');
|
||||
|
||||
if (clientMain && dataMain) {
|
||||
if (clientMain <= dataMain) {
|
||||
error = true;
|
||||
} else if (clientMain / 2 < dataMain) {
|
||||
warn = true;
|
||||
}
|
||||
}
|
||||
|
||||
let clientTotal = 0;
|
||||
let dataTotal = 0;
|
||||
|
||||
client.forEach(v => {
|
||||
clientTotal += v;
|
||||
});
|
||||
|
||||
data.forEach(v => {
|
||||
dataTotal += v;
|
||||
});
|
||||
|
||||
if (clientTotal <= dataTotal) {
|
||||
error = true;
|
||||
}
|
||||
|
||||
if (clientTotal / 4 < dataTotal) {
|
||||
warn = true;
|
||||
}
|
||||
|
||||
if (error) return ClientDataLevel.Error;
|
||||
else if (warn) return ClientDataLevel.Suspect;
|
||||
else return ClientDataLevel.Pass;
|
||||
}
|
||||
|
||||
async function getAllChars(client: RollupOutput[]) {
|
||||
const chars = new Set<string>();
|
||||
|
||||
// 1. 客户端构建结果
|
||||
client.forEach(v => {
|
||||
v.output.forEach(v => {
|
||||
if (v.type === 'chunk' && v.fileName.startsWith('index')) {
|
||||
const set = new Set(v.code);
|
||||
set.forEach(v => chars.add(v));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 2. 样板内容
|
||||
const files: string[] = [];
|
||||
|
||||
files.push(resolve(tempDir, 'client/main.js'));
|
||||
files.push(resolve(tempDir, 'client/project/data.js'));
|
||||
files.push(resolve(tempDir, 'client/project/enemys.js'));
|
||||
files.push(resolve(tempDir, 'client/project/events.js'));
|
||||
files.push(resolve(tempDir, 'client/project/functions.js'));
|
||||
files.push(resolve(tempDir, 'client/project/icons.js'));
|
||||
files.push(resolve(tempDir, 'client/project/items.js'));
|
||||
files.push(resolve(tempDir, 'client/project/maps.js'));
|
||||
|
||||
const floors = await readdir(resolve(tempDir, 'client/project/floors'));
|
||||
const ids = floors.map(v => resolve(tempDir, 'client/project/floors', v));
|
||||
files.push(...ids);
|
||||
|
||||
const libs = await readdir(resolve(tempDir, 'client/libs'));
|
||||
files.push(...libs.map(v => resolve(tempDir, 'client/libs', v)));
|
||||
|
||||
await Promise.all(
|
||||
files.map(async v => {
|
||||
const stats = await stat(v);
|
||||
if (!stats.isFile()) return;
|
||||
const file = await readFile(v, 'utf-8');
|
||||
const set = new Set(file);
|
||||
set.forEach(v => chars.add(v));
|
||||
})
|
||||
);
|
||||
|
||||
return chars;
|
||||
}
|
||||
|
||||
interface CompressedLoadListItem {
|
||||
type: ResourceType;
|
||||
name: string;
|
||||
usage: string;
|
||||
}
|
||||
|
||||
type CompressedLoadList = Record<string, CompressedLoadListItem[]>;
|
||||
|
||||
/**
|
||||
* 生成资源地图 json 文件
|
||||
*/
|
||||
function generateResourceJSON(resources: SplittedResource[]) {
|
||||
const list: CompressedLoadList = {};
|
||||
|
||||
resources.forEach(file => {
|
||||
const uri = `project/resource/${file.fileName}`;
|
||||
file.content.forEach(content => {
|
||||
const item: CompressedLoadListItem = {
|
||||
type: content.type,
|
||||
name: content.name,
|
||||
usage: content.usage
|
||||
};
|
||||
list[uri] ??= [];
|
||||
list[uri].push(item);
|
||||
});
|
||||
});
|
||||
|
||||
return JSON.stringify(list);
|
||||
}
|
||||
|
||||
async function buildGame() {
|
||||
logProgress(0, ProgressStatus.Working);
|
||||
|
||||
//#region 准备步骤
|
||||
try {
|
||||
await ensureDir(distDir);
|
||||
await ensureDir(tempDir);
|
||||
await emptyDir(distDir);
|
||||
await emptyDir(tempDir);
|
||||
await ensureDir(resolve(tempDir, 'fonts'));
|
||||
await ensureDir(resolve(tempDir, 'common'));
|
||||
await ensureDir(resolve(tempDir, 'resource'));
|
||||
} catch (e) {
|
||||
logProgress(0, ProgressStatus.Fail);
|
||||
process.stderr.write(String(e));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
logProgress(1, ProgressStatus.Working);
|
||||
|
||||
//#region 构建客户端
|
||||
const clientPack = await buildClient(resolve(tempDir, 'client')).catch(
|
||||
reason => {
|
||||
logProgress(1, ProgressStatus.Fail);
|
||||
process.stderr.write(String(reason));
|
||||
process.exit(1);
|
||||
}
|
||||
);
|
||||
|
||||
logProgress(2, ProgressStatus.Working);
|
||||
|
||||
//#region 构建数据端
|
||||
const dataPack = await buildData(
|
||||
resolve(tempDir, 'data'),
|
||||
resolve(process.cwd(), 'src/data.ts')
|
||||
).catch(reason => {
|
||||
logProgress(2, ProgressStatus.Fail);
|
||||
process.stderr.write(String(reason));
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
const clientSize = new Map<string, number>();
|
||||
const dataSize = new Map<string, number>();
|
||||
|
||||
const clientPackArr = [];
|
||||
const dataPackArr = [];
|
||||
|
||||
// 判断客户端与数据端的构建包大小,从而推断是否出现了数据端引用客户端的问题
|
||||
|
||||
if (clientPack instanceof Array) {
|
||||
clientPackArr.push(...clientPack);
|
||||
} else if ('close' in clientPack) {
|
||||
// pass.
|
||||
} else {
|
||||
clientPackArr.push(clientPack);
|
||||
}
|
||||
if (dataPack instanceof Array) {
|
||||
dataPackArr.push(...dataPack);
|
||||
} else if ('close' in dataPack) {
|
||||
// pass.
|
||||
} else {
|
||||
dataPackArr.push(dataPack);
|
||||
}
|
||||
|
||||
// 获取每个 chunk 的大小
|
||||
clientPackArr.forEach(v => {
|
||||
v.output.forEach(v => {
|
||||
const name = getFileName(v, true);
|
||||
const size = getFileSize(v);
|
||||
clientSize.set(name, size);
|
||||
});
|
||||
});
|
||||
dataPackArr.forEach(v => {
|
||||
v.output.forEach(v => {
|
||||
const name = getFileName(v, false);
|
||||
const size = getFileSize(v);
|
||||
dataSize.set(name, size);
|
||||
});
|
||||
});
|
||||
|
||||
const level = checkClientData(clientSize, dataSize);
|
||||
|
||||
if (level === ClientDataLevel.Error) {
|
||||
logProgress(2, ProgressStatus.Fail);
|
||||
process.stderr.write(
|
||||
`数据端似乎引用了客户端内容,请仔细检查后再构建!`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 解析全塔属性
|
||||
const dataFile = await readFile(
|
||||
resolve(process.cwd(), 'public/project/data.js'),
|
||||
'utf-8'
|
||||
);
|
||||
const dataObject: RequiredData = JSON.parse(
|
||||
dataFile.split('\n').slice(1).join('\n')
|
||||
);
|
||||
const mainData = dataObject.main;
|
||||
|
||||
logProgress(3, ProgressStatus.Working);
|
||||
|
||||
//#region 压缩 main
|
||||
try {
|
||||
const main = await readFile(
|
||||
resolve(tempDir, 'client/main.js'),
|
||||
'utf-8'
|
||||
);
|
||||
const [head, tail] = main.split('// >>>> body end');
|
||||
const transformed = await transformAsync(tail, {
|
||||
presets: [['@babel/preset-env']],
|
||||
sourceType: 'script',
|
||||
minified: true,
|
||||
comments: false
|
||||
});
|
||||
if (!transformed || !transformed.code) {
|
||||
throw new ReferenceError(
|
||||
`Cannot write main.js since transform result is empty.`
|
||||
);
|
||||
}
|
||||
const code = transformed.code;
|
||||
const modifiedHead = head.replace(
|
||||
'this.scriptCompress = false;',
|
||||
'this.scriptCompress = true;'
|
||||
);
|
||||
await writeFile(
|
||||
resolve(tempDir, 'common/main.js'),
|
||||
modifiedHead + '\n// >>>> body end\n' + code,
|
||||
'utf-8'
|
||||
);
|
||||
} catch (e) {
|
||||
logProgress(3, ProgressStatus.Fail);
|
||||
process.stderr.write(String(e));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
//#region 压缩字体
|
||||
const chars = await getAllChars(clientPackArr).catch(reason => {
|
||||
logProgress(4, ProgressStatus.Fail);
|
||||
process.stderr.write(String(reason));
|
||||
process.exit(1);
|
||||
});
|
||||
const { fonts } = mainData;
|
||||
|
||||
await Promise.all(
|
||||
fonts.map(v => {
|
||||
const fontmin = new Fontmin();
|
||||
const src = resolve(tempDir, 'client/project/fonts', `${v}.ttf`);
|
||||
const dest = resolve(tempDir, 'fonts');
|
||||
const plugin = Fontmin.glyph({
|
||||
text: [...chars].join('')
|
||||
});
|
||||
fontmin.src(src).dest(dest).use(plugin);
|
||||
return fontmin.runAsync();
|
||||
})
|
||||
).catch(reason => {
|
||||
logProgress(4, ProgressStatus.Fail);
|
||||
process.stderr.write(String(reason));
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
logProgress(5, ProgressStatus.Working);
|
||||
|
||||
//#region 资源分块
|
||||
const iconsFile = await readFile(
|
||||
resolve(process.cwd(), 'public/project/icons.js'),
|
||||
'utf-8'
|
||||
);
|
||||
const iconsObject: RequiredIconsData = JSON.parse(
|
||||
iconsFile.split('\n').slice(1).join('\n')
|
||||
);
|
||||
const resources = await splitResource(
|
||||
dataObject,
|
||||
iconsObject,
|
||||
resolve(tempDir, 'client'),
|
||||
resolve(tempDir, 'fonts'),
|
||||
RESOUCE_SIZE
|
||||
).catch(reason => {
|
||||
logProgress(5, ProgressStatus.Fail);
|
||||
process.stderr.write(String(reason));
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
resources.map(v =>
|
||||
writeFile(resolve(tempDir, 'resource', v.fileName), v.buffer)
|
||||
)
|
||||
).catch(reason => {
|
||||
logProgress(5, ProgressStatus.Fail);
|
||||
process.stderr.write(String(reason));
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
logProgress(6, ProgressStatus.Working);
|
||||
|
||||
//#region 最后处理
|
||||
|
||||
const toCopy = [
|
||||
'libs',
|
||||
'_server',
|
||||
'extensions',
|
||||
'index.html',
|
||||
'editor.html',
|
||||
'styles.css',
|
||||
'logo.png',
|
||||
'project/floors',
|
||||
'project/data.js',
|
||||
'project/enemys.js',
|
||||
'project/events.js',
|
||||
'project/functions.js',
|
||||
'project/icons.js',
|
||||
'project/items.js',
|
||||
'project/maps.js',
|
||||
'project/plugins.js',
|
||||
'project/materials/airwall.png'
|
||||
];
|
||||
|
||||
clientPackArr.forEach(v => {
|
||||
v.output.forEach(v => {
|
||||
toCopy.push(v.fileName);
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
toCopy.map(v =>
|
||||
copy(resolve(tempDir, 'client', v), resolve(distDir, v))
|
||||
)
|
||||
);
|
||||
await copy(
|
||||
resolve(tempDir, 'resource'),
|
||||
resolve(distDir, 'project/resource')
|
||||
);
|
||||
await copy(
|
||||
resolve(tempDir, 'common/main.js'),
|
||||
resolve(distDir, 'main.js')
|
||||
);
|
||||
await copy(
|
||||
resolve(tempDir, 'data/data.iife.js'),
|
||||
resolve(distDir, 'data.process.js')
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
dataObject.main.bgms.map(v =>
|
||||
copy(
|
||||
resolve(tempDir, 'client/project/bgms', v),
|
||||
resolve(distDir, 'project/bgms', v)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const scripts = archiver('zip');
|
||||
scripts.directory('packages/', resolve(process.cwd(), 'packages'));
|
||||
scripts.directory(
|
||||
'packages-user/',
|
||||
resolve(process.cwd(), 'packages-user')
|
||||
);
|
||||
scripts.directory('src/', resolve(process.cwd(), 'src'));
|
||||
|
||||
const output = createWriteStream(resolve(distDir, 'source-code.zip'));
|
||||
scripts.pipe(output);
|
||||
|
||||
output.on('error', err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
await new Promise<void>(res => {
|
||||
output.on('finish', () => res());
|
||||
scripts.finalize();
|
||||
});
|
||||
|
||||
const json = generateResourceJSON(resources);
|
||||
await writeFile(resolve(distDir, 'loadList.json'), json, 'utf-8');
|
||||
|
||||
await copy(
|
||||
resolve(process.cwd(), 'LICENSE'),
|
||||
resolve(distDir, 'LICENSE')
|
||||
);
|
||||
|
||||
await copy(
|
||||
resolve(process.cwd(), 'script/template/启动服务.exe'),
|
||||
resolve(distDir, '启动服务.exe')
|
||||
);
|
||||
|
||||
const bgPath = 'project/images/bg.jpg';
|
||||
|
||||
if (await pathExists(resolve(tempDir, 'client', bgPath))) {
|
||||
await copy(
|
||||
resolve(tempDir, 'client', bgPath),
|
||||
resolve(distDir, bgPath)
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
logProgress(6, ProgressStatus.Fail);
|
||||
process.stderr.write(String(e));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
logProgress(7, ProgressStatus.Working);
|
||||
|
||||
//#region 压缩游戏
|
||||
|
||||
try {
|
||||
await zip.compressDir(
|
||||
resolve(distDir),
|
||||
resolve(process.cwd(), 'dist.zip')
|
||||
);
|
||||
|
||||
await emptyDir(tempDir);
|
||||
await rmdir(tempDir);
|
||||
} catch (e) {
|
||||
logProgress(7, ProgressStatus.Fail);
|
||||
process.stderr.write(String(e));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
//#region 输出构建信息
|
||||
|
||||
const sourceStats = await stat(resolve(distDir, 'source-code.zip'));
|
||||
const sourceSize = sourceStats.size;
|
||||
const zipStats = await stat(resolve(process.cwd(), 'dist.zip'));
|
||||
const zipSize = zipStats.size;
|
||||
const resourceSize = resources.reduce(
|
||||
(prev, curr) => prev + curr.byteLength,
|
||||
0
|
||||
);
|
||||
|
||||
process.stdout.write(ansi.clear);
|
||||
process.stdout.write(`✅ 构建已完成!\r\n`);
|
||||
if (zipSize > 100 * 2 ** 20) {
|
||||
process.stdout.write(
|
||||
`⚠️ 压缩包大于 100M,可能导致发塔困难,请考虑降低塔的大小\r\n`
|
||||
);
|
||||
const suggections: string[] = [];
|
||||
if (dataObject.main.bgms.some(v => !v.endsWith('opus'))) {
|
||||
suggections.push(`将 BGM 和音效换用 opus 格式`);
|
||||
}
|
||||
if (dataObject.main.images.some(v => !v.endsWith('webp'))) {
|
||||
suggections.push(`将图片换用无损 webp 格式`);
|
||||
}
|
||||
if (suggections.length > 0) {
|
||||
process.stdout.write(`降低压缩包体积的可能方案:\r\n`);
|
||||
suggections.forEach((v, i) => {
|
||||
process.stdout.write(`${i + 1}. ${v}\r\n`);
|
||||
});
|
||||
}
|
||||
}
|
||||
process.stdout.write(`压缩包大小:${formatSize(zipSize)}\r\n`);
|
||||
process.stdout.write(`源码大小:${formatSize(sourceSize)}\r\n`);
|
||||
process.stdout.write(`资源大小:${formatSize(resourceSize)}\r\n`);
|
||||
resources.forEach(v => {
|
||||
process.stdout.write(
|
||||
`--> ${v.fileName} ${formatSize(v.byteLength)} | ${v.content.length} 个资源\r\n`
|
||||
);
|
||||
});
|
||||
process.stdout.write(`游戏已经输出至 dist 文件夹\r\n`);
|
||||
process.stdout.write(`游戏已压缩至 dist.zip 压缩文件\r\n`);
|
||||
}
|
||||
|
||||
// Execute
|
||||
(() => {
|
||||
buildGame();
|
||||
})();
|
||||
@@ -0,0 +1,60 @@
|
||||
import { build, loadConfigFromFile, mergeConfig, UserConfig } from 'vite';
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import dts from 'vite-plugin-dts';
|
||||
|
||||
const packages = path.resolve(process.cwd(), './packages');
|
||||
|
||||
// 获取所有包目录
|
||||
const packageDirs = ['render'];
|
||||
|
||||
// 构建每一个包
|
||||
async function buildPackages() {
|
||||
for (const packageName of packageDirs) {
|
||||
const packageDir = path.join(packages, packageName);
|
||||
const output = path.join(packageDir, 'dist');
|
||||
await fs.emptyDir(output);
|
||||
const configFile = path.resolve('./vite.config.ts');
|
||||
const config = await loadConfigFromFile(
|
||||
{ command: 'build', mode: 'production' },
|
||||
configFile
|
||||
);
|
||||
const resolved = mergeConfig(config?.config ?? {}, {
|
||||
plugins: [
|
||||
dts({
|
||||
entryRoot: path.join(packageDir, 'src/index.ts'),
|
||||
rollupTypes: true,
|
||||
strictOutput: false,
|
||||
copyDtsFiles: false,
|
||||
exclude: ['packages-user/**/*']
|
||||
})
|
||||
],
|
||||
build: {
|
||||
lib: {
|
||||
entry: path.join(packageDir, 'src/index.ts'),
|
||||
name: packageName,
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: format => `${packageName}.${format}.js`
|
||||
},
|
||||
outDir: output,
|
||||
sourcemap: true,
|
||||
emptyOutDir: true
|
||||
},
|
||||
publicDir: false
|
||||
} satisfies UserConfig);
|
||||
|
||||
await build(resolved);
|
||||
console.log(`✅ Package ${packageName} built successfully.`);
|
||||
}
|
||||
const dirs = await fs.readdir(packages);
|
||||
for (const name of dirs) {
|
||||
const dir = path.join(process.cwd(), name);
|
||||
await fs.emptyDir(dir);
|
||||
await fs.rmdir(dir);
|
||||
}
|
||||
}
|
||||
|
||||
buildPackages().catch(e => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { build, loadConfigFromFile, mergeConfig, UserConfig } from 'vite';
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
const packagesDir = path.resolve('./packages');
|
||||
const outputDir = path.resolve('./dist/packages');
|
||||
|
||||
// 清空 dist 目录
|
||||
fs.emptyDirSync(outputDir);
|
||||
|
||||
// 获取所有包目录
|
||||
const packageDirs = fs.readdirSync(packagesDir).filter(name => {
|
||||
const packagePath = path.join(packagesDir, name);
|
||||
return fs.statSync(packagePath).isDirectory();
|
||||
});
|
||||
|
||||
// 构建每一个包
|
||||
async function buildPackages() {
|
||||
for (const packageName of packageDirs) {
|
||||
const packageDir = path.join(packagesDir, packageName);
|
||||
const configFile = path.resolve('./vite.config.ts');
|
||||
const config = await loadConfigFromFile(
|
||||
{ command: 'build', mode: 'production' },
|
||||
configFile
|
||||
);
|
||||
const resolved = mergeConfig(config?.config ?? {}, {
|
||||
build: {
|
||||
lib: {
|
||||
entry: path.join(packageDir, 'src/index.ts'),
|
||||
name: packageName,
|
||||
formats: ['es'],
|
||||
fileName: format => `${packageName}.${format}.js`
|
||||
},
|
||||
outDir: path.join(outputDir, packageName),
|
||||
sourcemap: true,
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
external: [/node_modules/, /^@motajs\/.*/]
|
||||
}
|
||||
},
|
||||
publicDir: false
|
||||
} satisfies UserConfig);
|
||||
|
||||
await build(resolved);
|
||||
console.log(`✅ Package ${packageName} built successfully.`);
|
||||
}
|
||||
}
|
||||
|
||||
buildPackages().catch(e => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,188 @@
|
||||
import JSZip from 'jszip';
|
||||
import {
|
||||
RequiredData,
|
||||
RequiredIconsData,
|
||||
ResourceType,
|
||||
ResourceUsage
|
||||
} from './types';
|
||||
import { Stats } from 'fs';
|
||||
import { readdir, readFile, stat } from 'fs/promises';
|
||||
import { resolve } from 'path';
|
||||
import { fileHash } from './utils';
|
||||
|
||||
export interface ResourceInfo {
|
||||
name: string;
|
||||
type: ResourceType;
|
||||
usage: ResourceUsage;
|
||||
stats: Stats;
|
||||
}
|
||||
|
||||
export interface SplittedResource {
|
||||
readonly byteLength: number;
|
||||
readonly resource: JSZip;
|
||||
readonly buffer: Uint8Array;
|
||||
readonly fileName: string;
|
||||
readonly content: Readonly<ResourceInfo>[];
|
||||
}
|
||||
|
||||
interface ResourceContent extends ResourceInfo {
|
||||
content: string | Buffer | Uint8Array;
|
||||
exceed: boolean;
|
||||
}
|
||||
|
||||
interface ResourcePath {
|
||||
name: string;
|
||||
path: string;
|
||||
usage: ResourceUsage;
|
||||
}
|
||||
|
||||
function getTypeByUsage(usage: ResourceUsage): ResourceType {
|
||||
switch (usage) {
|
||||
case 'animate':
|
||||
return 'text';
|
||||
case 'autotile':
|
||||
case 'image':
|
||||
case 'tileset':
|
||||
return 'image';
|
||||
case 'sound':
|
||||
return 'byte';
|
||||
case 'font':
|
||||
return 'buffer';
|
||||
case 'material':
|
||||
return 'material';
|
||||
}
|
||||
}
|
||||
|
||||
function readFileOfType(path: string, type: ResourceType) {
|
||||
if (type === 'text') {
|
||||
return readFile(path, 'utf-8');
|
||||
} else {
|
||||
return readFile(path);
|
||||
}
|
||||
}
|
||||
|
||||
async function compressFiles(files: ResourceContent[]) {
|
||||
const zip = new JSZip();
|
||||
files.forEach(v => {
|
||||
const dir = `${v.type}/${v.name}`;
|
||||
zip.file(dir, v.content);
|
||||
});
|
||||
const buffer = await zip.generateAsync({ type: 'uint8array' });
|
||||
|
||||
const hash = fileHash(buffer);
|
||||
const name = `resource.${hash}.h5data`;
|
||||
|
||||
const resource: SplittedResource = {
|
||||
byteLength: buffer.byteLength,
|
||||
buffer: buffer,
|
||||
resource: zip,
|
||||
fileName: name,
|
||||
content: files
|
||||
};
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
export async function splitResource(
|
||||
data: RequiredData,
|
||||
icons: RequiredIconsData,
|
||||
base: string,
|
||||
fontsDir: string,
|
||||
limit: number
|
||||
) {
|
||||
const result: SplittedResource[] = [];
|
||||
|
||||
// 获取所有需要分块的资源
|
||||
const { animates, fonts, images, sounds, tilesets } = data.main;
|
||||
const autotiles = Object.keys(icons.autotile);
|
||||
const materials = await readdir(resolve(base, 'project/materials'));
|
||||
|
||||
const paths: ResourcePath[] = [
|
||||
...animates.map<ResourcePath>(v => ({
|
||||
name: `${v}.animate`,
|
||||
path: resolve(base, 'project/animates', `${v}.animate`),
|
||||
usage: 'animate'
|
||||
})),
|
||||
...fonts.map<ResourcePath>(v => ({
|
||||
name: `${v}.ttf`,
|
||||
path: resolve(fontsDir, `${v}.ttf`),
|
||||
usage: 'font'
|
||||
})),
|
||||
...images.map<ResourcePath>(v => ({
|
||||
name: v,
|
||||
path: resolve(base, 'project/images', v),
|
||||
usage: 'image'
|
||||
})),
|
||||
...sounds.map<ResourcePath>(v => ({
|
||||
name: v,
|
||||
path: resolve(base, 'project/sounds', v),
|
||||
usage: 'sound'
|
||||
})),
|
||||
...tilesets.map<ResourcePath>(v => ({
|
||||
name: v,
|
||||
path: resolve(base, 'project/tilesets', v),
|
||||
usage: 'tileset'
|
||||
})),
|
||||
...autotiles.map<ResourcePath>(v => ({
|
||||
name: `${v}.png`,
|
||||
path: resolve(base, 'project/autotiles', `${v}.png`),
|
||||
usage: 'autotile'
|
||||
})),
|
||||
...materials.map<ResourcePath>(v => ({
|
||||
name: v,
|
||||
path: resolve(base, 'project/materials', v),
|
||||
usage: 'material'
|
||||
}))
|
||||
];
|
||||
|
||||
const files = await Promise.all(
|
||||
paths.map(async ({ path, usage, name }) => {
|
||||
const stats = await stat(path);
|
||||
if (!stats.isFile()) {
|
||||
return Promise.reject(
|
||||
new ReferenceError(
|
||||
`Expected resource is a file, but get directory.`
|
||||
)
|
||||
);
|
||||
}
|
||||
const type = getTypeByUsage(usage);
|
||||
const content = await readFileOfType(path, type);
|
||||
const info: ResourceContent = {
|
||||
type,
|
||||
name,
|
||||
usage,
|
||||
stats,
|
||||
content,
|
||||
exceed: stats.size > limit
|
||||
};
|
||||
return info;
|
||||
})
|
||||
);
|
||||
|
||||
// 从小到大排序,这样的话可以尽量减小资源分块文件数量
|
||||
files.sort((a, b) => a.stats.size - b.stats.size);
|
||||
|
||||
let index = 0;
|
||||
while (index < files.length) {
|
||||
let total = 0;
|
||||
const start = index;
|
||||
let i = index;
|
||||
for (; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
if (file.exceed) {
|
||||
if (i === index) i = index + 1;
|
||||
break;
|
||||
} else {
|
||||
total += file.stats.size;
|
||||
}
|
||||
if (total > limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
index = i;
|
||||
const toZip = files.slice(start, index);
|
||||
result.push(await compressFiles(toZip));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import fs from 'fs/promises';
|
||||
|
||||
(async function () {
|
||||
// 1. 声明道具
|
||||
const item = await fs.readFile('./public/project/items.js', 'utf-8');
|
||||
const itemData = JSON.parse(item.split('\n').slice(1).join('')) as any;
|
||||
|
||||
let itemDec = 'interface ItemDeclaration {\n';
|
||||
for (const id in itemData) {
|
||||
itemDec += ` ${id}: '${itemData[id].cls}';\n`;
|
||||
}
|
||||
itemDec += '}';
|
||||
|
||||
// 2. 声明公共事件
|
||||
const event = await fs.readFile('./public/project/events.js', 'utf-8');
|
||||
const eventData = JSON.parse(event.split('\n').slice(1).join('')) as any;
|
||||
|
||||
let eventDec = 'type EventDeclaration = \n';
|
||||
for (const id in eventData.commonEvent) {
|
||||
eventDec += ` | '${id}'\n`;
|
||||
}
|
||||
|
||||
// 3. 声明数字id映射与cls映射
|
||||
const map = await fs.readFile('./public/project/maps.js', 'utf-8');
|
||||
const mapData = JSON.parse(map.split('\n').slice(1).join('')) as any;
|
||||
|
||||
let id2num = 'interface IdToNumber {\n';
|
||||
let num2id = 'interface NumberToId {\n';
|
||||
let id2cls = 'interface IdToCls {\n';
|
||||
for (const num in mapData) {
|
||||
const { id, cls } = mapData[num];
|
||||
id2num += ` ${id}: ${num};\n`;
|
||||
num2id += ` ${num}: '${id}';\n`;
|
||||
id2cls += ` ${id}: '${cls}';\n`;
|
||||
}
|
||||
id2cls += '}';
|
||||
id2num += '}';
|
||||
num2id += '}';
|
||||
|
||||
// 4. 声明楼层id,图片,动画,音效,文件别名,背景音乐,字体
|
||||
const data = await fs.readFile('./public/project/data.js', 'utf-8');
|
||||
const d = (JSON.parse(data.split('\n').slice(1).join('')) as any).main;
|
||||
|
||||
let floorId = 'type FloorIds =\n';
|
||||
let imgs = 'type ImageIds =\n';
|
||||
let anis = 'type AnimationIds =\n';
|
||||
let sounds = 'type SoundIds =\n';
|
||||
let names = 'interface NameMap {\n';
|
||||
let bgms = 'type BgmIds =\n';
|
||||
let fonts = 'type FontIds =\n';
|
||||
|
||||
floorId += d.floorIds.map((v: string) => ` | '${v}'\n`).join('');
|
||||
imgs += d.images.map((v: string) => ` | '${v}'\n`).join('');
|
||||
anis += d.animates.map((v: string) => ` | '${v}'\n`).join('');
|
||||
sounds += d.sounds.map((v: string) => ` | '${v}'\n`).join('');
|
||||
bgms += d.bgms.map((v: string) => ` | '${v}'\n`).join('');
|
||||
fonts += d.fonts.map((v: string) => ` | '${v}'\n`).join('');
|
||||
for (const name in d.nameMap) {
|
||||
names += ` '${name}': '${d.nameMap[name]}';\n`;
|
||||
}
|
||||
names += '}';
|
||||
|
||||
// 5. 写入文件
|
||||
await fs.writeFile('./src/types/source/cls.d.ts', id2cls, 'utf-8');
|
||||
await fs.writeFile('./src/types/source/events.d.ts', eventDec, 'utf-8');
|
||||
await fs.writeFile('./src/types/source/items.d.ts', itemDec, 'utf-8');
|
||||
await fs.writeFile(
|
||||
'./src/types/source/maps.d.ts',
|
||||
`${id2num}\n${num2id}`,
|
||||
'utf-8'
|
||||
);
|
||||
await fs.writeFile(
|
||||
'./src/types/source/data.d.ts',
|
||||
`
|
||||
${floorId}
|
||||
${d.images.length > 0 ? imgs : 'type ImageIds = never\n'}
|
||||
${d.animates.length > 0 ? anis : 'type AnimationIds = never\n'}
|
||||
${d.sounds.length > 0 ? sounds : 'type SoundIds = never\n'}
|
||||
${d.bgms.length > 0 ? bgms : 'type BgmIds = never\n'}
|
||||
${d.fonts.length > 0 ? fonts : 'type FontIds = never\n'}
|
||||
${names}
|
||||
`,
|
||||
'utf-8'
|
||||
);
|
||||
})();
|
||||
+772
@@ -0,0 +1,772 @@
|
||||
/* eslint-disable no-console */
|
||||
import {
|
||||
createServer,
|
||||
loadConfigFromFile,
|
||||
mergeConfig,
|
||||
UserConfig
|
||||
} from 'vite';
|
||||
import { Server } from 'http';
|
||||
import { ensureDir, move, pathExists, remove } from 'fs-extra';
|
||||
import { readFile, readdir, writeFile } from 'fs/promises';
|
||||
import { resolve, basename, join } from 'path';
|
||||
import * as rollup from 'rollup';
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import nodeResolve from '@rollup/plugin-node-resolve';
|
||||
import EventEmitter from 'events';
|
||||
import { WebSocket, WebSocketServer } from 'ws';
|
||||
import chokidar from 'chokidar';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import json from '@rollup/plugin-json';
|
||||
import replace from '@rollup/plugin-replace';
|
||||
import express, { Request, Response } from 'express';
|
||||
|
||||
const [, , vitePortStr = '5173', serverPortStr = '3000'] = process.argv;
|
||||
const vitePort = parseInt(vitePortStr);
|
||||
const serverPort = parseInt(serverPortStr);
|
||||
|
||||
const checkBase = resolve(process.cwd());
|
||||
const base = resolve(process.cwd(), 'public');
|
||||
|
||||
const enum APIStatus {
|
||||
Success,
|
||||
PermissionDeny,
|
||||
WriteError,
|
||||
FileNotExist,
|
||||
ReadError
|
||||
}
|
||||
|
||||
interface ResolveResult {
|
||||
safe: boolean;
|
||||
resolved: string;
|
||||
}
|
||||
|
||||
interface RollupInfo {
|
||||
dir: string;
|
||||
file: string;
|
||||
bundled: RefValue<boolean>;
|
||||
watcher: rollup.RollupWatcher;
|
||||
}
|
||||
const rollupMap = new Map<string, RollupInfo>();
|
||||
let bundleIndex = 0;
|
||||
let ws: WebSocket;
|
||||
let wt: chokidar.FSWatcher;
|
||||
|
||||
class RefValue<T> extends EventEmitter {
|
||||
private _value: T;
|
||||
public get value(): T {
|
||||
return this._value;
|
||||
}
|
||||
public set value(v: T) {
|
||||
this._value = v;
|
||||
this.emit('valueChange', v);
|
||||
}
|
||||
|
||||
constructor(value: T) {
|
||||
super();
|
||||
this._value = value;
|
||||
}
|
||||
|
||||
waitValueTo(value: T) {
|
||||
return new Promise(res => {
|
||||
if (this._value === value) {
|
||||
res(value);
|
||||
return;
|
||||
}
|
||||
const fn = (v: T) => {
|
||||
if (v === value) {
|
||||
this.off('valueChange', fn);
|
||||
res(v);
|
||||
}
|
||||
};
|
||||
this.on('valueChange', fn);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePath(path: string): ResolveResult {
|
||||
const targetPath = resolve(base, path);
|
||||
|
||||
const safe = targetPath.startsWith(checkBase);
|
||||
return {
|
||||
safe,
|
||||
resolved: targetPath
|
||||
};
|
||||
}
|
||||
|
||||
function parseBodyParam(body: string) {
|
||||
const arr = body.split('&');
|
||||
const obj: Record<string, string> = {};
|
||||
arr.forEach(v => {
|
||||
const [name, value] = v.split('=');
|
||||
obj[name] = value;
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
|
||||
function withSafeCheck(
|
||||
exec: (req: Request, res: Response, path: ResolveResult) => void
|
||||
) {
|
||||
return async (req: Request, res: Response) => {
|
||||
const query = parseBodyParam(req.body);
|
||||
const path = query.name ?? '';
|
||||
if (typeof path !== 'string') {
|
||||
res.status(500).end('Parameter Error: File path is required.');
|
||||
return;
|
||||
}
|
||||
const dir = resolvePath(path);
|
||||
if (!dir.safe) {
|
||||
res.status(500).end(
|
||||
'Permission Error: Cannot access file outside current working directory.'
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
exec(req, res, dir);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
interface AllFilesStatus {
|
||||
status: APIStatus;
|
||||
content: string;
|
||||
}
|
||||
|
||||
function getAllFiles(suffix: string, dir: string, join: string) {
|
||||
return async (req: Request, res: Response) => {
|
||||
const query = req.query ? req.query : parseBodyParam(req.body);
|
||||
const id = query.id;
|
||||
if (typeof id !== 'string') {
|
||||
res.status(404).end('Parameter Error: file names is required.');
|
||||
return;
|
||||
}
|
||||
|
||||
const list = id.split(',');
|
||||
const tasks = list.map<Promise<AllFilesStatus>>(async v => {
|
||||
const path = resolvePath(`${dir}${v}${suffix}`);
|
||||
if (!path.safe) {
|
||||
return Promise.resolve<AllFilesStatus>({
|
||||
status: APIStatus.PermissionDeny,
|
||||
content: ''
|
||||
});
|
||||
}
|
||||
|
||||
const exist = await pathExists(path.resolved);
|
||||
if (!exist) {
|
||||
return Promise.resolve<AllFilesStatus>({
|
||||
status: APIStatus.FileNotExist,
|
||||
content: ''
|
||||
});
|
||||
}
|
||||
|
||||
return readFile(path.resolved, 'utf-8').then(
|
||||
value => {
|
||||
return {
|
||||
status: APIStatus.Success,
|
||||
content: value
|
||||
};
|
||||
},
|
||||
reason => {
|
||||
console.error(reason);
|
||||
return { status: APIStatus.ReadError, content: '' };
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const contents = await Promise.all(tasks);
|
||||
if (contents.every(v => v.status === APIStatus.Success)) {
|
||||
const content = contents.map(v => v.content).join(join);
|
||||
if (suffix === '.js') {
|
||||
res.writeHead(200, { 'Content-type': 'text/javascript' });
|
||||
}
|
||||
res.end(content);
|
||||
} else {
|
||||
const strArray = contents.map((v, i) => {
|
||||
if (v.status === APIStatus.PermissionDeny) {
|
||||
return `Index: ${i}; Permission Error: Cannot access file outside current working directory`;
|
||||
} else if (v.status === APIStatus.Success) {
|
||||
return `Index: ${i}: Internal Error: Read file error.`;
|
||||
} else {
|
||||
return 'Success';
|
||||
}
|
||||
});
|
||||
const str = strArray.filter(v => v !== 'Success').join('\n');
|
||||
res.status(500).end(str);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function getEsmFile(
|
||||
req: Request,
|
||||
res: Response,
|
||||
dir: string
|
||||
): Promise<void> {
|
||||
const path = resolvePath(dir);
|
||||
if (!path.safe) {
|
||||
res.status(500).end(
|
||||
'Permission Error: Cannot access file outside current working directory'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const watcher = rollupMap.get(path.resolved);
|
||||
|
||||
if (!watcher) {
|
||||
const file = (bundleIndex++).toString();
|
||||
await ensureDir('_bundle');
|
||||
// 配置rollup监听器
|
||||
const w = rollup.watch({
|
||||
input: path.resolved,
|
||||
output: {
|
||||
file: `_bundle/${file}.js`,
|
||||
sourcemap: true,
|
||||
format: 'es'
|
||||
},
|
||||
cache: true,
|
||||
watch: {
|
||||
exclude: '**/node_modules/**'
|
||||
},
|
||||
plugins: [
|
||||
typescript({
|
||||
sourceMap: true,
|
||||
noCheck: true,
|
||||
paths: {
|
||||
'@motajs/*': ['packages/*/src'],
|
||||
'@user/*': ['packages-user/*/src']
|
||||
}
|
||||
}),
|
||||
nodeResolve({
|
||||
browser: true,
|
||||
preferBuiltins: false
|
||||
}),
|
||||
commonjs(),
|
||||
json(),
|
||||
replace({
|
||||
'import.meta.env.DEV': 'false'
|
||||
})
|
||||
],
|
||||
onwarn() {}
|
||||
});
|
||||
|
||||
const info: RollupInfo = {
|
||||
watcher: w,
|
||||
file: `_bundle/${file}.js`,
|
||||
dir,
|
||||
bundled: new RefValue(false)
|
||||
};
|
||||
w.on('event', e => {
|
||||
if (e.code === 'ERROR') {
|
||||
res.status(500).end('Internal Error: Esm build error.');
|
||||
console.log(e.error);
|
||||
}
|
||||
|
||||
if (e.code === 'BUNDLE_END') {
|
||||
info.bundled.value = true;
|
||||
console.log(`${path.resolved} bundle end`);
|
||||
}
|
||||
|
||||
if (e.code === 'BUNDLE_START') {
|
||||
info.bundled.value = false;
|
||||
}
|
||||
});
|
||||
w.on('change', id => {
|
||||
console.log(`${id} changed. Refresh Page.`);
|
||||
if (ws) {
|
||||
ws.send(JSON.stringify({ type: 'reload' }));
|
||||
}
|
||||
});
|
||||
rollupMap.set(path.resolved, info);
|
||||
|
||||
// 配置完毕,直接重新获取即可(
|
||||
return getEsmFile(req, res, dir);
|
||||
} else {
|
||||
try {
|
||||
await watcher.bundled.waitValueTo(true);
|
||||
const content = await readFile(watcher.file, 'utf-8');
|
||||
res.writeHead(200, { 'Content-type': 'text/javascript' });
|
||||
res.end(content);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const apiListFile = withSafeCheck(async (_, res, path) => {
|
||||
const exist = await pathExists(path.resolved);
|
||||
if (!exist) {
|
||||
res.status(404).end('Permission Error: Path does not exist.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = await readdir(path.resolved);
|
||||
res.end(JSON.stringify(data));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
res.status(500).end('Internal Error: Read dir error.');
|
||||
}
|
||||
});
|
||||
|
||||
const apiMakeDir = withSafeCheck(async (_, res, path) => {
|
||||
try {
|
||||
await ensureDir(path.resolved);
|
||||
res.end();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
res.status(500).end('Internal Error: Make dir error.');
|
||||
}
|
||||
});
|
||||
|
||||
const apiReadFile = withSafeCheck(async (req, res, path) => {
|
||||
const query = parseBodyParam(req.body);
|
||||
const type = query.type ?? 'utf8';
|
||||
if (typeof type !== 'string') {
|
||||
res.status(500).end('Internal Error: Query parsed failed.');
|
||||
return;
|
||||
}
|
||||
const exist = await pathExists(path.resolved);
|
||||
if (!exist) {
|
||||
res.status(404).end('Permission Error: Path does not exist.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const file = await readFile(path.resolved, {
|
||||
encoding: type as BufferEncoding
|
||||
});
|
||||
res.end(file);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
res.status(500).end('Internal Error: Read file error.');
|
||||
}
|
||||
});
|
||||
|
||||
const apiWriteFile = withSafeCheck(async (req, res, path) => {
|
||||
const query = parseBodyParam(req.body);
|
||||
const type = query.type ?? 'utf8';
|
||||
if (typeof type !== 'string') {
|
||||
res.status(500).end('Internal Error: Query parsed failed.');
|
||||
return;
|
||||
}
|
||||
const value = query.value;
|
||||
if (typeof value !== 'string') {
|
||||
res.status(500).end('Parameter Error: File content is required.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await writeFile(path.resolved, value, {
|
||||
encoding: type as BufferEncoding
|
||||
});
|
||||
res.end();
|
||||
|
||||
if (/project(\/|\\)events\.js/.test(path.resolved)) {
|
||||
doDeclaration('events', value);
|
||||
}
|
||||
if (/project(\/|\\)items\.js/.test(path.resolved)) {
|
||||
doDeclaration('items', value);
|
||||
}
|
||||
if (/project(\/|\\)maps\.js/.test(path.resolved)) {
|
||||
doDeclaration('maps', value);
|
||||
}
|
||||
if (/project(\/|\\)data\.js/.test(path.resolved)) {
|
||||
doDeclaration('data', value);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
res.status(500).end(
|
||||
'Internal Error: Fail to write file or fail to do declaration.'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const apiDeleteFile = withSafeCheck(async (_, res, path) => {
|
||||
const exist = await pathExists(path.resolved);
|
||||
if (!exist) {
|
||||
res.status(404).end('Permission Error: Path does not exist.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await remove(path.resolved);
|
||||
res.end();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
res.status(500).end('Internal Error: Remove file error.');
|
||||
}
|
||||
});
|
||||
|
||||
const apiMoveFile = async (req: Request, res: Response) => {
|
||||
const query = parseBodyParam(req.body);
|
||||
const src = query.src;
|
||||
const dest = query.dest;
|
||||
|
||||
if (typeof src !== 'string' || typeof dest !== 'string') {
|
||||
res.status(500).end(
|
||||
'Parameter Error: Source path or destination path is required.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const srcPath = resolvePath(src);
|
||||
const destPath = resolvePath(dest);
|
||||
|
||||
if (!srcPath.safe || !destPath.safe) {
|
||||
res.status(500).end(
|
||||
'Permission Error: Cannot access file outside current working directory.'
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await move(srcPath.resolved, destPath.resolved);
|
||||
res.end();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
res.status(500).end('Internal Error: Move file error.');
|
||||
}
|
||||
};
|
||||
|
||||
const apiWriteMultiFiles = async (req: Request, res: Response) => {
|
||||
const query = parseBodyParam(req.body);
|
||||
const name = query.name;
|
||||
const value = query.value;
|
||||
|
||||
if (typeof name !== 'string' || typeof value !== 'string') {
|
||||
res.status(500).end(
|
||||
'Parameter Error: File names and content is required.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const pathList = name.split(';');
|
||||
const valueList = value.split(';');
|
||||
|
||||
if (pathList.length !== valueList.length) {
|
||||
res.status(500).end(
|
||||
'Parameter Error: File name and content count must match.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const tasks = pathList.map<Promise<APIStatus>>((v, i) => {
|
||||
const path = resolvePath(v);
|
||||
if (!path.safe) {
|
||||
return Promise.resolve<APIStatus>(APIStatus.PermissionDeny);
|
||||
}
|
||||
return new Promise<APIStatus>(resolve => {
|
||||
writeFile(v, valueList[i]).then(
|
||||
() => {
|
||||
resolve(APIStatus.Success);
|
||||
},
|
||||
reason => {
|
||||
console.error(reason);
|
||||
resolve(APIStatus.WriteError);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const status = await Promise.all(tasks);
|
||||
|
||||
if (status.every(v => v === APIStatus.Success)) {
|
||||
res.end();
|
||||
} else {
|
||||
const strArray = status.map((v, i) => {
|
||||
if (v === APIStatus.PermissionDeny) {
|
||||
return `Index: ${i}; Permission Error: Cannot access file outside current working directory`;
|
||||
} else if (v === APIStatus.Success) {
|
||||
return `Index: ${i}: Internal Error: Write file error.`;
|
||||
} else {
|
||||
return 'Success';
|
||||
}
|
||||
});
|
||||
const str = strArray.filter(v => v !== 'Success').join('\n');
|
||||
res.status(500).end(str);
|
||||
}
|
||||
};
|
||||
|
||||
const apiGetAllFloors = getAllFiles('.js', 'project/floors/', '\n');
|
||||
const apiGetAllAnimates = getAllFiles(
|
||||
'.animate',
|
||||
'project/animates/',
|
||||
'@@@~~~###~~~@@@'
|
||||
);
|
||||
|
||||
const apiGetEsmFiles = async (req: Request, res: Response) => {
|
||||
const query = req.query ? req.query : parseBodyParam(req.body);
|
||||
const name = query.name;
|
||||
if (typeof name !== 'string') {
|
||||
res.status(500).end('Parameter Error: File name is required.');
|
||||
return;
|
||||
}
|
||||
const path = resolvePath(join('..', name));
|
||||
if (!path.safe) {
|
||||
res.status(500).end(
|
||||
'Permission Error: Cannot access file outside current working directory'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
return getEsmFile(req, res, path.resolved);
|
||||
};
|
||||
|
||||
const apiGetPort = async (_req: Request, res: Response) => {
|
||||
const port = {
|
||||
vite: vitePort,
|
||||
server: serverPort
|
||||
};
|
||||
res.end(JSON.stringify(port));
|
||||
};
|
||||
|
||||
/**
|
||||
* 声明某种类型
|
||||
* @param {string} type 类型
|
||||
* @param {string} data 信息
|
||||
*/
|
||||
async function doDeclaration(type: string, data: string) {
|
||||
try {
|
||||
const buf = Buffer.from(data, 'base64');
|
||||
data = buf.toString('utf-8');
|
||||
if (type === 'events') {
|
||||
// 事件
|
||||
const eventData = JSON.parse(data.split('\n').slice(1).join(''));
|
||||
|
||||
let eventDec = 'type EventDeclaration = \n';
|
||||
for (const id in eventData.commonEvent) {
|
||||
eventDec += ` | '${id}'\n`;
|
||||
}
|
||||
await writeFile('src/types/source/events.d.ts', eventDec, 'utf-8');
|
||||
} else if (type === 'items') {
|
||||
// 道具
|
||||
const itemData = JSON.parse(data.split('\n').slice(1).join(''));
|
||||
|
||||
let itemDec = 'interface ItemDeclaration {\n';
|
||||
for (const id in itemData) {
|
||||
itemDec += ` ${id}: '${itemData[id].cls}';\n`;
|
||||
}
|
||||
itemDec += '}';
|
||||
await writeFile('src/types/source/items.d.ts', itemDec, 'utf-8');
|
||||
} else if (type === 'maps') {
|
||||
// 映射
|
||||
const d = JSON.parse(data.split('\n').slice(1).join(''));
|
||||
|
||||
let id2num = 'interface IdToNumber {\n';
|
||||
let num2id = 'interface NumberToId {\n';
|
||||
let id2cls = 'interface IdToCls {\n';
|
||||
for (const num in d) {
|
||||
const { id, cls } = d[num];
|
||||
id2num += ` ${id}: ${num};\n`;
|
||||
num2id += ` ${num}: '${id}';\n`;
|
||||
id2cls += ` ${id}: '${cls}';\n`;
|
||||
}
|
||||
id2cls += '}';
|
||||
id2num += '}';
|
||||
num2id += '}';
|
||||
await writeFile('src/types/source/cls.d.ts', id2cls, 'utf-8');
|
||||
await writeFile(
|
||||
'src/types/source/maps.d.ts',
|
||||
`${id2num}\n${num2id}`,
|
||||
'utf-8'
|
||||
);
|
||||
} else if (type === 'data') {
|
||||
// 全塔属性的注册信息
|
||||
const d = JSON.parse(data.split('\n').slice(1).join('')).main;
|
||||
|
||||
let floorId = 'type FloorIds =\n';
|
||||
let imgs = 'type ImageIds =\n';
|
||||
let anis = 'type AnimationIds =\n';
|
||||
let sounds = 'type SoundIds =\n';
|
||||
let names = 'interface NameMap {\n';
|
||||
let bgms = 'type BgmIds =\n';
|
||||
let fonts = 'type FontIds =\n';
|
||||
|
||||
floorId += d.floorIds.map((v: any) => ` | '${v}'\n`).join('');
|
||||
imgs += d.images.map((v: any) => ` | '${v}'\n`).join('');
|
||||
anis += d.animates.map((v: any) => ` | '${v}'\n`).join('');
|
||||
sounds += d.sounds.map((v: any) => ` | '${v}'\n`).join('');
|
||||
bgms += d.bgms.map((v: any) => ` | '${v}'\n`).join('');
|
||||
fonts += d.fonts.map((v: any) => ` | '${v}'\n`).join('');
|
||||
for (const name in d.nameMap) {
|
||||
names += ` '${name}': '${d.nameMap[name]}';\n`;
|
||||
}
|
||||
names += '}';
|
||||
|
||||
await writeFile(
|
||||
'src/types/source/data.d.ts',
|
||||
`
|
||||
${floorId}
|
||||
${d.images.length > 0 ? imgs : 'type ImageIds = never\n'}
|
||||
${d.animates.length > 0 ? anis : 'type AnimationIds = never\n'}
|
||||
${d.sounds.length > 0 ? sounds : 'type SoundIds = never\n'}
|
||||
${d.bgms.length > 0 ? bgms : 'type BgmIds = never\n'}
|
||||
${d.fonts.length > 0 ? fonts : 'type FontIds = never\n'}
|
||||
${names}
|
||||
`,
|
||||
'utf-8'
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
function watchProject() {
|
||||
if (wt) return;
|
||||
const watcher = chokidar.watch('public/', {
|
||||
persistent: true,
|
||||
ignored: [
|
||||
'**/node_modules/**',
|
||||
'**/.git/**',
|
||||
'**/thirdparty/**',
|
||||
'**/_docs/**',
|
||||
'**/_save/**',
|
||||
/\.min\./,
|
||||
/(^|[/\\])\../,
|
||||
/(^|[/\\])[^a-zA-Z:._0-9/\\]/,
|
||||
/_.*/
|
||||
]
|
||||
});
|
||||
wt = watcher;
|
||||
watcher.removeAllListeners();
|
||||
watcher.on('change', async path => {
|
||||
if (!ws) return;
|
||||
// 楼层热重载
|
||||
if (/project(\/|\\)floors(\/|\\).*\.js$/.test(path)) {
|
||||
const floor = basename(path).slice(0, -3);
|
||||
ws.send(JSON.stringify({ type: 'floorHotReload', floor }));
|
||||
console.log(`Floor hot reload: ${floor}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 脚本编辑热重载
|
||||
if (/project(\/|\\)functions\.js$/.test(path)) {
|
||||
ws.send(JSON.stringify({ type: 'functionsHotReload' }));
|
||||
console.log(`Functions hot reload.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 数据热重载
|
||||
if (/project(\/|\\).*\.js/.test(path)) {
|
||||
const data = basename(path).slice(0, -3);
|
||||
ws.send(JSON.stringify({ type: 'dataHotReload', data }));
|
||||
console.log(`Data hot reload: ${data}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// css热重载
|
||||
if (/.*\.css$/.test(path)) {
|
||||
ws.send(JSON.stringify({ type: 'cssHotReload', path }));
|
||||
console.log(`Css hot reload: ${path}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 剩余内容全部reload
|
||||
ws.send(JSON.stringify({ type: 'reload' }));
|
||||
});
|
||||
}
|
||||
|
||||
function setupSocket(socket: WebSocket) {
|
||||
ws = socket;
|
||||
socket.send(JSON.stringify({ type: 'connected' }));
|
||||
watchProject();
|
||||
}
|
||||
|
||||
async function startWsServer(http: Server) {
|
||||
if (ws) return;
|
||||
|
||||
const server = new WebSocketServer({
|
||||
server: http
|
||||
});
|
||||
|
||||
server.on('connection', socket => {
|
||||
setupSocket(socket);
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureConfig() {
|
||||
const { resolved, safe } = resolvePath('_server/config.json');
|
||||
if (!safe) {
|
||||
throw new Error('Internal Error: Fail to access editor config file.');
|
||||
}
|
||||
const exist = await pathExists(resolved);
|
||||
if (!exist) {
|
||||
return writeFile(resolved, '{}', { encoding: 'utf-8' });
|
||||
}
|
||||
}
|
||||
|
||||
(async function () {
|
||||
// 1. 加载 vite.config.ts
|
||||
const fsHost = `http://127.0.0.1:${serverPort}`;
|
||||
const config = await loadConfigFromFile({
|
||||
command: 'serve',
|
||||
mode: 'development'
|
||||
});
|
||||
if (!config) {
|
||||
console.error(`Cannot load config file.`);
|
||||
return;
|
||||
}
|
||||
const merged = mergeConfig(config.config, {
|
||||
server: {
|
||||
proxy: {
|
||||
'/readFile': fsHost,
|
||||
'/writeFile': fsHost,
|
||||
'/writeMultiFiles': fsHost,
|
||||
'/listFile': fsHost,
|
||||
'/makeDir': fsHost,
|
||||
'/moveFile': fsHost,
|
||||
'/deleteFile': fsHost,
|
||||
'/getPort': fsHost,
|
||||
'^/all/.*': fsHost,
|
||||
'^/forceTem/.*': {
|
||||
target: fsHost,
|
||||
changeOrigin: true,
|
||||
rewrite(path) {
|
||||
return path.replace(/^\/forceTem/, '');
|
||||
}
|
||||
},
|
||||
'/danmaku': 'https://h5mota.com/backend/tower/barrage.php'
|
||||
}
|
||||
}
|
||||
} satisfies UserConfig);
|
||||
|
||||
// 2. 启动vite服务
|
||||
const vite = await createServer({
|
||||
...merged,
|
||||
configFile: false
|
||||
});
|
||||
await vite.listen(vitePort);
|
||||
console.log(`游戏地址:http://localhost:${vitePort}/`);
|
||||
|
||||
// 3. 启动样板http服务
|
||||
await ensureConfig();
|
||||
|
||||
const app = express();
|
||||
app.use(express.text());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.static(base));
|
||||
|
||||
app.post('/listFile', apiListFile);
|
||||
app.post('/makeDir', apiMakeDir);
|
||||
app.post('/readFile', apiReadFile);
|
||||
app.post('/writeFile', apiWriteFile);
|
||||
app.post('/deleteFile', apiDeleteFile);
|
||||
app.post('/moveFile', apiMoveFile);
|
||||
app.post('/writeMultiFiles', apiWriteMultiFiles);
|
||||
app.get('/all/__all_floors__.js', apiGetAllFloors);
|
||||
app.get('/all/__all_animates__', apiGetAllAnimates);
|
||||
app.get('/esm', apiGetEsmFiles);
|
||||
app.get('/getPort', apiGetPort);
|
||||
|
||||
const server = app.listen(serverPort);
|
||||
|
||||
server.on('listening', () => {
|
||||
console.log(`编辑器地址:http://127.0.0.1:${serverPort}/editor.html`);
|
||||
console.log(
|
||||
`文档地址:http://127.0.0.1:${serverPort}/_docs/index.html`
|
||||
);
|
||||
});
|
||||
|
||||
// 4. 启动样板ws热重载服务
|
||||
startWsServer(server);
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
vite.close();
|
||||
server.close();
|
||||
process.exit(0);
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,76 @@
|
||||
import fs from 'fs-extra';
|
||||
import { extname, resolve } from 'path';
|
||||
import { formatSize } from './utils.js';
|
||||
|
||||
(async function () {
|
||||
let dir = process.argv.slice(2);
|
||||
if (dir.length === 0) dir = ['./src'];
|
||||
let totalLines = 0;
|
||||
let totalFiles = 0;
|
||||
let totalSize = 0;
|
||||
const list: Record<string, [number, number, number]> = {};
|
||||
const ignoreDir = [
|
||||
'node_modules',
|
||||
'floors',
|
||||
'thirdparty',
|
||||
'dist',
|
||||
'_bundle'
|
||||
];
|
||||
const ignoreFile = ['.d.ts', '.min.'];
|
||||
const exts = [
|
||||
'.ts',
|
||||
'.tsx',
|
||||
'.js',
|
||||
'.jsx',
|
||||
'.vue',
|
||||
'.less',
|
||||
'.css',
|
||||
'.html'
|
||||
];
|
||||
|
||||
const check = async (dir: string) => {
|
||||
if (ignoreDir.some(v => dir.includes(v))) return;
|
||||
const d = await fs.readdir(dir);
|
||||
for await (const one of d) {
|
||||
if (ignoreFile.some(v => one.includes(v))) continue;
|
||||
const stat = await fs.stat(resolve(dir, one));
|
||||
if (stat.isFile()) {
|
||||
if (exts.some(v => one.endsWith(v))) {
|
||||
const file = await fs.readFile(resolve(dir, one), 'utf-8');
|
||||
const lines = file.split('\n').length;
|
||||
const ext = extname(one);
|
||||
list[ext] ??= [0, 0, 0];
|
||||
list[ext][0]++;
|
||||
list[ext][1] += lines;
|
||||
list[ext][2] += stat.size;
|
||||
totalLines += lines;
|
||||
totalFiles++;
|
||||
totalSize += stat.size;
|
||||
}
|
||||
} else {
|
||||
await check(resolve(dir, one));
|
||||
}
|
||||
}
|
||||
};
|
||||
await Promise.all(dir.map(v => check(v)));
|
||||
|
||||
const sorted = Object.entries(list).sort((a, b) => {
|
||||
return a[1][1] - b[1][1];
|
||||
});
|
||||
for (const [ext, [file, lines, size]] of sorted) {
|
||||
console.log(
|
||||
`${ext.slice(1).padEnd(7, ' ')}files: ${file
|
||||
.toString()
|
||||
.padEnd(6, ' ')}lines: ${lines
|
||||
.toString()
|
||||
.padEnd(9, ' ')}size: ${formatSize(size)}`
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
`\x1b[33mtotal files: ${totalFiles
|
||||
.toString()
|
||||
.padEnd(6, ' ')}lines: ${totalLines
|
||||
.toString()
|
||||
.padEnd(9, ' ')}size: ${formatSize(totalSize)}\x1b[0m`
|
||||
);
|
||||
})();
|
||||
@@ -0,0 +1,55 @@
|
||||
/* eslint-disable no-console */
|
||||
import { copy, emptyDir, ensureDir } from 'fs-extra';
|
||||
import { writeFile } from 'fs/promises';
|
||||
import { resolve } from 'path';
|
||||
|
||||
const base = resolve(process.cwd());
|
||||
const template = resolve(base, 'template');
|
||||
|
||||
const serve = `在 2.B 样板中,不再使用先前的启动服务,而使用一个单独的软件。
|
||||
你可以加造塔群 959329661 后,在群文件 - 启动服务 中获取,文件夹中会有一个专门的安装教程。`;
|
||||
|
||||
async function packTemplate() {
|
||||
await ensureDir(template);
|
||||
await emptyDir(template);
|
||||
|
||||
// 复制必要文件
|
||||
const toCopy = [
|
||||
'.vscode',
|
||||
'packages',
|
||||
'packages-user',
|
||||
'public',
|
||||
'script',
|
||||
'src',
|
||||
'.gitignore',
|
||||
'.madgerc',
|
||||
'.prettierignore',
|
||||
'.prettierrc',
|
||||
'eslint.config.js',
|
||||
'index.html',
|
||||
'LICENSE',
|
||||
'package.json',
|
||||
'pnpm-lock.yaml',
|
||||
'pnpm-workspace.yaml',
|
||||
'README.md',
|
||||
'tsconfig.json',
|
||||
'tsconfig.node.json',
|
||||
'vite.config.ts'
|
||||
];
|
||||
|
||||
await Promise.all(
|
||||
toCopy.map(v =>
|
||||
copy(resolve(base, v), resolve(template, v), {
|
||||
filter: src => !src.includes('node_modules')
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
await writeFile(resolve(template, '启动服务呢?.txt'), serve, 'utf-8');
|
||||
|
||||
console.log(`样板打包完成`);
|
||||
}
|
||||
|
||||
(() => {
|
||||
packTemplate();
|
||||
})();
|
||||
@@ -0,0 +1,39 @@
|
||||
export interface RequiredData {
|
||||
main: {
|
||||
floorIds: string[];
|
||||
images: string[];
|
||||
tilesets: string[];
|
||||
animates: string[];
|
||||
bgms: string[];
|
||||
sounds: string[];
|
||||
fonts: string[];
|
||||
};
|
||||
firstData: {
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RequiredIconsData {
|
||||
autotile: {
|
||||
[x: string]: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type ResourceUsage =
|
||||
| 'image'
|
||||
| 'tileset'
|
||||
| 'animate'
|
||||
| 'sound'
|
||||
| 'font'
|
||||
| 'autotile'
|
||||
| 'material';
|
||||
|
||||
export type ResourceType =
|
||||
| 'text'
|
||||
| 'buffer'
|
||||
| 'image'
|
||||
| 'material'
|
||||
| 'audio'
|
||||
| 'json'
|
||||
| 'zip'
|
||||
| 'byte';
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
export function uniqueSymbol() {
|
||||
return Math.ceil(Math.random() * 0xefffffff + 0x10000000).toString(16);
|
||||
}
|
||||
|
||||
export function fileHash(
|
||||
content: string | Buffer | Uint8Array,
|
||||
length: number = 8
|
||||
) {
|
||||
return createHash('sha256').update(content).digest('hex').slice(0, length);
|
||||
}
|
||||
|
||||
export function formatSize(size: number) {
|
||||
if (size < 1 << 10) {
|
||||
return `${size.toFixed(2)}B`;
|
||||
} else if (size < 1 << 20) {
|
||||
return `${(size / (1 << 10)).toFixed(2)}KB`;
|
||||
} else if (size < 1 << 30) {
|
||||
return `${(size / (1 << 20)).toFixed(2)}MB`;
|
||||
} else {
|
||||
return `${(size / (1 << 30)).toFixed(2)}GB`;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user