move building to esbuild

change public to dist
This commit is contained in:
Vitaly 2023-08-28 06:37:57 +03:00
commit fc7869000e
14 changed files with 421 additions and 30 deletions

12
.vscode/launch.json vendored
View file

@ -8,9 +8,9 @@
"name": "Attach Chrome",
"request": "attach",
"outFiles": [
"${workspaceFolder}/public/**/*.js",
// "!${workspaceFolder}/public/**/*vendors*",
"!${workspaceFolder}/public/**/*minecraftData*",
"${workspaceFolder}/dist/**/*.js",
// "!${workspaceFolder}/dist/**/*vendors*",
"!${workspaceFolder}/dist/**/*minecraftData*",
"!**/node_modules/**"
],
"skipFiles": [
@ -26,9 +26,9 @@
"request": "launch",
"url": "http://localhost:8080/",
"outFiles": [
"${workspaceFolder}/public/**/*.js",
// "!${workspaceFolder}/public/**/*vendors*",
"!${workspaceFolder}/public/**/*minecraftData*",
"${workspaceFolder}/dist/**/*.js",
// "!${workspaceFolder}/dist/**/*vendors*",
"!${workspaceFolder}/dist/**/*minecraftData*",
"!**/node_modules/**"
],
"skipFiles": [

125
esbuild.mjs Normal file
View file

@ -0,0 +1,125 @@
//@ts-check
import * as esbuild from 'esbuild'
import fs from 'fs'
// import htmlPlugin from '@chialab/esbuild-plugin-html'
import server from './server.js'
import { analyzeMetafile } from 'esbuild'
import { clients, plugins } from './scripts/esbuildPlugins.mjs'
import { generateSW } from 'workbox-build'
import { getSwAdditionalEntries } from './scripts/build.js'
/** @type {import('esbuild').BuildOptions} */
let baseConfig = {}
// // legacy html config
// baseConfig = {
// entryPoints: ['index.html'],
// assetNames: 'assets/[name]',
// chunkNames: '[ext]/[name]',
// outdir: 'dist',
// outfile: undefined,
// plugins: [
// htmlPlugin({
// scriptsTarget: 'esnext',
// })
// ],
// }
// // testing config
// baseConfig = {
// entryPoints: ['files/index.js'],
// outfile: 'out.js',
// outdir: undefined,
// }
fs.copyFileSync('index.html', 'dist/index.html')
fs.writeFileSync('dist/index.html', fs.readFileSync('dist/index.html', 'utf8').replace('<!-- inject script -->', '<script src="index.js"></script>'), 'utf8')
const banner = [
'window.global = globalThis;',
// auto-reload
'(() => new EventSource("/esbuild").onmessage = ({ data: _data }) => {const data = JSON.parse(_data);if (!data.update)return;sessionStorage.lastReload = JSON.stringify({buildTime:data.update.time, reloadStart:Date.now()});location.reload()})();'
]
const buildingVersion = new Date().toISOString().split(':')[0]
const ctx = await esbuild.context({
bundle: true,
entryPoints: ['src/index.js'],
logLevel: 'info',
platform: 'browser',
sourcemap: true,
outdir: 'dist',
mainFields: [
'browser', 'module', 'main'
],
keepNames: true,
...baseConfig,
banner: {
js: banner.join('\n')
},
alias: {
events: 'events', // make explicit
buffer: 'buffer',
'fs': 'browserfs/dist/shims/fs.js',
http: 'http-browserify',
perf_hooks: './src/perf_hooks_replacement.js',
crypto: './src/crypto.js',
stream: 'stream-browserify',
net: 'net-browserify',
dns: './src/dns.js'
},
inject: [
'./src/shims.js'
],
metafile: true,
plugins: [
...plugins,
...baseConfig.plugins ?? [],
],
minify: process.argv.includes('--minify'),
define: {
'process.env.BUILD_VERSION': process.argv.includes('--watch') ? undefined : JSON.stringify(buildingVersion),
'process.env.GITHUB_URL':
JSON.stringify(`https://github.com/${process.env.GITHUB_REPOSITORY || `${process.env.VERCEL_GIT_REPO_OWNER}/${process.env.VERCEL_GIT_REPO_SLUG}`}`)
},
// chunkNames: '[name]',
})
if (process.argv.includes('--watch')) {
await ctx.watch()
server.app.get('/esbuild', (req, res, next) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
})
// Send a comment to keep the connection alive
res.write(': ping\n\n')
// Add the client response to the clients array
clients.push(res)
// Handle any client disconnection logic
res.on('close', () => {
const index = clients.indexOf(res)
if (index !== -1) {
clients.splice(index, 1)
}
})
})
} else {
fs.writeFileSync('dist/version.txt', buildingVersion, 'utf-8')
const result = await ctx.rebuild()
// console.log(await analyzeMetafile(result.metafile))
const { count, size, warnings } = await generateSW({
// dontCacheBustURLsMatching: [new RegExp('...')],
globDirectory: 'dist',
additionalManifestEntries: getSwAdditionalEntries(),
swDest: 'dist/service-worker.js',
})
await ctx.dispose()
}

View file

@ -2,7 +2,7 @@
<html>
<head>
<title>Prismarine Web Client</title>
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="index.css">
<link rel="favicon" href="favicon.ico">
<link rel="icon" type="image/png" href="favicon.png" />
<meta name="description" content="Minecraft web client running in your browser">
@ -30,5 +30,6 @@
<pmui-notification></pmui-notification>
<context-menu id="context-menu"></context-menu>
</div>
<!-- inject script -->
</body>
</html>

View file

@ -3,8 +3,9 @@
"version": "1.5.0",
"description": "A minecraft client running in a browser",
"scripts": {
"start": "NODE_OPTIONS=--max-old-space-size=8192 run-p watch prod-start",
"build": "NODE_OPTIONS=--max-old-space-size=8192 rimraf public && node scripts/build.js copyFiles && webpack --config webpack.prod.js --progress",
"start": "node scripts/build.js copyFilesDev && node esbuild.mjs --minify --watch",
"start-watch-script": "nodemon -w esbuild.mjs esbuild.mjs",
"build": "node scripts/build.js copyFiles && node esbuild.mjs --minify --build-only",
"watch": "node scripts/build.js copyFilesDev && webpack serve --config webpack.dev.js --progress",
"postinstall": "node scripts/patchPackages.js",
"test:cypress": "cypress run",
@ -32,12 +33,14 @@
"compression": "^1.7.4",
"cypress-plugin-snapshots": "^1.4.4",
"diamond-square": "^1.2.0",
"esbuild": "^0.19.2",
"esbuild-loader": "^4.0.0",
"esbuild-plugin-polyfill-node": "^0.3.0",
"express": "^4.18.2",
"fs-extra": "^11.1.1",
"iconify-icon": "^1.0.8",
"lit": "^2.8.0",
"minecraft-data": "^3.0.0",
"net-browserify": "github:PrismarineJS/net-browserify",
"prismarine-world": "^3.6.2",
"querystring": "^0.2.1",
@ -48,7 +51,8 @@
"speed-measure-webpack-plugin": "^1.5.0",
"stats.js": "^0.17.0",
"url": "^0.11.1",
"valtio": "^1.11.1"
"valtio": "^1.11.1",
"workbox-build": "^7.0.0"
},
"devDependencies": {
"@types/three": "0.128.0",
@ -62,6 +66,7 @@
"cypress": "^9.5.4",
"cypress-esbuild-preprocessor": "^1.0.2",
"events": "^3.3.0",
"filesize": "^10.0.12",
"html-webpack-plugin": "^5.5.3",
"http-browserify": "^1.7.0",
"http-server": "^14.1.1",

View file

@ -10,7 +10,12 @@ const THREE = require('three')
const path = require('path')
const textureCache = {}
// todo not ideal, export different functions for browser and node
function loadTexture (texture, cb) {
if (process.platform === 'browser') {
return require('./utils.web').loadTexture(texture, cb)
}
if (textureCache[texture]) {
cb(textureCache[texture])
} else {
@ -22,6 +27,9 @@ function loadTexture (texture, cb) {
}
function loadJSON (json, cb) {
if (process.platform === 'browser') {
return require('./utils.web').loadJSON(json, cb)
}
cb(require(path.resolve(__dirname, '../../public/' + json)))
}

View file

@ -11,22 +11,25 @@ const filesAlwaysToCopy = [
]
// these files could be copied at build time eg with copy plugin, but copy plugin slows down the config (2x in my testing, sometimes with too many open files error) is slow so we also copy them there
const webpackFilesToCopy = [
{ from: './node_modules/prismarine-viewer2/public/blocksStates/', to: 'public/blocksStates/' },
{ from: './node_modules/prismarine-viewer2/public/textures/', to: 'public/textures/' },
{ from: './node_modules/prismarine-viewer2/public/worker.js', to: 'public/worker.js' },
{ from: './node_modules/prismarine-viewer2/public/supportedVersions.json', to: 'public/supportedVersions.json' },
{ from: './assets/', to: './public/' },
{ from: './config.json', to: 'public/config.json' }
{ from: './node_modules/prismarine-viewer2/public/blocksStates/', to: 'dist/blocksStates/' },
{ from: './node_modules/prismarine-viewer2/public/textures/', to: 'dist/textures/' },
{ from: './node_modules/prismarine-viewer2/public/worker.js', to: 'dist/worker.js' },
{ from: './node_modules/prismarine-viewer2/public/supportedVersions.json', to: 'dist/supportedVersions.json' },
{ from: './assets/', to: './dist/' },
{ from: './config.json', to: 'dist/config.json' }
]
exports.webpackFilesToCopy = webpackFilesToCopy
exports.copyFiles = (isDev = false) => {
console.time('copy files');
[...filesAlwaysToCopy, ...webpackFilesToCopy].forEach(file => {
fsExtra.copySync(file.from, file.to)
})
console.timeEnd('copy files')
}
exports.copyFilesDev = () => {
if (fsExtra.existsSync('public/config.json')) return
if (fsExtra.existsSync('dist/config.json')) return
exports.copyFiles(true)
}
@ -34,6 +37,9 @@ exports.getSwAdditionalEntries = () => {
// need to be careful with this
const singlePlayerVersion = defaultLocalServerOptions.version
const filesToCachePatterns = [
'index.js',
'index.css',
'favicon.ico',
`blocksStates/${singlePlayerVersion}.json`,
'extra-textures/**',
// todo-low copy from assets
@ -59,9 +65,9 @@ exports.getSwAdditionalEntries = () => {
const output = []
console.log('Generating sw additional entries...')
for (const pattern of filesToCachePatterns) {
const files = glob.sync(pattern, { cwd: 'public' })
const files = glob.sync(pattern, { cwd: 'dist' })
for (const file of files) {
const fullPath = path.join('public', file)
const fullPath = path.join('dist', file)
if (!fs.lstatSync(fullPath).isFile()) continue
let revision = null
const url = './' + file.replace(/\\/g, '/')

229
scripts/esbuildPlugins.mjs Normal file
View file

@ -0,0 +1,229 @@
//@ts-check
import { polyfillNode } from 'esbuild-plugin-polyfill-node'
import { join, dirname } from 'path'
import * as fs from 'fs'
import { filesize } from 'filesize'
let clients = []
/** @type {import('esbuild').Plugin[]} */
const plugins = [
{
name: 'strict-aliases',
setup (build) {
build.onResolve({
filter: /^minecraft-protocol$/,
}, async ({ kind, resolveDir }) => {
return {
path: (await build.resolve('minecraft-protocol/src/index.js', { kind, resolveDir })).path,
}
})
// build.onResolve({
// filter: /^\.\/data.js$/,
// }, ({ resolveDir, path }) => {
// if (!resolveDir.endsWith('minecraft-data')) return
// return {
// namespace: 'load-global-minecraft-data',
// path
// }
// })
// build.onLoad({
// filter: /.+/,
// namespace: 'load-global-minecraft-data',
// }, () => {
// return {
// contents: 'module.exports = window.minecraftData',
// loader: 'js',
// }
// })
// build.onResolve({
// filter: /^minecraft-assets$/,
// }, ({ resolveDir, path }) => {
// // if (!resolveDir.endsWith('minecraft-data')) return
// return {
// namespace: 'load-global-minecraft-assets',
// path
// }
// })
// build.onLoad({
// filter: /.+/,
// namespace: 'load-global-minecraft-assets',
// }, async () => {
// const resolvedPath = await build.resolve('minecraft-assets/index.js', { kind: 'require-call', resolveDir: process.cwd() })
// let contents = (await fs.promises.readFile(resolvedPath.path, 'utf8'))
// contents = contents.slice(0, contents.indexOf('const data = ')) + 'const data = window.minecraftAssets;' + contents.slice(contents.indexOf('module.exports.versions'))
// return {
// contents,
// loader: 'js',
// resolveDir: dirname(resolvedPath.path),
// }
// })
}
},
{
name: 'assets-resolve',
setup (build) {
build.onResolve({
filter: /.*/,
}, async ({ path, ...rest }) => {
if (['.woff', '.woff2', '.ttf', '.png', '.jpg', '.jpeg', '.gif', '.svg'].some(ext => path.endsWith(ext))) {
return {
path,
namespace: 'assets',
external: true,
}
}
})
build.onEnd(({ metafile, outputFiles }) => {
// top 5 biggest deps
//@ts-ignore
// const deps = Object.entries(metafile.inputs).sort(([, a], [, b]) => b.bytes - a.bytes).map(([x, { bytes }]) => [x, filesize(bytes)]).slice(0, 5)
// console.log(deps)
})
},
},
{
name: 'prevent-incorrect-linking',
setup (build) {
build.onResolve({
filter: /.+/,
}, ({ resolveDir }) => {
// disallow imports from outside the root directory to ensure modules are resolved from node_modules of this workspace
if (!resolveDir.startsWith(process.cwd())) {
throw new Error(`Restricted import from outside the root directory: ${resolveDir}`)
}
return undefined
})
}
},
{
name: 'watch-notify',
setup (build) {
let count = 0
let time
build.onStart(() => {
time = Date.now()
})
build.onEnd(({ errors, outputFiles, metafile, warnings }) => {
const elapsed = Date.now() - time
console.log(`Done in ${elapsed}ms`)
if (count++ === 0) {
return
}
if (errors.length) {
clients.forEach((res) => {
res.write(`data: ${JSON.stringify({ errors: errors.map(error => error.text) })}\n\n`)
res.flush()
})
return
}
clients.forEach((res) => {
res.write(`data: ${JSON.stringify({ update: { time: elapsed } })}\n\n`)
res.flush()
})
clients.length = 0
})
}
},
{
name: 'esbuild-readdir',
setup (build) {
build.onResolve({
filter: /^esbuild-readdir:.+$/,
}, ({ resolveDir, path }) => {
return {
namespace: 'esbuild-readdir',
path,
pluginData: {
resolveDir: join(resolveDir, path.replace(/^esbuild-readdir:/, ''))
},
}
})
build.onLoad({
filter: /.+/,
namespace: 'esbuild-readdir',
}, async ({ pluginData }) => {
const { resolveDir } = pluginData
const files = await fs.promises.readdir(resolveDir)
return {
contents: `module.exports = ${JSON.stringify(files)}`,
resolveDir,
loader: 'js',
}
})
}
},
{
name: 'esbuild-import-glob',
setup (build) {
build.onResolve({
filter: /^esbuild-import-glob\(path:(.+),skipFiles:(.+)\)+$/,
}, ({ resolveDir, path }) => {
return {
namespace: 'esbuild-import-glob',
path,
pluginData: {
resolveDir
},
}
})
build.onLoad({
filter: /.+/,
namespace: 'esbuild-import-glob',
}, async ({ pluginData, path }) => {
const { resolveDir } = pluginData
const [, userPath, skipFiles] = /^esbuild-import-glob\(path:(.+),skipFiles:(.+)\)+$/g.exec(path)
const files = (await fs.promises.readdir(join(resolveDir, userPath))).filter(f => !skipFiles.includes(f))
return {
contents: `module.exports = { ${files.map(f => `'${f}': require('./${join(userPath, f)}')`).join(',')} }`,
resolveDir,
loader: 'js',
}
})
}
},
{
name: 'fix-dynamic-require',
setup (build) {
build.onResolve({
filter: /1\.14\/chunk/,
}, async ({ resolveDir, path }) => {
if (!resolveDir.includes('prismarine-provider-anvil')) return
return {
namespace: 'fix-dynamic-require',
path,
pluginData: {
resolvedPath: `${join(resolveDir, path)}.js`,
resolveDir
},
}
})
build.onLoad({
filter: /.+/,
namespace: 'fix-dynamic-require',
}, async ({ pluginData: { resolvedPath, resolveDir } }) => {
const resolvedFile = await fs.promises.readFile(resolvedPath, 'utf8')
return {
contents: resolvedFile.replace("require(`prismarine-chunk/src/pc/common/BitArray${noSpan ? 'NoSpan' : ''}`)", "noSpan ? require(`prismarine-chunk/src/pc/common/BitArray`) : require(`prismarine-chunk/src/pc/common/BitArrayNoSpan`)"),
resolveDir,
loader: 'js',
}
})
}
},
polyfillNode({
polyfills: {
fs: false,
crypto: false,
events: false,
http: false,
stream: false,
buffer: false,
perf_hooks: false,
net: false,
},
})
]
export { plugins, clients }

View file

@ -10,11 +10,13 @@ function removeLinesBetween (start, end) {
let startIndex = lines.findIndex(line => line === start)
if (startIndex === -1) return
let endIndex = startIndex + lines.slice(startIndex).findIndex(line => line === end)
const linesToRemove = endIndex - startIndex + 1
lines.splice(startIndex, linesToRemove)
// insert block comments
lines.splice(startIndex, 0, `/*`)
lines.splice(endIndex + 2, 0, `*/`)
}
// todo removing bedrock support for now, will optiimze in future instead
removeLinesBetween(" 'bedrock': {", ' }')
// fs.writeFileSync(path.join(dataPath, '../dataGlobal.js'), newContents, 'utf8')
fs.writeFileSync(dataPath, lines.join('\n'), 'utf8')

View file

@ -4,12 +4,13 @@ const express = require('express')
const netApi = require('net-browserify')
const compression = require('compression')
const path = require('path')
const cors = require('cors')
const https = require('https')
const fs = require('fs')
// Create our app
const app = express()
app.get('/config.json', (_, res) => res.sendFile(path.join(__dirname, 'config.json')))
app.use(compression())
app.use(netApi({ allowOrigin: '*' }))
if (process.argv[3] === 'dev') {
@ -25,10 +26,12 @@ if (process.argv[3] === 'dev') {
})
)
} else {
app.use(express.static(path.join(__dirname, './public')))
app.use(express.static(path.join(__dirname, './dist')))
}
// Start the server
const server = app.listen(process.argv[2] === undefined ? 8080 : process.argv[2], function () {
const server = process.argv.includes('--build-only') ? undefined : app.listen(require.main !== module || process.argv[2] === undefined ? 8080 : process.argv[2], function () {
console.log('Server listening on port ' + server.address().port)
})
module.exports = { app }

2
src/crypto.js Normal file
View file

@ -0,0 +1,2 @@
export * from 'crypto-browserify'
export function createPublicKey () { }

8
src/globals.d.ts vendored
View file

@ -28,3 +28,11 @@ interface ObjectConstructor {
fromEntries<T extends [string, any][]>(obj: T): Record<T[number][0], T[number][1]>
assign<T extends Record<string, any>, K extends Record<string, any>>(target: T, source: K): asserts target is T & K
}
declare module '*.css' {
/**
* @deprecated Use `import style from './style.css?inline'` instead.
*/
const css: string
export default css
}

View file

@ -5,6 +5,7 @@
// soureMapSupport.install({
// environment: 'browser',
// })
require('./styles.css')
require('iconify-icon')
require('./chat')
@ -37,9 +38,6 @@ require('./botControls')
require('./dragndrop')
require('./browserfs')
// @ts-ignore
require('crypto').createPublicKey = () => { }
const net = require('net')
const Stats = require('stats.js')
@ -64,6 +62,7 @@ const { customCommunication } = require('./customServer')
const { default: updateTime } = require('./updateTime')
const { options } = require('./optionsStorage')
const { subscribeKey } = require('valtio/utils')
const _ = require('lodash')
if ('serviceWorker' in navigator && !isCypress()) {
window.addEventListener('load', () => {

3
src/shims.js Normal file
View file

@ -0,0 +1,3 @@
const BrowserFS = require('browserfs')
export { BrowserFS }

View file

@ -8,7 +8,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin')
const config = {
entry: path.resolve(__dirname, './src/index.js'),
output: {
path: path.resolve(__dirname, './public'),
path: path.resolve(__dirname, './dist'),
filename: './[name].js',
publicPath: './',
hotUpdateChunkFilename: 'hot/hot-update.[name].js',
@ -91,7 +91,7 @@ const config = {
),
new CopyPlugin({
patterns: [
{ from: path.join(__dirname, 'src/styles.css'), to: './styles.css' },
{ from: path.join(__dirname, 'src/styles.css'), to: './index.css' },
]
})
]