feat: replace vite with esbuild

This commit is contained in:
Sidharth Vinod
2023-02-19 17:28:35 +05:30
parent f48e48d5fd
commit a0d9069eb0
16 changed files with 182 additions and 316 deletions

31
.esbuild/build.ts Normal file
View File

@@ -0,0 +1,31 @@
import { build } from 'esbuild';
import { getBuildConfig, packageOptions } from './util.js';
import { context } from 'esbuild';
const shouldWatch = process.argv.includes('--watch');
const buildPackage = async (entryName: keyof typeof packageOptions) => {
await build(getBuildConfig({ entryName, minify: false }));
await build(getBuildConfig({ entryName, minify: true }));
await build(getBuildConfig({ entryName, minify: false, core: true }));
};
const handler = (e) => {
console.error(e);
process.exit(1);
};
const main = async () => {
const packageNames = Object.keys(packageOptions) as (keyof typeof packageOptions)[];
for (const pkg of packageNames) {
await buildPackage(pkg).catch(handler);
}
};
const watch = async () => {
const ctx = await context(getBuildConfig({ entryName: 'mermaid', minify: false }));
ctx.watch();
console.log('Watching for changes');
};
void (shouldWatch ? watch : main)();

View File

@@ -1,23 +1,21 @@
import express from 'express';
import cors from 'cors';
import { createServer as createViteServer } from 'vite';
import proxy from 'express-http-proxy';
import { getBuildConfig } from './util.js';
import { context } from 'esbuild';
async function createServer() {
const app = express();
// Create Vite server in middleware mode
const vite = await createViteServer({
configFile: './vite.config.ts',
server: { middlewareMode: true },
appType: 'custom', // don't include Vite's default HTML handling middlewares
});
const config = getBuildConfig({ minify: false, core: false, entryName: 'mermaid' });
const ctx = await context(config);
ctx.watch();
let { host, port } = await ctx.serve({ servedir: './dist' });
app.use(cors());
app.use(express.static('./packages/mermaid/dist'));
app.use(express.static('./packages/mermaid-example-diagram/dist'));
app.use(vite.middlewares);
app.use(express.static('demos'));
app.use(express.static('cypress/platform'));
app.use('/', proxy(`http://${host}:${port}`));
app.listen(9000, () => {
console.log(`Listening on http://localhost:9000`);

89
.esbuild/util.ts Normal file
View File

@@ -0,0 +1,89 @@
import { resolve } from 'path';
import { fileURLToPath } from 'url';
import { BuildOptions } from 'esbuild';
import { readFileSync } from 'fs';
import { readFile } from 'fs/promises';
import { transformJison } from './jisonTransformer.js';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
export const packageOptions = {
mermaid: {
name: 'mermaid',
packageName: 'mermaid',
file: 'mermaid.ts',
},
'mermaid-example-diagram': {
name: 'mermaid-example-diagram',
packageName: 'mermaid-example-diagram',
file: 'detector.ts',
},
};
interface MermaidBuildOptions {
minify: boolean;
core?: boolean;
entryName: keyof typeof packageOptions;
}
const buildOptions = (override: BuildOptions): BuildOptions => {
return {
bundle: true,
splitting: true,
minify: true,
keepNames: true,
banner: { js: '"use strict";' },
format: 'esm',
platform: 'browser',
tsconfig: 'tsconfig.json',
resolveExtensions: ['.ts', '.js', '.json', '.jison'],
external: ['require', 'fs', 'path'],
outdir: 'dist',
plugins: [jisonPlugin],
sourcemap: 'external',
outExtension: { '.js': '.mjs' },
...override,
};
};
const jisonPlugin = {
name: 'jison',
setup(build) {
build.onLoad({ filter: /\.jison$/ }, async (args) => {
// Load the file from the file system
const source = await readFile(args.path, 'utf8');
const contents = transformJison(source);
return { contents, warnings: [] };
});
},
};
export const getBuildConfig = ({ minify, core, entryName }: MermaidBuildOptions): BuildOptions => {
const external: string[] = ['require', 'fs', 'path'];
const { name, file, packageName } = packageOptions[entryName];
let output: BuildOptions = buildOptions({
absWorkingDir: resolve(__dirname, `../packages/${packageName}`),
entryPoints: {
[`${name}.esm${core ? '.core' : ''}${minify ? '.min' : ''}`]: `src/${file}`,
},
});
if (core) {
const { dependencies } = JSON.parse(
readFileSync(resolve(__dirname, `../packages/${packageName}/package.json`), 'utf-8')
);
// Core build is used to generate file without bundled dependencies.
// This is used by downstream projects to bundle dependencies themselves.
// Ignore dependencies and any dependencies of dependencies
external.push(...Object.keys(dependencies));
output.external = external;
}
// if (watch && config.build) {
// config.build.watch = {
// include: ['packages/mermaid-example-diagram/src/**', 'packages/mermaid/src/**'],
// };
// }
return output;
};

View File

@@ -1,142 +0,0 @@
import { build, InlineConfig, type PluginOption } from 'vite';
import { resolve } from 'path';
import { fileURLToPath } from 'url';
import jisonPlugin from './jisonPlugin.js';
import { readFileSync } from 'fs';
import { visualizer } from 'rollup-plugin-visualizer';
import type { TemplateType } from 'rollup-plugin-visualizer/dist/plugin/template-types.js';
const visualize = process.argv.includes('--visualize');
const watch = process.argv.includes('--watch');
const mermaidOnly = process.argv.includes('--mermaid');
const __dirname = fileURLToPath(new URL('.', import.meta.url));
type OutputOptions = Exclude<
Exclude<InlineConfig['build'], undefined>['rollupOptions'],
undefined
>['output'];
const visualizerOptions = (packageName: string, core = false): PluginOption[] => {
if (packageName !== 'mermaid' || !visualize) {
return [];
}
return ['network', 'treemap', 'sunburst'].map(
(chartType) =>
visualizer({
filename: `./stats/${chartType}${core ? '.core' : ''}.html`,
template: chartType as TemplateType,
gzipSize: true,
brotliSize: true,
}) as PluginOption
);
};
const packageOptions = {
mermaid: {
name: 'mermaid',
packageName: 'mermaid',
file: 'mermaid.ts',
},
'mermaid-example-diagram': {
name: 'mermaid-example-diagram',
packageName: 'mermaid-example-diagram',
file: 'detector.ts',
},
};
interface BuildOptions {
minify: boolean | 'esbuild';
core?: boolean;
watch?: boolean;
entryName: keyof typeof packageOptions;
}
export const getBuildConfig = ({ minify, core, watch, entryName }: BuildOptions): InlineConfig => {
const external: (string | RegExp)[] = ['require', 'fs', 'path'];
console.log(entryName, packageOptions[entryName]);
const { name, file, packageName } = packageOptions[entryName];
let output: OutputOptions = [
{
name,
format: 'esm',
sourcemap: true,
entryFileNames: `${name}.esm${minify ? '.min' : ''}.mjs`,
},
];
if (core) {
const { dependencies } = JSON.parse(
readFileSync(resolve(__dirname, `../packages/${packageName}/package.json`), 'utf-8')
);
// Core build is used to generate file without bundled dependencies.
// This is used by downstream projects to bundle dependencies themselves.
// Ignore dependencies and any dependencies of dependencies
// Adapted from the RegEx used by `rollup-plugin-node`
external.push(new RegExp('^(?:' + Object.keys(dependencies).join('|') + ')(?:/.+)?$'));
// This needs to be an array. Otherwise vite will build esm & umd with same name and overwrite esm with umd.
output = [
{
name,
format: 'esm',
sourcemap: true,
entryFileNames: `${name}.core.mjs`,
},
];
}
const config: InlineConfig = {
configFile: false,
build: {
emptyOutDir: false,
outDir: resolve(__dirname, `../packages/${packageName}/dist`),
lib: {
entry: resolve(__dirname, `../packages/${packageName}/src/${file}`),
name,
// the proper extensions will be added
fileName: name,
},
minify,
rollupOptions: {
external,
output,
},
},
resolve: {
extensions: ['.jison', '.js', '.ts', '.json'],
},
plugins: [jisonPlugin(), ...visualizerOptions(packageName, core)],
};
if (watch && config.build) {
config.build.watch = {
include: ['packages/mermaid-example-diagram/src/**', 'packages/mermaid/src/**'],
};
}
return config;
};
const buildPackage = async (entryName: keyof typeof packageOptions) => {
await build(getBuildConfig({ minify: false, entryName }));
await build(getBuildConfig({ minify: 'esbuild', entryName }));
await build(getBuildConfig({ minify: false, core: true, entryName }));
};
const main = async () => {
const packageNames = Object.keys(packageOptions) as (keyof typeof packageOptions)[];
for (const pkg of packageNames.filter((pkg) => !mermaidOnly || pkg === 'mermaid')) {
await buildPackage(pkg);
}
};
if (watch) {
build(getBuildConfig({ minify: false, watch, core: false, entryName: 'mermaid' }));
if (!mermaidOnly) {
build(getBuildConfig({ minify: false, watch, entryName: 'mermaid-example-diagram' }));
}
} else if (visualize) {
await build(getBuildConfig({ minify: false, core: true, entryName: 'mermaid' }));
await build(getBuildConfig({ minify: false, core: false, entryName: 'mermaid' }));
} else {
void main();
}

View File

@@ -1,4 +1,4 @@
import { transformJison } from './jisonTransformer.js';
import { transformJison } from '../.esbuild/jisonTransformer.js';
const fileRegex = /\.(jison)$/;
export default function jison() {

View File

@@ -188,7 +188,7 @@ mindmap
//import mindmap from '../../packages/mermaid-mindmap/src/detector';
// import example from '../../packages/mermaid-example-diagram/src/detector';
// import timeline from '../../packages/mermaid-timeline/src/detector';
import mermaid from '../../packages/mermaid/src/mermaid';
import mermaid from './mermaid.esm.mjs';
// await mermaid.registerExternalDiagrams([]);
mermaid.parseError = function (err, hash) {
// console.error('Mermaid error: ', err);

View File

@@ -13,7 +13,7 @@ example-diagram
<script type="module">
import exampleDiagram from '../../packages/mermaid-example-diagram/src/detector';
// import example from '../../packages/mermaid-example-diagram/src/detector';
import mermaid from '../../packages/mermaid/src/mermaid';
import mermaid from './mermaid.esm.mjs';
await mermaid.registerExternalDiagrams([exampleDiagram]);
await mermaid.initialize({ logLevel: 0 });

View File

@@ -240,7 +240,7 @@ sequenceDiagram
</pre>
<script type="module">
import mermaid from '../../packages/mermaid/src/mermaid';
import mermaid from './mermaid.esm.mjs';
mermaid.parseError = function (err, hash) {
// console.error('Mermaid error: ', err);
};

View File

@@ -40,7 +40,6 @@
<script type="module">
import mermaid from './mermaid.esm.mjs';
import mermaidMindmap from './mermaid-mindmap.esm.mjs';
const ALLOWED_TAGS = [
'a',
@@ -82,7 +81,6 @@
mermaid.parseError = function (err, hash) {
// console.error('Mermaid error: ', err);
};
await mermaid.registerExternalDiagrams([mermaidMindmap]);
mermaid.initialize({
theme: 'base',
startOnLoad: true,

View File

@@ -37,7 +37,7 @@
</pre>
<script type="module">
import mermaid from '../packages/mermaid';
import mermaid from '/mermaid.esm.mjs';
mermaid.initialize({
theme: 'forest',
// themeCSS: '.node rect { fill: red; }',

View File

@@ -15,13 +15,13 @@
"git graph"
],
"scripts": {
"build:vite": "ts-node-esm --transpileOnly .vite/build.ts",
"build:mermaid": "pnpm build:vite --mermaid",
"build:esbuild": "pnpm run -r clean && ts-node-esm --transpileOnly .esbuild/build.ts",
"build:mermaid": "pnpm build:esbuild --mermaid",
"build:viz": "pnpm build:mermaid --visualize",
"build:types": "tsc -p ./packages/mermaid/tsconfig.json --emitDeclarationOnly && tsc -p ./packages/mermaid-example-diagram/tsconfig.json --emitDeclarationOnly",
"build:watch": "pnpm build:vite --watch",
"build": "pnpm run -r clean && pnpm build:types && pnpm build:vite",
"dev": "concurrently \"pnpm build:vite --watch\" \"ts-node-esm .vite/server.ts\"",
"build:watch": "pnpm build:esbuild --watch",
"build": "pnpm run -r clean && pnpm build:types && pnpm build:esbuild",
"dev": "concurrently \"pnpm build:esbuild --watch\" \"ts-node-esm --transpileOnly .esbuild/server.ts\"",
"release": "pnpm build",
"lint": "eslint --cache --cache-strategy content --ignore-path .gitignore . && pnpm lint:jison && prettier --cache --check .",
"lint:fix": "eslint --cache --cache-strategy content --fix --ignore-path .gitignore . && prettier --write . && ts-node-esm scripts/fixCSpell.ts",
@@ -61,6 +61,7 @@
"@types/cors": "^2.8.13",
"@types/eslint": "^8.4.10",
"@types/express": "^4.17.17",
"@types/express-http-proxy": "^1.6.3",
"@types/js-yaml": "^4.0.5",
"@types/jsdom": "^21.0.0",
"@types/lodash": "^4.14.188",
@@ -73,6 +74,7 @@
"@vitest/coverage-c8": "^0.28.4",
"@vitest/spy": "^0.28.4",
"@vitest/ui": "^0.28.4",
"chokidar": "^3.5.3",
"concurrently": "^7.5.0",
"cors": "^2.8.5",
"coveralls": "^3.1.1",
@@ -92,6 +94,7 @@
"eslint-plugin-tsdoc": "^0.2.17",
"eslint-plugin-unicorn": "^45.0.0",
"express": "^4.18.2",
"express-http-proxy": "^1.6.3",
"globby": "^13.1.2",
"husky": "^8.0.2",
"jest": "^29.3.1",

View File

@@ -1,15 +0,0 @@
const { esmBuild, esmCoreBuild } = require('./util.cjs');
const { build } = require('esbuild');
const handler = (e) => {
console.error(e);
process.exit(1);
};
const watch = process.argv.includes('--watch');
// mermaid.esm.mjs
build(esmBuild({ minify: false })).catch(handler);
// mermaid.esm.min.mjs
build(esmBuild({ minify: true })).catch(handler);
// mermaid.core.mjs (node_modules unbundled)
build(esmCoreBuild()).catch(handler);

View File

@@ -1,53 +0,0 @@
const esbuild = require('esbuild');
const http = require('http');
const path = require('path');
const { iifeBuild } = require('./util.cjs');
// Start esbuild's server on a random local port
esbuild
.serve(
{
servedir: path.join(__dirname, '..'),
},
iifeBuild({ minify: false })
)
.then((result) => {
// The result tells us where esbuild's local server is
const { host, port } = result;
// Then start a proxy server on port 3000
http
.createServer((req, res) => {
if (req.url.includes('mermaid.js')) {
req.url = '/dist/mermaid.js';
}
const options = {
hostname: host,
port: port,
path: req.url,
method: req.method,
headers: req.headers,
};
// Forward each incoming request to esbuild
const proxyReq = http.request(options, (proxyRes) => {
// If esbuild returns "not found", send a custom 404 page
console.error('pp', req.url);
if (proxyRes.statusCode === 404) {
if (!req.url.endsWith('.html')) {
res.writeHead(404, { 'Content-Type': 'text/html' });
res.end('<h1>A custom 404 page</h1>');
return;
}
}
// Otherwise, forward the response from esbuild to the client
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res, { end: true });
});
// Forward the body of the request to esbuild
req.pipe(proxyReq, { end: true });
})
.listen(3000);
});

View File

@@ -1,84 +0,0 @@
const { Generator } = require('jison');
const fs = require('fs');
const { dependencies } = require('../package.json');
/** @typedef {import('esbuild').BuildOptions} Options */
/**
* @param {Options} override
* @returns {Options}
*/
const buildOptions = (override = {}) => {
return {
bundle: true,
splitting: true,
minify: true,
keepNames: true,
banner: { js: '"use strict";' },
globalName: 'mermaid',
platform: 'browser',
tsconfig: 'tsconfig.json',
resolveExtensions: ['.ts', '.js', '.json', '.jison'],
external: ['require', 'fs', 'path'],
outdir: 'dist',
plugins: [jisonPlugin],
sourcemap: 'external',
...override,
};
};
const getOutFiles = (extension) => {
return {
[`mermaid${extension}`]: 'src/mermaid.ts',
};
};
/**
* Build options for mermaid.esm.* build.
*
* For ESM browser use.
*
* @param {Options} override - Override options.
* @returns {Options} ESBuild build options.
*/
exports.esmBuild = (override = { minify: true }) => {
return buildOptions({
format: 'esm',
entryPoints: getOutFiles(`.esm${override.minify ? '.min' : ''}`),
outExtension: { '.js': '.mjs' },
...override,
});
};
/**
* Build options for mermaid.core.* build.
*
* This build does not bundle `./node_modules/`, as it is designed to be used with
* Webpack/ESBuild/Vite to use mermaid inside an app/website.
*
* @param {Options} override - Override options.
* @returns {Options} ESBuild build options.
*/
exports.esmCoreBuild = (override) => {
return buildOptions({
format: 'esm',
entryPoints: getOutFiles(`.core`),
outExtension: { '.js': '.mjs' },
external: ['require', 'fs', 'path', ...Object.keys(dependencies)],
platform: 'neutral',
...override,
});
};
const jisonPlugin = {
name: 'jison',
setup(build) {
build.onLoad({ filter: /\.jison$/ }, async (args) => {
// Load the file from the file system
const source = await fs.promises.readFile(args.path, 'utf8');
const contents = new Generator(source, { 'token-stack': true }).generate({
moduleMain: '() => {}', // disable moduleMain (default one requires Node.JS modules)
});
return { contents, warnings: [] };
});
},
};

41
pnpm-lock.yaml generated
View File

@@ -25,6 +25,9 @@ importers:
'@types/express':
specifier: ^4.17.17
version: 4.17.17
'@types/express-http-proxy':
specifier: ^1.6.3
version: 1.6.3
'@types/js-yaml':
specifier: ^4.0.5
version: 4.0.5
@@ -61,6 +64,9 @@ importers:
'@vitest/ui':
specifier: ^0.28.4
version: 0.28.4
chokidar:
specifier: ^3.5.3
version: 3.5.3
concurrently:
specifier: ^7.5.0
version: 7.5.0
@@ -118,6 +124,9 @@ importers:
express:
specifier: ^4.18.2
version: 4.18.2
express-http-proxy:
specifier: ^1.6.3
version: 1.6.3
globby:
specifier: ^13.1.2
version: 13.1.2
@@ -2882,6 +2891,12 @@ packages:
resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==}
dev: true
/@types/express-http-proxy/1.6.3:
resolution: {integrity: sha512-dX3+Cb0HNPtqhC5JUWzzuODHRlgJRZx7KvwKVVwkOvm+8vOtpsh3qy8+qLv5X1hs4vdVHWKyXf86DwJot5H8pg==}
dependencies:
'@types/express': 4.17.17
dev: true
/@types/express-serve-static-core/4.17.31:
resolution: {integrity: sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==}
dependencies:
@@ -5736,6 +5751,17 @@ packages:
ms: 2.0.0
dev: true
/debug/3.2.7:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.1.3
dev: true
/debug/3.2.7_supports-color@8.1.1:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies:
@@ -6092,6 +6118,10 @@ packages:
resolution: {integrity: sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==}
dev: true
/es6-promise/4.2.8:
resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==}
dev: true
/esbuild/0.16.17:
resolution: {integrity: sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==}
engines: {node: '>=12'}
@@ -6614,6 +6644,17 @@ packages:
jest-util: 29.3.1
dev: true
/express-http-proxy/1.6.3:
resolution: {integrity: sha512-/l77JHcOUrDUX8V67E287VEUQT0lbm71gdGVoodnlWBziarYKgMcpqT7xvh/HM8Jv52phw8Bd8tY+a7QjOr7Yg==}
engines: {node: '>=6.0.0'}
dependencies:
debug: 3.2.7
es6-promise: 4.2.8
raw-body: 2.5.1
transitivePeerDependencies:
- supports-color
dev: true
/express/4.18.2:
resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==}
engines: {node: '>= 0.10.0'}