|
@@ -18,6 +18,10 @@ let path = require('path');
|
|
|
let pretty = require('pretty-bytes');
|
|
|
let shell = require('shelljs');
|
|
|
let babel = require('gulp-babel');
|
|
|
+let assert = require('assert');
|
|
|
+let gulpIf = require('gulp-if');
|
|
|
+
|
|
|
+let isSilentDetect = false; // <-- 添加全局标志位
|
|
|
|
|
|
// 在Gulp 4.x中,runSequence已被移除,使用gulp.series和gulp.parallel代替
|
|
|
// let runSequence = require('run-sequence');
|
|
@@ -29,7 +33,7 @@ function cleanOutput() {
|
|
|
|
|
|
// 复制静态资源
|
|
|
function copyAssets() {
|
|
|
- return gulp.src(['apps/**/*.{gif,png,jpg,jpeg,cur,ico}', '!apps/static/screenshot/**/*']).pipe(copy('output'));
|
|
|
+ return gulp.src(['apps/**/*.{gif,png,jpg,jpeg,cur,ico,ttf,.woff2}', '!apps/static/screenshot/**/*']).pipe(copy('output'));
|
|
|
}
|
|
|
|
|
|
// 处理JSON文件
|
|
@@ -64,16 +68,29 @@ function processJs() {
|
|
|
})
|
|
|
};
|
|
|
|
|
|
+ // 定义哪些文件不需要 Babel 和 Uglify 处理
|
|
|
+ const shouldSkipProcessing = (file) => {
|
|
|
+ const relativePath = path.relative(path.join(process.cwd(), 'apps'), file.path);
|
|
|
+ // 跳过 chart-maker/lib、static/vendor 和 code-compress 下的文件
|
|
|
+ return relativePath.startsWith('chart-maker/lib/')
|
|
|
+ || relativePath.startsWith('static/vendor/')
|
|
|
+ || relativePath.startsWith('code-compress/');
|
|
|
+ // 或者更具体地跳过这三个文件:
|
|
|
+ // return relativePath === 'chart-maker/lib/xlsx.full.min.js'
|
|
|
+ // || relativePath === 'static/vendor/evalCore.min.js'
|
|
|
+ // || relativePath === 'code-compress/htmlminifier.min.js';
|
|
|
+ };
|
|
|
+
|
|
|
return gulp.src('apps/**/*.js')
|
|
|
.pipe(jsMerge())
|
|
|
- .pipe(babel({
|
|
|
+ .pipe(gulpIf(file => !shouldSkipProcessing(file), babel({
|
|
|
presets: ['@babel/preset-env']
|
|
|
- }))
|
|
|
- .pipe(uglifyjs({
|
|
|
+ })))
|
|
|
+ .pipe(gulpIf(file => !shouldSkipProcessing(file), uglifyjs({
|
|
|
compress: {
|
|
|
ecma: 2015
|
|
|
}
|
|
|
- }))
|
|
|
+ })))
|
|
|
.pipe(gulp.dest('output/apps'));
|
|
|
}
|
|
|
|
|
@@ -204,6 +221,264 @@ function syncFiles() {
|
|
|
return gulp.src('apps/**/*').pipe(gulp.dest('output/apps'));
|
|
|
}
|
|
|
|
|
|
+// 设置静默标志
|
|
|
+function setSilentDetect(cb) {
|
|
|
+ isSilentDetect = true;
|
|
|
+ cb();
|
|
|
+}
|
|
|
+
|
|
|
+// 取消静默标志
|
|
|
+function unsetSilentDetect(cb) {
|
|
|
+ isSilentDetect = false;
|
|
|
+ cb();
|
|
|
+}
|
|
|
+
|
|
|
+// 检测未使用的静态文件
|
|
|
+function detectUnusedFiles(cb) {
|
|
|
+ const allFiles = new Set();
|
|
|
+ const referencedFiles = new Set();
|
|
|
+
|
|
|
+ // 检查文件是否应该被排除
|
|
|
+ function shouldExcludeFile(filePath) {
|
|
|
+ // 排除 content-script 文件
|
|
|
+ if (filePath.includes('content-script.js') || filePath.includes('content-script.css')) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ // 排除 node_modules 目录
|
|
|
+ if (filePath.includes('node_modules')) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ // 排除 fh-config.js
|
|
|
+ if (filePath.endsWith('fh-config.js')) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 递归获取所有文件
|
|
|
+ function getAllFiles(dir) {
|
|
|
+ const files = fs.readdirSync(dir);
|
|
|
+ files.forEach(file => {
|
|
|
+ const fullPath = path.join(dir, file);
|
|
|
+ if (fs.statSync(fullPath).isDirectory()) {
|
|
|
+ if (file !== 'node_modules') { // 排除 node_modules 目录
|
|
|
+ getAllFiles(fullPath);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 只关注静态资源文件,并排除特殊文件
|
|
|
+ if (/\.(js|css|png|jpg|jpeg|gif|svg)$/i.test(file) && !shouldExcludeFile(fullPath)) {
|
|
|
+ const relativePath = path.relative('output/apps', fullPath);
|
|
|
+ allFiles.add(relativePath);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从文件内容中查找引用
|
|
|
+ function findReferences(content, filePath) {
|
|
|
+ const fileDir = path.dirname(filePath);
|
|
|
+ const patterns = [
|
|
|
+ // Capture content inside quotes (potential paths)
|
|
|
+ /['"`][^`'"]*?([./\w-]+\.(?:js|css|png|jpg|jpeg|gif|svg))['"`]/g,
|
|
|
+ // Capture content inside url()
|
|
|
+ /url\(['"]?([./\w-]+(?:\.(?:png|jpg|jpeg|gif|svg))?)['"]?\)/gi,
|
|
|
+ // Capture @import paths
|
|
|
+ /@import\s+['"]([^'"]+\.css)['"];?/gi,
|
|
|
+ // Capture src/href attributes, including chrome-extension
|
|
|
+ /(?:src|href)=['"](chrome-extension:\/\/[^/]+\/)?([^'"?#]+(?:\.(?:js|css|png|jpg|jpeg|gif|svg)))['"]/gi
|
|
|
+ ];
|
|
|
+
|
|
|
+ patterns.forEach((pattern, index) => {
|
|
|
+ let match;
|
|
|
+ while ((match = pattern.exec(content)) !== null) {
|
|
|
+
|
|
|
+ let extractedPath = '';
|
|
|
+ // Special handling for src/href pattern which captures optional chrome-extension part
|
|
|
+ if (index === 3) {
|
|
|
+ extractedPath = match[2]; // The path part after potential chrome-extension prefix
|
|
|
+ } else {
|
|
|
+ extractedPath = match[1];
|
|
|
+ }
|
|
|
+
|
|
|
+ // Skip empty or invalid matches
|
|
|
+ if (!extractedPath || typeof extractedPath !== 'string') continue;
|
|
|
+
|
|
|
+ // Skip special files early
|
|
|
+ if (shouldExcludeFile(extractedPath)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ let finalPathToAdd = '';
|
|
|
+
|
|
|
+ // Check if it was originally a chrome-extension url (by checking match[1] from the specific regex)
|
|
|
+ const isChromeExt = index === 3 && match[1];
|
|
|
+
|
|
|
+ if (isChromeExt || extractedPath.startsWith('/')) {
|
|
|
+ // chrome-extension paths are relative to root, absolute paths are relative to root
|
|
|
+ finalPathToAdd = extractedPath.startsWith('/') ? extractedPath.slice(1) : extractedPath;
|
|
|
+ } else {
|
|
|
+ // Resolve relative paths (./, ../, or direct filename)
|
|
|
+ const absolutePath = path.resolve(fileDir, extractedPath);
|
|
|
+ finalPathToAdd = path.relative('output/apps', absolutePath);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Final check before adding
|
|
|
+ if (finalPathToAdd && !shouldExcludeFile(finalPathToAdd)) {
|
|
|
+ // Ensure consistent path separators (use /) and add to set
|
|
|
+ referencedFiles.add(finalPathToAdd.replace(/\\/g, '/'));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 读取manifest.json中的引用
|
|
|
+ function processManifest() {
|
|
|
+ const manifestPath = 'output/apps/manifest.json';
|
|
|
+ if (fs.existsSync(manifestPath)) {
|
|
|
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
|
+
|
|
|
+ // 检查manifest中的各种可能包含资源的字段
|
|
|
+ const checkManifestField = (obj) => {
|
|
|
+ if (typeof obj === 'string' && /\.(js|css|png|jpg|jpeg|gif|svg)$/i.test(obj)) {
|
|
|
+ const normalizedPath = obj.startsWith('/') ? obj.slice(1) : obj;
|
|
|
+ if (!shouldExcludeFile(normalizedPath)) {
|
|
|
+ referencedFiles.add(normalizedPath);
|
|
|
+ }
|
|
|
+ } else if (Array.isArray(obj)) {
|
|
|
+ obj.forEach(item => checkManifestField(item));
|
|
|
+ } else if (typeof obj === 'object' && obj !== null) {
|
|
|
+ Object.values(obj).forEach(value => checkManifestField(value));
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 特殊处理content_scripts
|
|
|
+ if (manifest.content_scripts) {
|
|
|
+ manifest.content_scripts.forEach(script => {
|
|
|
+ if (script.js) {
|
|
|
+ script.js.forEach(js => {
|
|
|
+ const normalizedPath = js.startsWith('/') ? js.slice(1) : js;
|
|
|
+ referencedFiles.add(normalizedPath);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ if (script.css) {
|
|
|
+ script.css.forEach(css => {
|
|
|
+ const normalizedPath = css.startsWith('/') ? css.slice(1) : css;
|
|
|
+ referencedFiles.add(normalizedPath);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ checkManifestField(manifest);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 单元测试
|
|
|
+ function runTests() {
|
|
|
+ if (!isSilentDetect) console.log('\n运行单元测试...');
|
|
|
+
|
|
|
+ // 测试文件排除逻辑
|
|
|
+ assert.strictEqual(shouldExcludeFile('path/to/content-script.js'), true, 'Should exclude content-script.js');
|
|
|
+ assert.strictEqual(shouldExcludeFile('path/to/content-script.css'), true, 'Should exclude content-script.css');
|
|
|
+ assert.strictEqual(shouldExcludeFile('path/to/node_modules/file.js'), true, 'Should exclude node_modules files');
|
|
|
+ assert.strictEqual(shouldExcludeFile('path/to/normal.js'), false, 'Should not exclude normal files');
|
|
|
+
|
|
|
+ // 测试路径解析
|
|
|
+ const testContent = `
|
|
|
+ <link rel="stylesheet" href="./style.css">
|
|
|
+ <script src="../js/script.js"></script>
|
|
|
+ <img src="/images/test.png">
|
|
|
+ <div style="background: url('./bg.jpg')">
|
|
|
+ @import '../common.css';
|
|
|
+ <img src="chrome-extension://abcdefgh/static/icon.png">
|
|
|
+ `;
|
|
|
+
|
|
|
+ const testFilePath = 'output/apps/test/index.html';
|
|
|
+ referencedFiles.clear(); // 清理之前的测试数据
|
|
|
+ findReferences(testContent, testFilePath);
|
|
|
+
|
|
|
+ // 验证引用是否被正确解析
|
|
|
+ const refs = Array.from(referencedFiles);
|
|
|
+ assert(refs.includes('test/style.css'), 'Should handle relative path with ./');
|
|
|
+ assert(refs.includes('js/script.js'), 'Should handle relative path with ../');
|
|
|
+ assert(refs.includes('images/test.png'), 'Should handle absolute path');
|
|
|
+ assert(refs.includes('test/bg.jpg'), 'Should handle url() in CSS');
|
|
|
+ assert(refs.includes('common.css'), 'Should handle @import in CSS');
|
|
|
+ assert(refs.includes('static/icon.png'), 'Should handle chrome-extension urls');
|
|
|
+
|
|
|
+ // 清理测试数据
|
|
|
+ referencedFiles.clear();
|
|
|
+
|
|
|
+ if (!isSilentDetect) console.log('单元测试通过!');
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 运行单元测试
|
|
|
+ runTests();
|
|
|
+
|
|
|
+ // 执行实际检测
|
|
|
+ getAllFiles('output/apps');
|
|
|
+ processManifest();
|
|
|
+
|
|
|
+ // 扫描所有文件内容中的引用
|
|
|
+ const filesToScan = fs.readdirSync('output/apps', { recursive: true })
|
|
|
+ .filter(file => !shouldExcludeFile(file));
|
|
|
+
|
|
|
+ filesToScan.forEach(file => {
|
|
|
+ const fullPath = path.join('output/apps', file);
|
|
|
+ if (fs.statSync(fullPath).isFile() && /\.(html|js|css|json)$/i.test(file)) {
|
|
|
+ const content = fs.readFileSync(fullPath, 'utf8');
|
|
|
+ findReferences(content, fullPath);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 找出未被引用的文件
|
|
|
+ const unusedFiles = Array.from(allFiles).filter(file => !referencedFiles.has(file));
|
|
|
+
|
|
|
+ if (unusedFiles.length > 0) {
|
|
|
+ if (!isSilentDetect) console.log('\n发现以下未被引用的文件:');
|
|
|
+ if (!isSilentDetect) console.log('=====================================');
|
|
|
+ let totalUnusedSize = 0;
|
|
|
+ unusedFiles.forEach(file => {
|
|
|
+ if (!isSilentDetect) console.log(file);
|
|
|
+ try {
|
|
|
+ const fullPath = path.join('output/apps', file);
|
|
|
+ if (fs.existsSync(fullPath)) {
|
|
|
+ totalUnusedSize += fs.statSync(fullPath).size;
|
|
|
+ // 删除文件
|
|
|
+ fs.unlinkSync(fullPath);
|
|
|
+ if (!isSilentDetect) console.log(` -> 已删除: ${file}`);
|
|
|
+ } else {
|
|
|
+ if (!isSilentDetect) console.warn(` -> 文件不存在,无法删除或统计大小: ${file}`);
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ if (!isSilentDetect) console.warn(`无法删除或获取文件大小: ${file}`, err);
|
|
|
+ }
|
|
|
+
|
|
|
+ });
|
|
|
+ if (!isSilentDetect) console.log('=====================================');
|
|
|
+ if (!isSilentDetect) console.log(`共清理 ${unusedFiles.length} 个未使用的文件,释放空间: ${pretty(totalUnusedSize)}`);
|
|
|
+
|
|
|
+ // 输出详细信息(仅在DEBUG模式下)
|
|
|
+ if (process.env.DEBUG && !isSilentDetect) {
|
|
|
+ console.log('\n调试信息:');
|
|
|
+ console.log('----------------------------------------');
|
|
|
+ console.log('所有文件:');
|
|
|
+ Array.from(allFiles).forEach(file => console.log(`- ${file}`));
|
|
|
+ console.log('----------------------------------------');
|
|
|
+ console.log('被引用的文件:');
|
|
|
+ Array.from(referencedFiles).forEach(file => console.log(`- ${file}`));
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (!isSilentDetect) console.log('\n没有发现未使用的文件!');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ if (!isSilentDetect) console.error('检测过程中发生错误:', error);
|
|
|
+ }
|
|
|
+
|
|
|
+ cb();
|
|
|
+}
|
|
|
+
|
|
|
// 注册任务
|
|
|
gulp.task('clean', cleanOutput);
|
|
|
gulp.task('copy', copyAssets);
|
|
@@ -215,12 +490,18 @@ gulp.task('zip', zipPackage);
|
|
|
gulp.task('edge', edgePackage);
|
|
|
gulp.task('firefox', firefoxPackage);
|
|
|
gulp.task('sync', syncFiles);
|
|
|
+gulp.task('detect', detectUnusedFiles);
|
|
|
+gulp.task('setSilent', setSilentDetect);
|
|
|
+gulp.task('unsetSilent', unsetSilentDetect);
|
|
|
|
|
|
// 定义默认任务 - 在Gulp 4.x中,使用series和parallel代替runSequence
|
|
|
gulp.task('default',
|
|
|
gulp.series(
|
|
|
cleanOutput,
|
|
|
gulp.parallel(copyAssets, processCss, processJs, processHtml, processJson),
|
|
|
+ setSilentDetect,
|
|
|
+ detectUnusedFiles,
|
|
|
+ unsetSilentDetect,
|
|
|
zipPackage
|
|
|
)
|
|
|
);
|