Replace Webpack with Vite (#4765)

Co-authored-by: Tom Moor <tom@getoutline.com>
Co-authored-by: Vio <vio@beanon.com>
This commit is contained in:
Hans Pagel
2023-02-16 04:39:46 +01:00
committed by GitHub
parent 490d05b68b
commit e754f89e5c
40 changed files with 1646 additions and 3565 deletions

View File

@@ -13,7 +13,13 @@
],
"plugins": [
"babel-plugin-transform-typescript-metadata",
["@babel/plugin-proposal-decorators", { "legacy": true }],
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
],
"@babel/plugin-proposal-class-properties",
[
"transform-inline-environment-variables",
{

View File

@@ -12,45 +12,34 @@ import { Integration } from "@server/models";
import presentEnv from "@server/presenters/env";
import { getTeamFromContext } from "@server/utils/passport";
import prefetchTags from "@server/utils/prefetchTags";
import readManifestFile from "@server/utils/readManifestFile";
const isProduction = env.ENVIRONMENT === "production";
const isDevelopment = env.ENVIRONMENT === "development";
const isTest = env.ENVIRONMENT === "test";
const readFile = util.promisify(fs.readFile);
let indexHtmlCache: Buffer | undefined;
const readIndexFile = async (ctx: Context): Promise<Buffer> => {
if (isProduction) {
return (
indexHtmlCache ??
(indexHtmlCache = await readFile(
path.join(__dirname, "../../app/index.html")
))
);
const readIndexFile = async (): Promise<Buffer> => {
if (isProduction || isTest) {
if (indexHtmlCache) {
return indexHtmlCache;
}
}
if (isTest) {
return (
indexHtmlCache ??
(indexHtmlCache = await readFile(
path.join(__dirname, "../static/index.html")
))
return await readFile(path.join(__dirname, "../static/index.html"));
}
if (isDevelopment) {
return await readFile(
path.join(__dirname, "../../../server/static/index.html")
);
}
const middleware = ctx.devMiddleware;
await new Promise((resolve) => middleware.waitUntilValid(resolve));
return new Promise((resolve, reject) => {
middleware.fileSystem.readFile(
`${ctx.webpackConfig.output.path}/index.html`,
(err: Error, result: Buffer) => {
if (err) {
return reject(err);
}
resolve(result);
}
);
});
return (indexHtmlCache = await readFile(
path.join(__dirname, "../../app/index.html")
));
};
export const renderApp = async (
@@ -74,10 +63,26 @@ export const renderApp = async (
}
const { shareId } = ctx.params;
const page = await readIndexFile(ctx);
const page = await readIndexFile();
const environment = `
window.env = ${JSON.stringify(presentEnv(env, options.analytics))};
`;
const entry = "app/index.tsx";
const scriptTags = isProduction
? `<script type="module" src="${env.CDN_URL || ""}/static/${
readManifestFile()[entry]["file"]
}"></script>`
: `<script type="module">
import RefreshRuntime from 'http://localhost:3001/@react-refresh'
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => { }
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
<script type="module" src="http://localhost:3001/@vite/client"></script>
<script type="module" src="http://localhost:3001/${entry}"></script>
`;
ctx.body = page
.toString()
.replace(/\/\/inject-env\/\//g, environment)
@@ -85,7 +90,8 @@ export const renderApp = async (
.replace(/\/\/inject-description\/\//g, escape(description))
.replace(/\/\/inject-canonical\/\//g, canonical)
.replace(/\/\/inject-prefetch\/\//g, shareId ? "" : prefetchTags)
.replace(/\/\/inject-slack-app-id\/\//g, env.SLACK_APP_ID || "");
.replace(/\/\/inject-slack-app-id\/\//g, env.SLACK_APP_ID || "")
.replace(/\/\/inject-script-tags\/\//g, scriptTags);
};
export const renderShare = async (ctx: Context, next: Next) => {

View File

@@ -18,7 +18,6 @@ import api from "../routes/api";
import auth from "../routes/auth";
const isProduction = env.ENVIRONMENT === "production";
const isTest = env.ENVIRONMENT === "test";
// Construct scripts CSP based on services in use by this installation
const defaultSrc = ["'self'"];
@@ -31,6 +30,12 @@ const scriptSrc = [
"cdn.zapier.com",
];
// Allow to load assets from Vite
if (!isProduction) {
scriptSrc.push("127.0.0.1:3001");
scriptSrc.push("localhost:3001");
}
if (env.GOOGLE_ANALYTICS_ID) {
scriptSrc.push("www.google-analytics.com");
}
@@ -62,52 +67,6 @@ export default function init(app: Koa = new Koa()): Koa {
// trust header fields set by our proxy. eg X-Forwarded-For
app.proxy = true;
} else if (!isTest) {
const convert = require("koa-convert");
const webpack = require("webpack");
const devMiddleware = require("koa-webpack-dev-middleware");
const hotMiddleware = require("koa-webpack-hot-middleware");
const config = require("../../webpack.config.dev");
const compile = webpack(config);
/* eslint-enable global-require */
const middleware = devMiddleware(compile, {
// display no info to console (only warnings and errors)
noInfo: true,
// display nothing to the console
quiet: false,
watchOptions: {
poll: 1000,
ignored: ["node_modules", "flow-typed", "server", "build", "__mocks__"],
},
// Uncomment to test service worker
// headers: {
// "Service-Worker-Allowed": "/",
// },
// public path to bind the middleware to
// use the same as in webpack
publicPath: config.output.publicPath,
// options for formatting the statistics
stats: {
colors: true,
},
});
app.use(async (ctx, next) => {
ctx.webpackConfig = config;
ctx.devMiddleware = middleware;
await next();
});
app.use(convert(middleware));
app.use(
convert(
hotMiddleware(compile, {
// @ts-expect-error ts-migrate(7019) FIXME: Rest parameter 'args' implicitly has an 'any[]' ty... Remove this comment to see the full error message
log: (...args) => Logger.info("lifecycle", ...args),
path: "/__webpack_hmr",
heartbeat: 10 * 1000,
})
)
);
}
app.use(mount("/auth", auth));

View File

@@ -70,5 +70,6 @@
.setAttribute("content", color);
}
</script>
//inject-script-tags//
</body>
</html>

View File

@@ -10,6 +10,8 @@ declare module "oy-vey";
declare module "fetch-test-server";
declare module "dotenv";
declare module "email-providers" {
const list: string[];
export default list;

View File

@@ -1,8 +1,9 @@
import fs from "fs";
import path from "path";
import * as React from "react";
import ReactDOMServer from "react-dom/server";
import env from "@server/env";
import readManifestFile, { ManifestStructure } from "./readManifestFile";
const isProduction = env.ENVIRONMENT === "production";
const prefetchTags = [];
@@ -16,45 +17,47 @@ if (process.env.AWS_S3_UPLOAD_BUCKET_URL) {
);
}
let manifestData = {};
if (isProduction) {
const manifest = readManifestFile();
try {
const manifest = fs.readFileSync(
path.join(__dirname, "../../app/manifest.json"),
"utf8"
);
manifestData = JSON.parse(manifest);
} catch (err) {
// no-op
}
const returnFileAndImportsFromManifest = (
manifest: ManifestStructure,
file: string
): string[] => {
return [
manifest[file]["file"],
...manifest[file]["imports"].map((entry: string) => {
return manifest[entry]["file"];
}),
];
};
Object.values(manifestData).forEach((filename) => {
if (typeof filename !== "string") {
return;
}
if (!env.CDN_URL) {
return;
}
if (filename.endsWith(".js")) {
// Preload resources you have high-confidence will be used in the current
// page.Prefetch resources likely to be used for future navigations
const shouldPreload =
filename.includes("/main") ||
filename.includes("/runtime") ||
filename.includes("preload-");
if (shouldPreload) {
Array.from([
...returnFileAndImportsFromManifest(manifest, "app/index.tsx"),
...returnFileAndImportsFromManifest(manifest, "app/editor/index.tsx"),
]).forEach((file) => {
if (file.endsWith(".js")) {
prefetchTags.push(
<link rel="preload" href={filename} key={filename} as="script" />
<link
rel="prefetch"
href={`${env.CDN_URL || ""}/static/${file}`}
key={file}
as="script"
crossOrigin="anonymous"
/>
);
} else if (file.endsWith(".css")) {
prefetchTags.push(
<link
rel="prefetch"
href={`${env.CDN_URL || ""}/static/${file}`}
key={file}
as="style"
crossOrigin="anonymous"
/>
);
}
} else if (filename.endsWith(".css")) {
prefetchTags.push(
<link rel="prefetch" href={filename} key={filename} as="style" />
);
}
});
});
}
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'Element[]' is not assignable to ... Remove this comment to see the full error message
export default ReactDOMServer.renderToString(prefetchTags);
export default ReactDOMServer.renderToString(<>{prefetchTags}</>);

View File

@@ -0,0 +1,28 @@
import fs from "fs";
import path from "path";
export type Chunk = {
file: string;
src: string;
isEntry?: boolean;
};
export type ManifestStructure = Record<string, Chunk>;
export const readManifestFile = (file = "./build/app/manifest.json") => {
const absoluteFilePath = path.resolve(file);
let manifest = "{}";
try {
manifest = fs.readFileSync(absoluteFilePath, "utf8") as string;
} catch (err) {
console.warn(
`Can not find ${absoluteFilePath}. Try executing "yarn vite:build" before running in production mode.`
);
}
return JSON.parse(manifest) as ManifestStructure;
};
export default readManifestFile;

View File

@@ -50,10 +50,11 @@ export async function checkMigrations() {
return;
}
const isProduction = env.ENVIRONMENT === "production";
const teams = await Team.count();
const providers = await AuthenticationProvider.count();
if (teams && !providers) {
if (isProduction && teams && !providers) {
Logger.warn(`
This version of Outline cannot start until a data migration is complete.
Backup your database, run the database migrations and the following script: