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:
@@ -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",
|
||||
{
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -70,5 +70,6 @@
|
||||
.setAttribute("content", color);
|
||||
}
|
||||
</script>
|
||||
//inject-script-tags//
|
||||
</body>
|
||||
</html>
|
||||
|
||||
2
server/typings/index.d.ts
vendored
2
server/typings/index.d.ts
vendored
@@ -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;
|
||||
|
||||
@@ -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}</>);
|
||||
|
||||
28
server/utils/readManifestFile.ts
Normal file
28
server/utils/readManifestFile.ts
Normal 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;
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user