tutanota/buildSrc/buildWebapp.js

258 lines
8.5 KiB
JavaScript
Raw Normal View History

/**
Exports the buildWebapp function that can be used for production builds.
*/
import { rollup } from "rollup"
import typescript from "@rollup/plugin-typescript"
import terser from "@rollup/plugin-terser"
import path from "node:path"
import { nodeResolve } from "@rollup/plugin-node-resolve"
import commonjs from "@rollup/plugin-commonjs"
import fs from "fs-extra"
import { bundleDependencyCheckPlugin, getChunkName, resolveLibs } from "./RollupConfig.js"
import os from "node:os"
import * as env from "./env.js"
import { createHtml } from "./createHtml.js"
import { domainConfigs } from "./DomainConfigs.js"
import { visualizer } from "rollup-plugin-visualizer"
import { rollupWasmLoader } from "@tutao/tuta-wasm-loader"
/**
* Builds the web app for production.
* @param version Version of the app. Will be used for html generation and service worker versioning.
* @param stage Deployment for which to build: 'prod' will build for the production system, 'test' for the test system, 'local' will use localhost.
* @param host If stage is left undefined, the value provided here will be used to construct the app URL for HTML creation.
* @param measure Function that returns the current elapsed build time.
* @param minify Boolean. Set to true to perform minification.
* @param projectDir Path to the tutanota root directory.
* @param app App to build, 'mail' for mail app and 'calendar' for calendar app
* @returns Nothing meaningful.
*/
export async function buildWebapp({ version, stage, host, measure, minify, projectDir, app }) {
const isCalendarApp = app === "calendar"
const tsConfig = isCalendarApp ? "tsconfig-calendar-app.json" : "tsconfig.json"
const buildDir = isCalendarApp ? "build-calendar-app" : "build"
const resolvedBuildDir = path.resolve(buildDir)
const entryFile = isCalendarApp ? "src/calendar-app/calendar-app.ts" : "src/mail-app/app.ts"
const workerFile = isCalendarApp ? "src/calendar-app/workerUtils/worker/calendar-worker.ts" : "src/mail-app/workerUtils/worker/mail-worker.ts"
const builtWorkerFile = isCalendarApp ? "calendar-worker.js" : "mail-worker.js"
console.log("Building app", app)
console.log("started cleaning", measure())
await fs.emptyDir(buildDir)
console.log("bundling polyfill", measure())
const polyfillBundle = await rollup({
input: ["src/polyfill.ts"],
plugins: [
typescript(),
minify && terser(),
// nodeResolve is for our own modules
nodeResolve({
preferBuiltins: true,
resolveOnly: [/^@tutao\/.*$/],
}),
commonjs(),
],
})
await polyfillBundle.write({
sourcemap: false,
format: "iife",
file: `${buildDir}/polyfill.js`,
})
console.log("started copying images", measure())
await fs.copy(path.join(projectDir, "/resources/images"), path.join(projectDir, `/${buildDir}/images`))
await fs.copy(path.join(projectDir, "/resources/favicon"), path.join(projectDir, `${buildDir}/images`))
await fs.copy(path.join(projectDir, "/resources/pdf"), path.join(projectDir, `${buildDir}/pdf`))
await fs.copy(path.join(projectDir, "/resources/wordlibrary.json"), path.join(projectDir, `${buildDir}/wordlibrary.json`))
await fs.copy(path.join(projectDir, "/src/braintree.html"), path.join(projectDir, `/${buildDir}/braintree.html`))
console.log("started bundling", measure())
const bundle = await rollup({
input: [entryFile, workerFile],
preserveEntrySignatures: false,
perf: true,
plugins: [
typescript({
tsconfig: tsConfig,
}),
resolveLibs(),
commonjs({
exclude: "src/**",
}),
minify && terser(),
analyzer(projectDir, buildDir),
visualizer({ filename: `${buildDir}/stats.html`, gzipSize: true }),
2024-07-30 10:57:18 +02:00
bundleDependencyCheckPlugin(),
nodeResolve({
preferBuiltins: true,
resolveOnly: [/^@tutao\/.*$/],
}),
rollupWasmLoader({
webassemblyLibraries: [
{
name: "liboqs.wasm",
2025-01-06 18:34:51 +01:00
command: "make -f Makefile_liboqs build",
workingDir: "libs/webassembly/",
outputPath: path.join(resolvedBuildDir, "wasm/liboqs.wasm"),
2025-01-06 18:34:51 +01:00
fallback: {
command: "make -f Makefile_liboqs fallback",
workingDir: "libs/webassembly/",
outputPath: path.join(resolvedBuildDir, "wasm/liboqs.js"),
},
},
{
name: "argon2.wasm",
command: "make -f Makefile_argon2 build fallback",
workingDir: "libs/webassembly/",
outputPath: path.join(resolvedBuildDir, "wasm/argon2.wasm"),
2025-01-06 18:34:51 +01:00
fallback: {
command: "make -f Makefile_argon2 fallback",
workingDir: "libs/webassembly/",
outputPath: path.join(resolvedBuildDir, "wasm/argon2.js"),
},
},
],
}),
],
})
console.log("bundling timings: ")
for (let [k, v] of Object.entries(bundle.getTimings())) {
console.log(k, v[0])
}
console.log("started writing bundles into", buildDir, measure())
const output = await bundle.write({
sourcemap: true,
format: "esm",
dir: buildDir,
manualChunks(id, { getModuleInfo, getModuleIds }) {
return getChunkName(id, { getModuleInfo })
},
chunkFileNames: (chunkInfo) => {
return "[name]-[hash].js"
2022-12-27 15:37:40 +01:00
},
})
2022-12-27 15:37:40 +01:00
const chunks = output.output.map((c) => c.fileName)
// we have to use System.import here because bootstrap is not executed until we actually import()
// unlike nollup+es format where it just runs on being loaded like you expect
2022-12-27 15:37:40 +01:00
await fs.promises.writeFile(
`${buildDir}/worker-bootstrap.js`,
`import "./polyfill.js"
import "./${builtWorkerFile}"`,
2022-12-27 15:37:40 +01:00
)
let restUrl
let networkDebugging
2022-12-27 15:37:40 +01:00
if (stage === "test") {
2023-10-19 09:45:30 +02:00
restUrl = "https://app.test.tuta.com"
networkDebugging = true
2022-12-27 15:37:40 +01:00
} else if (stage === "prod") {
restUrl = "https://app.tuta.com"
networkDebugging = false
2022-12-27 15:37:40 +01:00
} else if (stage === "local") {
restUrl = "http://" + os.hostname() + ":9000"
networkDebugging = true
2022-12-27 15:37:40 +01:00
} else if (stage === "release") {
restUrl = undefined
networkDebugging = false
2022-12-27 15:37:40 +01:00
} else {
// host
restUrl = host
networkDebugging = true
}
await createHtml(
env.create({
2022-12-27 15:37:40 +01:00
staticUrl: stage === "release" || stage === "local" ? null : restUrl,
version,
mode: "Browser",
2022-12-27 15:37:40 +01:00
dist: true,
domainConfigs,
networkDebugging,
}),
app,
)
if (stage !== "release") {
await createHtml(env.create({ staticUrl: restUrl, version, mode: "App", dist: true, domainConfigs, networkDebugging }), app)
}
await bundleServiceWorker(chunks, version, minify, buildDir)
}
async function bundleServiceWorker(bundles, version, minify, buildDir) {
const customDomainFileExclusions = ["index.html", "index.js"]
const filesToCache = ["index.js", "index.html", "polyfill.js", "worker-bootstrap.js"]
// we always include English
// we still cache native-common even though we don't need it because worker has to statically depend on it
2022-12-27 15:37:40 +01:00
.concat(
bundles.filter(
(it) =>
it.startsWith("translation-en") ||
(!it.startsWith("translation") && !it.startsWith("native-main") && !it.startsWith("SearchInPageOverlay")),
),
)
.concat(["images/logo-favicon.png", "images/logo-favicon-152.png", "images/logo-favicon-196.png", "images/font.ttf"])
const swBundle = await rollup({
2024-07-05 11:32:43 +02:00
input: ["src/common/serviceworker/sw.ts"],
plugins: [
typescript(),
minify && terser(),
{
name: "sw-banner",
banner() {
return `function filesToCache() { return ${JSON.stringify(filesToCache)} }
function version() { return "${version}" }
function customDomainCacheExclusions() { return ${JSON.stringify(customDomainFileExclusions)} }`
2022-12-27 15:37:40 +01:00
},
},
],
})
await swBundle.write({
sourcemap: true,
format: "iife",
file: `${buildDir}/sw.js`,
})
}
/**
* A little plugin to:
* - Print out each chunk size and contents
* - Create a graph file with chunk dependencies.
*/
function analyzer(projectDir, buildDir) {
return {
name: "analyze",
async generateBundle(outOpts, bundle) {
const prefix = projectDir
let buffer = "digraph G {\n"
buffer += "edge [dir=back]\n"
for (const [fileName, info] of Object.entries(bundle)) {
if (fileName.startsWith("translation")) continue
// https://www.rollupjs.org/plugin-development/#generatebundle
if (info.type === "asset") continue
for (const dep of info.imports) {
if (!dep.includes("translation")) {
buffer += `"${dep}" -> "${fileName}"\n`
}
}
console.log(fileName, "", info.code.length / 1024 + "K")
for (const module of Object.keys(info.modules)) {
if (module.includes("src/common/api/entities")) {
continue
}
const moduleName = module.startsWith(prefix) ? module.substring(prefix.length) : module
2023-02-10 10:11:25 +01:00
console.log("\t" + moduleName)
}
}
buffer += "}\n"
await fs.writeFile(`${buildDir}/bundles.dot`, buffer)
},
}
2022-12-27 15:37:40 +01:00
}