UNPKG

@oeyoews/tiddlywiki-plugin-dev

Version:

[![](https://img.shields.io/badge/Join-TiddlyWiki_CN-blue)](https://github.com/tiddly-gittly)

423 lines (401 loc) 13.6 kB
/* eslint-disable max-lines */ import fs from 'fs'; import path from 'path'; import chalk from 'chalk'; import sha256 from 'sha256'; import esbuild from 'esbuild'; import { uniq } from 'lodash'; import UglifyJS from 'uglify-js'; import CleanCSS from 'clean-css'; import cliProgress from 'cli-progress'; import browserslist from 'browserslist'; import type { ITiddlerFields, ITiddlyWiki } from 'tw5-typed'; import postCssPlugin from 'esbuild-style-plugin'; import tailwindcss from 'tailwindcss'; import autoprefixer from 'autoprefixer'; import { esbuildPluginBrowserslist } from 'esbuild-plugin-browserslist'; import { walkFilesSync } from './utils'; const nodejsBuiltinModules = [ 'assert', 'buffer', 'child_process', 'cluster', 'crypto', 'dgram', 'dns', 'domain', 'events', 'fs', 'fsevents', 'http', 'https', 'net', 'os', 'path', 'punycode', 'querystring', 'readline', 'stream', 'string_decoder', 'timers', 'tls', 'tty', 'url', 'util', 'v8', 'vm', 'zlib', ]; const injectPath = path.resolve(__dirname, 'esbuild-inject.js'); const rootPath = process.cwd(); // 插件构建缓存 const pluginCache: Record<string, ITiddlerFields> = {}; const UglifyJSOption = { warnings: false, v8: true, ie: true, webkit: true, }; const cleanCSS = new CleanCSS({ compatibility: 'ie9', level: 2, }); const minifyTiddler = (tiddler: ITiddlerFields) => { const { text, type } = tiddler; try { if (type === 'application/javascript') { const minified = UglifyJS.minify(text, UglifyJSOption).code; if (minified !== undefined) { return { ...tiddler, text: minified, }; } } else if (type === 'text/css') { const minified = cleanCSS.minify(text).styles; if (minified !== undefined) { return { ...tiddler, text: minified, }; } } } catch (e) { console.error(e); console.error(`Failed to minify ${tiddler.title}.`); } return tiddler; }; export const rebuild = async ( $tw: ITiddlyWiki, pluginsDir: string, updatePaths: string[] = [], devMode = true, excludeFilter?: string, ): Promise<ITiddlerFields[]> => { const baseDir = path.resolve(pluginsDir); if (!fs.existsSync(baseDir)) { return []; } // Touch TailwindCss config file const tailwindConfigPath = path.resolve('.', 'tailwind.config.js'); if (!fs.existsSync(tailwindConfigPath)) { fs.writeFileSync( tailwindConfigPath, [ 'module.exports = {', " content: ['./src/**/*.{mjs,cjs,js,ts,jsx,tsx}'],", ' theme: { extend: {} },', ' plugins: [],', '};', ].join('\n'), 'utf-8', ); } // eslint-disable-next-line no-console console.log(chalk.green.bold('Compiling...')); const bar = new cliProgress.SingleBar( { format: `${chalk.green('{bar}')} {percentage}% | {plugin}`, stopOnComplete: true, }, cliProgress.Presets.shades_classic, ); const updateDirs = uniq( updatePaths .filter(file => file) .map(file => path.resolve(path.dirname(file))), ); const pluginDirs = fs .readdirSync(baseDir) .map(dirname => path.resolve(baseDir, dirname)) .filter(dir => fs.statSync(dir).isDirectory()); bar.start(pluginDirs.length, 0); const plugins = await Promise.all( pluginDirs.map(async (dir, index) => { bar.update(index, { plugin: path.basename(dir) }); // 检查插件是否被修改过,缓存 const update = !pluginCache.hasOwnProperty(dir) || updateDirs.length === 0 || updateDirs.some(updateDir => updateDir.startsWith(dir)); if (!update) { bar.update(index + 1, { plugin: path.basename(dir) }); return pluginCache[dir]; } // 读取非编译内容 if (!fs.existsSync(path.resolve(dir, 'plugin.info'))) { return undefined; } const plugin = $tw.loadPluginFolder(dir)!; // 过滤空插件 if (!plugin?.title) { return undefined; } // 筛选插件 if ( excludeFilter && $tw.wiki.filterTiddlers(`[[${plugin.title}]] +${excludeFilter}`) .length > 0 ) { return undefined; } // 编译选项 const browserslistStr = plugin['Modern.TiddlyDev#BrowsersList'] ?? '>0.25%, not ie 11, not op_mini all'; const externalModules = $tw.utils.parseStringArray( plugin['Modern.TiddlyDev#ExternalModules'] ?? '', ); const sourceMap = plugin['Modern.TiddlyDev#SourceMap']?.toLowerCase?.() === 'true'; const minifyPlugin = plugin['Modern.TiddlyDev#Minify']?.toLowerCase?.() !== 'false'; const tiddlers = JSON.parse(plugin.text).tiddlers as Record< string, ITiddlerFields >; // 删除之前可能存在于 Wiki 的同名插件,以免被旧的覆盖掉 $tw.wiki.deleteTiddler(plugin.title); // 过滤没有 .meta 且不带原信息的文件,这些文件的 title 都是其绝对路径,*nix/bsd(macos)是/.*, win是\w:/ Object.keys(tiddlers).forEach(title => { if (fs.existsSync(title) && !fs.existsSync(`${title}.meta`)) { delete tiddlers[title]; } }); // 检索编译入口 const entryPoints: string[] = []; const metaMap = new Map<string, ITiddlerFields>(); walkFilesSync(dir, filepath => { let meta = $tw.loadMetadataForFile(filepath); if (!meta) { return; } metaMap.set(filepath, meta); if ( ['.ts', '.tsx', '.cjs', '.mjs', '.jsx'].includes( path.extname(filepath).toLowerCase(), ) ) { if (meta['Modern.TiddlyDev#IncludeSource'] === 'true') { tiddlers[meta.title] = { ...meta, text: fs.readFileSync(filepath, 'utf-8'), 'module-type': undefined, }; if (meta['Modern.TiddlyDev#NoCompile'] !== 'true') { // 编译 + 保留源文件 entryPoints.push(filepath); const titlePath = meta.title.split('/'); const parts = titlePath[titlePath.length - 1].split('.'); if ( parts.length < 2 || parts[parts.length - 1].toLowerCase() === 'js' || !['ts', 'tsx', 'cjs', 'mjs', 'jsx'].includes( parts[parts.length - 1].toLowerCase(), ) ) { parts.push('js'); } else { parts[parts.length - 1] = 'js'; } titlePath[titlePath.length - 1] = parts.join('.'); meta = { ...meta, title: titlePath.join('/'), }; } else { // 不编译 + 保留原文件 // do nothing } } else { delete tiddlers[meta.title]; if (meta['Modern.TiddlyDev#NoCompile'] !== 'true') { // 编译 + 不保留原文件 entryPoints.push(filepath); const titlePath = meta.title.split('/'); const parts = titlePath[titlePath.length - 1].split('.'); if ( parts.length < 2 || !['js', 'ts', 'tsx', 'cjs', 'mjs', 'jsx'].includes( parts[parts.length - 1].toLowerCase(), ) ) { parts.push('js'); } else { parts[parts.length - 1] = 'js'; } titlePath[titlePath.length - 1] = parts.join('.'); meta = { ...meta, title: titlePath.join('/'), }; } else { // 不编译 + 不保留原文件 // do nothing } } } metaMap.set(filepath, meta); }); // 编译 const { outputFiles, metafile } = await esbuild.build({ entryPoints, bundle: true, // 为什么不用 ESbuild 的压缩:UglifyJS 的压缩效率更好 // 参考:https://github.com/privatenumber/minification-benchmarks minify: false, write: false, allowOverwrite: true, incremental: true, outdir: baseDir, outbase: baseDir, sourcemap: devMode || sourceMap ? 'inline' : false, // https://esbuild.github.io/api/#format format: 'cjs', // https://esbuild.github.io/api/#tree-shaking treeShaking: true, // https://esbuild.github.io/api/#platform platform: 'browser', // https://esbuild.github.io/api/#external external: ['$:/*', ...nodejsBuiltinModules, ...(externalModules ?? [])], inject: [injectPath], // https://esbuild.github.io/api/#analyze metafile: true, banner: { js: '/* Compiled by Modern.TiddlyDev: https://github.com/tiddly-gittly/Modern.TiddlyDev */', css: '/* Compiled by Modern.TiddlyDev: https://github.com/tiddly-gittly/Modern.TiddlyDev */', }, loader: { '.png': 'dataurl', '.woff': 'dataurl', '.woff2': 'dataurl', '.eot': 'dataurl', '.ttf': 'dataurl', '.svg': 'dataurl', }, plugins: [ // http://browserl.ist/?q=%3E0.25%25%2C+not+ie+11%2C+not+op_mini+all esbuildPluginBrowserslist(browserslist(browserslistStr), { printUnknownTargets: false, }), postCssPlugin({ postcss: { plugins: [tailwindcss as any, autoprefixer as any], }, }), ], }); // 格式化并保存编译结果 outputFiles.forEach(file => { // esbuild 的 matadata 路径无论是 windows 还是 POSIX 都是以 / 为分隔符,因此要额外处理 const output = metafile!.outputs[ path.relative(rootPath, file.path).split(path.sep).join('/') ]; let meta: ITiddlerFields = {} as any; if (output.entryPoint) { // 入口,一定是源代码文件 const resolved = path.resolve(output.entryPoint); const relatived = path.relative(dir, output.entryPoint); if (metaMap.has(resolved)) { meta = { ...metaMap.get(resolved)!, type: 'application/javascript', 'Modern.TiddlyDev#Origin': relatived, }; } else { // 应该不存在这种情况 return; } } else { // 不是入口却被导出了,说明是资源文件 const name = Object.keys(output.inputs)[0]; if (name) { const resolved = path.resolve(name); const relatived = path.relative(dir, name); const type = $tw.config.fileExtensionInfo[ path.extname(file.path).toLowerCase() ]?.type ?? ''; meta = { title: '', tags: type === 'text/css' ? ['$:/tags/Stylesheet'] : [], ...(metaMap.get(resolved) ?? {}), type, 'Modern.TiddlyDev#Origin': relatived, } as ITiddlerFields; } if (!meta.title) { const parsed = path.parse(path.relative(dir, file.path)); const tmp = path.join(plugin.title, parsed.dir, parsed.name); if (tiddlers.hasOwnProperty(`${tmp}${parsed.ext}`)) { let id = 1; while (tiddlers.hasOwnProperty(`${tmp}${id}${parsed.ext}`)) { id++; } (meta as any).title = tiddlers.hasOwnProperty( `${tmp}${id}${parsed.ext}`, ); } else { (meta as any).title = `${tmp}${parsed.ext}`; } } } tiddlers[meta.title] = { ...meta, text: file.text, }; }); // 最小化 if (!devMode && minifyPlugin) { Object.keys(tiddlers).forEach(title => { if (tiddlers[title]['Modern.TiddlyDev#Minify'] !== 'false') { tiddlers[title] = minifyTiddler(tiddlers[title]); } }); } // 得到的字段要按字典序排序,保证哈希一致性 const t = { ...plugin, text: JSON.stringify({ tiddlers }), }; pluginCache[dir] = {} as unknown as ITiddlerFields; for (const key of Object.keys(t).sort()) { (pluginCache[dir] as any)[key] = (t as any)[key]; } // 哈希校验 if (!devMode) { (pluginCache[dir] as any)['Modern.TiddlyDev#SHA256-Hashed'] = sha256( JSON.stringify(pluginCache[dir]), ); } bar.update(index + 1, { plugin: path.basename(dir) }); return pluginCache[dir]; }), ); // eslint-disable-next-line no-console console.log(''); return plugins.filter(plugin => plugin !== undefined) as ITiddlerFields[]; }; /* eslint-enable max-lines */