Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: build with esbuild, apply source maps #392

Merged
merged 10 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions .aegir.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,15 @@ export default {
// .css deps aren't checked properly.
'ipfs-css',
'tachyons',

// required by webpack
'webpack-cli',
'webpack-dev-server',
'babel-loader',
'style-loader',
'css-loader'
],
productionIgnorePatterns: [
'webpack.config.js',
'playwright.config.js',
'test-e2e',
'.aegir.js',
'/test',
'dist'
'dist',
'build.js'
]
}
}
202 changes: 202 additions & 0 deletions build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/* eslint-disable no-console */
import { execSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import esbuild from 'esbuild'

const copyPublicFiles = () => {
const srcDir = path.resolve('public')
const destDir = path.resolve('dist')

// Ensure the destination directory exists
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true })
}

// Read all files in the source directory
const files = fs.readdirSync(srcDir)

// Copy each file to the destination directory
files.forEach(file => {
const srcFile = path.join(srcDir, file)
const destFile = path.join(destDir, file)
fs.copyFileSync(srcFile, destFile)
console.log(`${file} copied to dist folder.`)
})
}

function gitRevision () {
try {
const ref = execSync('git rev-parse --abbrev-ref HEAD').toString().trim()
const sha = execSync('git rev-parse --short HEAD').toString().trim()

try {
// detect production build
execSync('git fetch --force --depth=1 --quiet origin production')
const latestProduction = execSync('git rev-parse remotes/origin/production').toString().trim()
if (latestProduction.startsWith(sha)) {
return `production@${sha}`
}

// detect staging build
execSync('git fetch --force --depth=1 --quiet origin staging')
const latestStaging = execSync('git rev-parse remotes/origin/staging').toString().trim()
if (latestStaging.startsWith(sha)) {
return `staging@${sha}`
}
} catch (_) { /* noop */ }

return `${ref}@${sha}`
} catch (_) {
return `no-git-dirty@${new Date().getTime().toString()}`
}
}
/**
* Inject the dist/index.js and dist/index.css into the dist/index.html file
*
* @param {esbuild.Metafile} metafile
*/
const injectAssets = (metafile) => {
const htmlFilePath = path.resolve('dist/index.html')

// Extract the output file names from the metafile
const outputs = metafile.outputs
const scriptFile = Object.keys(outputs).find(file => file.endsWith('.js') && file.includes('ipfs-sw-index'))
const cssFile = Object.keys(outputs).find(file => file.endsWith('.css') && file.includes('ipfs-sw-index'))

const scriptTag = `<script type="module" src="${path.basename(scriptFile)}"></script>`
const linkTag = `<link rel="stylesheet" href="${path.basename(cssFile)}">`

// Read the index.html file
let htmlContent = fs.readFileSync(htmlFilePath, 'utf8')

// Inject the link tag for CSS before the closing </head> tag
htmlContent = htmlContent.replace('</head>', `${linkTag}</head>`)

// Inject the script tag for JS before the closing </body> tag
htmlContent = htmlContent.replace('</body>', `${scriptTag}</body>`)

// Inject the git revision into the index
htmlContent = htmlContent.replace(/<%= GIT_VERSION %>/g, gitRevision())

// Write the modified HTML back to the index.html file
fs.writeFileSync(htmlFilePath, htmlContent)
console.log(`Injected ${path.basename(scriptFile)} and ${path.basename(cssFile)} into index.html.`)
}

/**
* We need the service worker to have a consistent name
*
* @type {esbuild.Plugin}
*/
const renameSwPlugin = {
name: 'rename-sw-plugin',
setup (build) {
build.onEnd(() => {
const outdir = path.resolve('dist')
const files = fs.readdirSync(outdir)

files.forEach(file => {
if (file.startsWith('ipfs-sw-sw-')) {
// everything after the dot
const extension = file.slice(file.indexOf('.'))
const oldPath = path.join(outdir, file)
const newPath = path.join(outdir, `ipfs-sw-sw${extension}`)
fs.renameSync(oldPath, newPath)
console.log(`Renamed ${file} to ipfs-sw-sw${extension}`)
if (extension === '.js') {
// Replace sourceMappingURL with new path
const contents = fs.readFileSync(newPath, 'utf8')
const newContents = contents.replace(/sourceMappingURL=.*\.js\.map/, 'sourceMappingURL=ipfs-sw-sw.js.map')
fs.writeFileSync(newPath, newContents)
}
}
})
})
}
}

/**
* @type {esbuild.Plugin}
*/
const modifyBuiltFiles = {
name: 'modify-built-files',
setup (build) {
build.onEnd(async (result) => {
copyPublicFiles()
injectAssets(result.metafile)
})
}
}

/**
* @type {esbuild.BuildOptions}
*/
export const buildOptions = {
entryPoints: ['src/index.tsx', 'src/sw.ts'],
bundle: true,
outdir: 'dist',
loader: {
'.js': 'jsx',
'.css': 'css',
'.eot': 'file',
'.otf': 'file',
'.woff': 'file',
'.woff2': 'file',
'.svg': 'file'
},
minify: true,
sourcemap: true,
metafile: true,
splitting: false,
target: ['es2020'],
format: 'esm',
entryNames: 'ipfs-sw-[name]-[hash]',
assetNames: 'ipfs-sw-[name]-[hash]',
plugins: [renameSwPlugin, modifyBuiltFiles]
}

const ctx = await esbuild.context(buildOptions)

const buildAndWatch = async () => {
try {
await ctx.watch()

process.on('exit', async () => {
await ctx.dispose()
})

console.log('Watching for changes...')
await ctx.rebuild()
console.log('Initial build completed successfully.')
} catch (error) {
console.error('Build failed:', error)
process.exit(1)
}
}

const watchRequested = process.argv.includes('--watch')
const serveRequested = process.argv.includes('--serve')

if (!watchRequested && !serveRequested) {
esbuild.build(buildOptions).then(result => {
console.log('Build completed successfully.')
}).catch(error => {
console.error('Build failed:', error)
process.exit(1)
})
await ctx.dispose()
}

if (watchRequested) {
await buildAndWatch()
}

if (serveRequested) {
const { host, port } = await ctx.serve({
servedir: 'dist',
port: 8345,
host: 'localhost'
})
console.info(`Listening on http://${host}:${port}`)
}
Loading
Loading