tutanota/buildSrc/buildWebapp.js

213 lines
6.5 KiB
JavaScript
Raw Normal View History

/**
Exports the buildWebapp function that can be used for production builds.
*/
2022-12-27 15:37:40 +01:00
import { rollup } from "rollup"
import typescript from "@rollup/plugin-typescript"
2022-12-27 15:37:40 +01:00
import { terser } from "rollup-plugin-terser"
import path from "path"
2022-12-27 15:37:40 +01:00
import { nodeResolve } from "@rollup/plugin-node-resolve"
import commonjs from "@rollup/plugin-commonjs"
import fs from "fs-extra"
2022-12-27 15:37:40 +01:00
import { bundleDependencyCheckPlugin, getChunkName, resolveLibs } from "./RollupConfig.js"
import { visualizer } from "rollup-plugin-visualizer"
import os from "os"
import * as env from "./env.js"
2022-12-27 15:37:40 +01:00
import { createHtml } from "./createHtml.js"
/**
* 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.
* @returns Nothing meaningful.
*/
2022-12-27 15:37:40 +01:00
export async function buildWebapp({ version, stage, host, measure, minify, projectDir }) {
console.log("started cleaning", measure())
await fs.emptyDir("build")
console.log("bundling polyfill", measure())
const polyfillBundle = await rollup({
input: ["src/polyfill.ts"],
plugins: [
typescript(),
minify && terser(),
{
name: "append-libs",
resolveId(id) {
if (id === "systemjs") {
return path.resolve("libs/s.js")
}
},
},
// nodeResolve is for oxmsg and our own modules
nodeResolve(),
commonjs(),
],
})
await polyfillBundle.write({
sourcemap: false,
format: "iife",
2022-12-27 15:37:40 +01:00
file: "build/dist/polyfill.js",
})
console.log("started copying images", measure())
2022-12-27 15:37:40 +01:00
await fs.copy(path.join(projectDir, "/resources/images"), path.join(projectDir, "/build/dist/images"))
await fs.copy(path.join(projectDir, "/resources/favicon"), path.join(projectDir, "build/dist/images"))
await fs.copy(path.join(projectDir, "/resources/wordlibrary.json"), path.join(projectDir, "build/dist/wordlibrary.json"))
await fs.copy(path.join(projectDir, "/src/braintree.html"), path.join(projectDir, "/build/dist/braintree.html"))
console.log("started bundling", measure())
const bundle = await rollup({
input: ["src/app.ts", "src/api/worker/worker.ts"],
preserveEntrySignatures: false,
perf: true,
plugins: [
typescript({}),
resolveLibs(),
commonjs({
exclude: "src/**",
}),
minify && terser(),
analyzer(projectDir),
2022-12-27 15:37:40 +01:00
visualizer({ filename: "build/stats.html", gzipSize: true }),
bundleDependencyCheckPlugin(),
nodeResolve(),
],
})
console.log("bundling timings: ")
for (let [k, v] of Object.entries(bundle.getTimings())) {
console.log(k, v[0])
}
console.log("started writing bundles", measure())
const output = await bundle.write({
sourcemap: true,
format: "system",
dir: "build/dist",
2022-12-27 15:37:40 +01:00
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(
"build/dist/worker-bootstrap.js",
`importScripts("./polyfill.js")
const importPromise = System.import("./worker.js")
self.onmessage = function (msg) {
importPromise.then(function () {
self.onmessage(msg)
})
}
2022-12-27 15:37:40 +01:00
`,
)
let restUrl
2022-12-27 15:37:40 +01:00
if (stage === "test") {
restUrl = "https://test.tutanota.com"
} else if (stage === "prod") {
restUrl = "https://mail.tutanota.com"
} else if (stage === "local") {
restUrl = "http://" + os.hostname() + ":9000"
2022-12-27 15:37:40 +01:00
} else if (stage === "release") {
restUrl = undefined
2022-12-27 15:37:40 +01:00
} else {
// host
restUrl = host
}
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,
}),
)
if (stage !== "release") {
2022-12-27 15:37:40 +01:00
await createHtml(env.create({ staticUrl: restUrl, version, mode: "App", dist: true }))
}
await bundleServiceWorker(chunks, version, minify)
}
async function bundleServiceWorker(bundles, version, minify) {
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({
input: ["src/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",
2022-12-27 15:37:40 +01:00
file: "build/dist/sw.js",
})
}
/**
* A little plugin to:
* - Print out each chunk size and contents
* - Create a graph file with chunk dependencies.
*/
function analyzer(projectDir) {
return {
name: "analyze",
async generateBundle(outOpts, bundle) {
const prefix = projectDir
let buffer = "digraph G {\n"
buffer += "edge [dir=back]\n"
for (const [key, value] of Object.entries(bundle)) {
if (key.startsWith("translation")) continue
for (const dep of value.imports) {
if (!dep.includes("translation")) {
buffer += `"${dep}" -> "${key}"\n`
}
}
console.log(key, "", value.code.length / 1024 + "K")
for (const module of Object.keys(value.modules)) {
if (module.includes("src/api/entities")) {
continue
}
const moduleName = module.startsWith(prefix) ? module.substring(prefix.length) : module
console.log("" + moduleName)
}
}
buffer += "}\n"
await fs.writeFile("build/bundles.dot", buffer)
},
}
2022-12-27 15:37:40 +01:00
}