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:
@@ -98,8 +98,11 @@ jobs:
|
|||||||
- restore_cache:
|
- restore_cache:
|
||||||
key: dependency-cache-{{ checksum "package.json" }}
|
key: dependency-cache-{{ checksum "package.json" }}
|
||||||
- run:
|
- run:
|
||||||
name: build-webpack
|
name: build-vite
|
||||||
command: yarn build:webpack
|
command: yarn vite:build
|
||||||
|
- run:
|
||||||
|
name: Send bundle stats to RelativeCI
|
||||||
|
command: npx relative-ci-agent
|
||||||
build-image:
|
build-image:
|
||||||
executor: docker-publisher
|
executor: docker-publisher
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,4 +11,4 @@ fakes3/*
|
|||||||
.idea
|
.idea
|
||||||
*.pem
|
*.pem
|
||||||
*.key
|
*.key
|
||||||
*.cert
|
*.cert
|
||||||
|
|||||||
@@ -25,26 +25,12 @@ import {
|
|||||||
import Fade from "./Fade";
|
import Fade from "./Fade";
|
||||||
|
|
||||||
const DocumentHistory = React.lazy(
|
const DocumentHistory = React.lazy(
|
||||||
() =>
|
() => import("~/scenes/Document/components/History")
|
||||||
import(
|
|
||||||
/* webpackChunkName: "document-history" */
|
|
||||||
"~/scenes/Document/components/History"
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
const DocumentInsights = React.lazy(
|
const DocumentInsights = React.lazy(
|
||||||
() =>
|
() => import("~/scenes/Document/components/Insights")
|
||||||
import(
|
|
||||||
/* webpackChunkName: "document-insights" */
|
|
||||||
"~/scenes/Document/components/Insights"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const CommandBar = React.lazy(
|
|
||||||
() =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "command-bar" */
|
|
||||||
"~/components/CommandBar"
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
const CommandBar = React.lazy(() => import("~/components/CommandBar"));
|
||||||
|
|
||||||
const AuthenticatedLayout: React.FC = ({ children }) => {
|
const AuthenticatedLayout: React.FC = ({ children }) => {
|
||||||
const { ui, auth } = useStores();
|
const { ui, auth } = useStores();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as React from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { usePopoverState, PopoverDisclosure } from "reakit/Popover";
|
import { usePopoverState, PopoverDisclosure } from "reakit/Popover";
|
||||||
import Document from "~/models/Document";
|
import Document from "~/models/Document";
|
||||||
import { AvatarWithPresence } from "~/components/Avatar";
|
import AvatarWithPresence from "~/components/Avatar/AvatarWithPresence";
|
||||||
import DocumentViews from "~/components/DocumentViews";
|
import DocumentViews from "~/components/DocumentViews";
|
||||||
import Facepile from "~/components/Facepile";
|
import Facepile from "~/components/Facepile";
|
||||||
import NudeButton from "~/components/NudeButton";
|
import NudeButton from "~/components/NudeButton";
|
||||||
|
|||||||
@@ -30,13 +30,7 @@ import { sharedDocumentPath } from "~/utils/routeHelpers";
|
|||||||
import { isHash } from "~/utils/urls";
|
import { isHash } from "~/utils/urls";
|
||||||
import DocumentBreadcrumb from "./DocumentBreadcrumb";
|
import DocumentBreadcrumb from "./DocumentBreadcrumb";
|
||||||
|
|
||||||
const LazyLoadedEditor = React.lazy(
|
const LazyLoadedEditor = React.lazy(() => import("~/editor"));
|
||||||
() =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "preload-shared-editor" */
|
|
||||||
"~/editor"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
export type Props = Optional<
|
export type Props = Optional<
|
||||||
EditorProps,
|
EditorProps,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class ErrorBoundary extends React.Component<Props> {
|
|||||||
if (
|
if (
|
||||||
this.props.reloadOnChunkMissing &&
|
this.props.reloadOnChunkMissing &&
|
||||||
error.message &&
|
error.message &&
|
||||||
error.message.match(/chunk/)
|
error.message.match(/dynamically imported module/)
|
||||||
) {
|
) {
|
||||||
// If the editor bundle fails to load then reload the entire window. This
|
// If the editor bundle fails to load then reload the entire window. This
|
||||||
// can happen if a deploy happens between the user loading the initial JS
|
// can happen if a deploy happens between the user loading the initial JS
|
||||||
@@ -60,7 +60,9 @@ class ErrorBoundary extends React.Component<Props> {
|
|||||||
if (this.error) {
|
if (this.error) {
|
||||||
const error = this.error;
|
const error = this.error;
|
||||||
const isReported = !!env.SENTRY_DSN && isCloudHosted;
|
const isReported = !!env.SENTRY_DSN && isCloudHosted;
|
||||||
const isChunkError = this.error.message.match(/chunk/);
|
const isChunkError = this.error.message.match(
|
||||||
|
/dynamically imported module/
|
||||||
|
);
|
||||||
|
|
||||||
if (isChunkError) {
|
if (isChunkError) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -53,11 +53,7 @@ const style = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TwitterPicker = React.lazy(
|
const TwitterPicker = React.lazy(
|
||||||
() =>
|
() => import("react-color/lib/components/twitter/Twitter")
|
||||||
import(
|
|
||||||
/* webpackChunkName: "twitter-picker" */
|
|
||||||
"react-color/lib/components/twitter/Twitter"
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const icons = {
|
export const icons = {
|
||||||
|
|||||||
@@ -5,13 +5,7 @@ import scrollIntoView from "smooth-scroll-into-view-if-needed";
|
|||||||
import useQuery from "~/hooks/useQuery";
|
import useQuery from "~/hooks/useQuery";
|
||||||
import type { Props } from "./Table";
|
import type { Props } from "./Table";
|
||||||
|
|
||||||
const Table = React.lazy(
|
const Table = React.lazy(() => import("~/components/Table"));
|
||||||
() =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "table" */
|
|
||||||
"~/components/Table"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const TableFromParams = (
|
const TableFromParams = (
|
||||||
props: Omit<Props, "onChangeSort" | "onChangePage" | "topRef">
|
props: Omit<Props, "onChangeSort" | "onChangePage" | "topRef">
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import { formatDistanceToNow } from "date-fns";
|
import { formatDistanceToNow } from "date-fns";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
const LocaleTime = React.lazy(
|
const LocaleTime = React.lazy(() => import("~/components/LocaleTime"));
|
||||||
() =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "locale-time" */
|
|
||||||
"~/components/LocaleTime"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
type Props = React.ComponentProps<typeof LocaleTime> & {
|
type Props = React.ComponentProps<typeof LocaleTime> & {
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
// eslint-disable-next-line import/no-unresolved
|
||||||
|
import "vite/modulepreload-polyfill";
|
||||||
import "focus-visible";
|
import "focus-visible";
|
||||||
|
import "setimmediate";
|
||||||
import { LazyMotion } from "framer-motion";
|
import { LazyMotion } from "framer-motion";
|
||||||
import { KBarProvider } from "kbar";
|
import { KBarProvider } from "kbar";
|
||||||
import { Provider } from "mobx-react";
|
import { Provider } from "mobx-react";
|
||||||
@@ -35,33 +38,6 @@ if (env.SENTRY_DSN) {
|
|||||||
initSentry(history);
|
initSentry(history);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("serviceWorker" in window.navigator) {
|
|
||||||
window.addEventListener("load", () => {
|
|
||||||
// see: https://bugs.chromium.org/p/chromium/issues/detail?id=1097616
|
|
||||||
// In some rare (<0.1% of cases) this call can return `undefined`
|
|
||||||
const maybePromise = window.navigator.serviceWorker.register(
|
|
||||||
"/static/service-worker.js",
|
|
||||||
{
|
|
||||||
scope: "/",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (maybePromise?.then) {
|
|
||||||
maybePromise
|
|
||||||
.then((registration) => {
|
|
||||||
Logger.debug("lifecycle", "SW registered: ", registration);
|
|
||||||
})
|
|
||||||
.catch((registrationError) => {
|
|
||||||
Logger.debug(
|
|
||||||
"lifecycle",
|
|
||||||
"SW registration failed: ",
|
|
||||||
registrationError
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure to return the specific export containing the feature bundle.
|
// Make sure to return the specific export containing the feature bundle.
|
||||||
const loadFeatures = () => import("./utils/motion").then((res) => res.default);
|
const loadFeatures = () => import("./utils/motion").then((res) => res.default);
|
||||||
|
|
||||||
@@ -116,13 +92,38 @@ window.addEventListener("load", async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// https://github.com/googleanalytics/autotrack/issues/137#issuecomment-305890099
|
// https://github.com/googleanalytics/autotrack/issues/137#issuecomment-305890099
|
||||||
await import(
|
await import("autotrack/autotrack.js");
|
||||||
/* webpackChunkName: "autotrack" */
|
|
||||||
"autotrack/autotrack.js"
|
|
||||||
);
|
|
||||||
window.ga("require", "outboundLinkTracker");
|
window.ga("require", "outboundLinkTracker");
|
||||||
window.ga("require", "urlChangeTracker");
|
window.ga("require", "urlChangeTracker");
|
||||||
window.ga("require", "eventTracker", {
|
window.ga("require", "eventTracker", {
|
||||||
attributePrefix: "data-",
|
attributePrefix: "data-",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if ("serviceWorker" in navigator) {
|
||||||
|
window.addEventListener("load", () => {
|
||||||
|
// see: https://bugs.chromium.org/p/chromium/issues/detail?id=1097616
|
||||||
|
// In some rare (<0.1% of cases) this call can return `undefined`
|
||||||
|
const maybePromise = navigator.serviceWorker.register("/static/sw.js", {
|
||||||
|
scope: "/",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (maybePromise?.then) {
|
||||||
|
maybePromise
|
||||||
|
.then((registration) => {
|
||||||
|
Logger.debug(
|
||||||
|
"lifecycle",
|
||||||
|
"[ServiceWorker] Registered.",
|
||||||
|
registration
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((registrationError) => {
|
||||||
|
Logger.debug(
|
||||||
|
"lifecycle",
|
||||||
|
"[ServiceWorker] Registration failed.",
|
||||||
|
registrationError
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
NavigationNode,
|
NavigationNode,
|
||||||
} from "@shared/types";
|
} from "@shared/types";
|
||||||
import { sortNavigationNodes } from "@shared/utils/collections";
|
import { sortNavigationNodes } from "@shared/utils/collections";
|
||||||
import CollectionsStore from "~/stores/CollectionsStore";
|
import type CollectionsStore from "~/stores/CollectionsStore";
|
||||||
import Document from "~/models/Document";
|
import Document from "~/models/Document";
|
||||||
import ParanoidModel from "~/models/ParanoidModel";
|
import ParanoidModel from "~/models/ParanoidModel";
|
||||||
import { client } from "~/utils/ApiClient";
|
import { client } from "~/utils/ApiClient";
|
||||||
@@ -18,9 +18,6 @@ export default class Collection extends ParanoidModel {
|
|||||||
@observable
|
@observable
|
||||||
isSaving: boolean;
|
isSaving: boolean;
|
||||||
|
|
||||||
@observable
|
|
||||||
isLoadingUsers: boolean;
|
|
||||||
|
|
||||||
@Field
|
@Field
|
||||||
@observable
|
@observable
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -16,41 +16,11 @@ import useCurrentTeam from "~/hooks/useCurrentTeam";
|
|||||||
import usePolicy from "~/hooks/usePolicy";
|
import usePolicy from "~/hooks/usePolicy";
|
||||||
import { matchDocumentSlug as slug } from "~/utils/routeHelpers";
|
import { matchDocumentSlug as slug } from "~/utils/routeHelpers";
|
||||||
|
|
||||||
const SettingsRoutes = React.lazy(
|
const SettingsRoutes = React.lazy(() => import("./settings"));
|
||||||
() =>
|
const Document = React.lazy(() => import("~/scenes/Document"));
|
||||||
import(
|
const Collection = React.lazy(() => import("~/scenes/Collection"));
|
||||||
/* webpackChunkName: "settings" */
|
const Home = React.lazy(() => import("~/scenes/Home"));
|
||||||
"./settings"
|
const Search = React.lazy(() => import("~/scenes/Search"));
|
||||||
)
|
|
||||||
);
|
|
||||||
const Document = React.lazy(
|
|
||||||
() =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "preload-document" */
|
|
||||||
"~/scenes/Document"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const Collection = React.lazy(
|
|
||||||
() =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "collection" */
|
|
||||||
"~/scenes/Collection"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const Home = React.lazy(
|
|
||||||
() =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "home" */
|
|
||||||
"~/scenes/Home"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const Search = React.lazy(
|
|
||||||
() =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "search" */
|
|
||||||
"~/scenes/Search"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const RedirectDocument = ({
|
const RedirectDocument = ({
|
||||||
match,
|
match,
|
||||||
|
|||||||
@@ -6,41 +6,11 @@ import FullscreenLoading from "~/components/FullscreenLoading";
|
|||||||
import Route from "~/components/ProfiledRoute";
|
import Route from "~/components/ProfiledRoute";
|
||||||
import { matchDocumentSlug as slug } from "~/utils/routeHelpers";
|
import { matchDocumentSlug as slug } from "~/utils/routeHelpers";
|
||||||
|
|
||||||
const Authenticated = React.lazy(
|
const Authenticated = React.lazy(() => import("~/components/Authenticated"));
|
||||||
() =>
|
const AuthenticatedRoutes = React.lazy(() => import("./authenticated"));
|
||||||
import(
|
const SharedDocument = React.lazy(() => import("~/scenes/Document/Shared"));
|
||||||
/* webpackChunkName: "preload-authenticated" */
|
const Login = React.lazy(() => import("~/scenes/Login"));
|
||||||
"~/components/Authenticated"
|
const Logout = React.lazy(() => import("~/scenes/Logout"));
|
||||||
)
|
|
||||||
);
|
|
||||||
const AuthenticatedRoutes = React.lazy(
|
|
||||||
() =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "preload-authenticated-routes" */
|
|
||||||
"./authenticated"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const SharedDocument = React.lazy(
|
|
||||||
() =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "shared-document" */
|
|
||||||
"~/scenes/Document/Shared"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const Login = React.lazy(
|
|
||||||
() =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "login" */
|
|
||||||
"~/scenes/Login"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const Logout = React.lazy(
|
|
||||||
() =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "logout" */
|
|
||||||
"~/scenes/Logout"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
export default function Routes() {
|
export default function Routes() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
const MultiplayerEditor = React.lazy(
|
const MultiplayerEditor = React.lazy(() => import("./MultiplayerEditor"));
|
||||||
() =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "preload-multiplayer-editor" */
|
|
||||||
"./MultiplayerEditor"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
export default MultiplayerEditor;
|
export default MultiplayerEditor;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import invariant from "invariant";
|
import invariant from "invariant";
|
||||||
import { orderBy } from "lodash";
|
import { lowerFirst, orderBy } from "lodash";
|
||||||
import { observable, action, computed, runInAction } from "mobx";
|
import { observable, action, computed, runInAction } from "mobx";
|
||||||
import { Class } from "utility-types";
|
import { Class } from "utility-types";
|
||||||
import RootStore from "~/stores/RootStore";
|
import RootStore from "~/stores/RootStore";
|
||||||
@@ -20,10 +20,6 @@ export enum RPCAction {
|
|||||||
|
|
||||||
type FetchPageParams = PaginationParams & Record<string, any>;
|
type FetchPageParams = PaginationParams & Record<string, any>;
|
||||||
|
|
||||||
function modelNameFromClassName(string: string) {
|
|
||||||
return string.charAt(0).toLowerCase() + string.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEFAULT_PAGINATION_LIMIT = 25;
|
export const DEFAULT_PAGINATION_LIMIT = 25;
|
||||||
|
|
||||||
export const PAGINATION_SYMBOL = Symbol.for("pagination");
|
export const PAGINATION_SYMBOL = Symbol.for("pagination");
|
||||||
@@ -61,7 +57,7 @@ export default abstract class BaseStore<T extends BaseModel> {
|
|||||||
constructor(rootStore: RootStore, model: Class<T>) {
|
constructor(rootStore: RootStore, model: Class<T>) {
|
||||||
this.rootStore = rootStore;
|
this.rootStore = rootStore;
|
||||||
this.model = model;
|
this.model = model;
|
||||||
this.modelName = modelNameFromClassName(model.name);
|
this.modelName = lowerFirst(model.name);
|
||||||
|
|
||||||
if (!this.apiEndpoint) {
|
if (!this.apiEndpoint) {
|
||||||
this.apiEndpoint = `${this.modelName}s`;
|
this.apiEndpoint = `${this.modelName}s`;
|
||||||
|
|||||||
12
app/typings/window.d.ts
vendored
12
app/typings/window.d.ts
vendored
@@ -1,11 +1,9 @@
|
|||||||
declare global {
|
declare global {
|
||||||
interface NodeRequire {
|
interface ImportMeta {
|
||||||
/** A special feature supported by webpack's compiler that allows you to get all matching modules starting from some base directory. */
|
/**
|
||||||
context: (
|
* A special feature that allows you to get all matching modules starting from some base directory.
|
||||||
directory: string,
|
*/
|
||||||
useSubdirectories: boolean,
|
glob: (pattern: string, option?: { eager: boolean }) => any;
|
||||||
regExp: RegExp
|
|
||||||
) => any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
|
|||||||
@@ -13,25 +13,31 @@ export function loadPlugins(): { [id: string]: Plugin } {
|
|||||||
const plugins = {};
|
const plugins = {};
|
||||||
|
|
||||||
function importAll(r: any, property: string) {
|
function importAll(r: any, property: string) {
|
||||||
r.keys().forEach((key: string) => {
|
Object.keys(r).forEach((key: string) => {
|
||||||
const id = key.split("/")[1];
|
const id = key.split("/")[3];
|
||||||
plugins[id] = plugins[id] || {
|
plugins[id] = plugins[id] || {
|
||||||
id,
|
id,
|
||||||
};
|
};
|
||||||
|
plugins[id][property] = r[key].default;
|
||||||
const plugin = r(key);
|
|
||||||
plugins[id][property] = "default" in plugin ? plugin.default : plugin;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
importAll(
|
importAll(
|
||||||
require.context("../../plugins", true, /client\/Settings\.[tj]sx?$/),
|
import.meta.glob("../../plugins/*/client/Settings.{ts,js,tsx,jsx}", {
|
||||||
|
eager: true,
|
||||||
|
}),
|
||||||
"settings"
|
"settings"
|
||||||
);
|
);
|
||||||
importAll(
|
importAll(
|
||||||
require.context("../../plugins", true, /client\/Icon\.[tj]sx?$/),
|
import.meta.glob("../../plugins/*/client/Icon.{ts,js,tsx,jsx}", {
|
||||||
|
eager: true,
|
||||||
|
}),
|
||||||
"icon"
|
"icon"
|
||||||
);
|
);
|
||||||
importAll(require.context("../../plugins", true, /plugin\.json?$/), "config");
|
importAll(
|
||||||
|
import.meta.glob("../../plugins/*/plugin.json", { eager: true }),
|
||||||
|
"config"
|
||||||
|
);
|
||||||
|
|
||||||
return plugins;
|
return plugins;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,7 @@ export async function loadPolyfills() {
|
|||||||
|
|
||||||
if (!supportsResizeObserver()) {
|
if (!supportsResizeObserver()) {
|
||||||
polyfills.push(
|
polyfills.push(
|
||||||
import(
|
import("@juggle/resize-observer").then((module) => {
|
||||||
/* webpackChunkName: "resize-observer" */
|
|
||||||
"@juggle/resize-observer"
|
|
||||||
).then((module) => {
|
|
||||||
window.ResizeObserver = module.ResizeObserver;
|
window.ResizeObserver = module.ResizeObserver;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -27,8 +24,8 @@ export async function loadPolyfills() {
|
|||||||
*/
|
*/
|
||||||
function supportsResizeObserver() {
|
function supportsResizeObserver() {
|
||||||
return (
|
return (
|
||||||
"ResizeObserver" in global &&
|
"ResizeObserver" in window &&
|
||||||
"ResizeObserverEntry" in global &&
|
"ResizeObserverEntry" in window &&
|
||||||
"contentRect" in ResizeObserverEntry.prototype
|
"contentRect" in ResizeObserverEntry.prototype
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export function initSentry(history: History) {
|
|||||||
],
|
],
|
||||||
tracesSampleRate: env.ENVIRONMENT === "production" ? 0.1 : 1,
|
tracesSampleRate: env.ENVIRONMENT === "production" ? 0.1 : 1,
|
||||||
ignoreErrors: [
|
ignoreErrors: [
|
||||||
|
"Failed to fetch dynamically imported module",
|
||||||
"ResizeObserver loop completed with undelivered notifications",
|
"ResizeObserver loop completed with undelivered notifications",
|
||||||
"ResizeObserver loop limit exceeded",
|
"ResizeObserver loop limit exceeded",
|
||||||
"AuthorizationError",
|
"AuthorizationError",
|
||||||
@@ -26,8 +27,8 @@ export function initSentry(history: History) {
|
|||||||
"RateLimitExceededError",
|
"RateLimitExceededError",
|
||||||
"ServiceUnavailableError",
|
"ServiceUnavailableError",
|
||||||
"UpdateRequiredError",
|
"UpdateRequiredError",
|
||||||
"ChunkLoadError",
|
|
||||||
"file://",
|
"file://",
|
||||||
|
"chrome-extension://",
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
3
build.sh
3
build.sh
@@ -21,6 +21,3 @@ cp ./server/collaboration/Procfile ./build/server/collaboration/Procfile
|
|||||||
cp package.json ./build
|
cp package.json ./build
|
||||||
cp ./server/static/error.dev.html ./build/server/error.dev.html
|
cp ./server/static/error.dev.html ./build/server/error.dev.html
|
||||||
cp ./server/static/error.prod.html ./build/server/error.prod.html
|
cp ./server/static/error.prod.html ./build/server/error.prod.html
|
||||||
|
|
||||||
# Link webpack config
|
|
||||||
ln -sf "$(pwd)/webpack.config.dev.js" ./build
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Outline is composed of a backend and frontend codebase in this monorepo. As both
|
|||||||
|
|
||||||
## Frontend
|
## Frontend
|
||||||
|
|
||||||
Outline's frontend is a React application compiled with [Webpack](https://webpack.js.org/). It uses [MobX](https://mobx.js.org/) for state management and [Styled Components](https://www.styled-components.com/) for component styles. Unless global, state logic and styles are always co-located with React components together with their subcomponents to make the component tree easier to manage.
|
Outline's frontend is a React application compiled with [Vite](https://vitejs.dev/). It uses [MobX](https://mobx.js.org/) for state management and [Styled Components](https://www.styled-components.com/) for component styles. Unless global, state logic and styles are always co-located with React components together with their subcomponents to make the component tree easier to manage.
|
||||||
|
|
||||||
```
|
```
|
||||||
app
|
app
|
||||||
|
|||||||
35
package.json
35
package.json
@@ -8,14 +8,14 @@
|
|||||||
"copy:i18n": "mkdir -p ./build/shared/i18n && cp -R ./shared/i18n/locales ./build/shared/i18n",
|
"copy:i18n": "mkdir -p ./build/shared/i18n && cp -R ./shared/i18n/locales ./build/shared/i18n",
|
||||||
"build:i18n": "i18next --silent '{shared,app,server,plugins}/**/*.{ts,tsx}' && yarn copy:i18n",
|
"build:i18n": "i18next --silent '{shared,app,server,plugins}/**/*.{ts,tsx}' && yarn copy:i18n",
|
||||||
"build:server": "./build.sh",
|
"build:server": "./build.sh",
|
||||||
"build:webpack": "webpack --config webpack.config.prod.js",
|
"build": "yarn clean && yarn vite:build && yarn build:i18n && yarn build:server",
|
||||||
"build": "yarn clean && yarn build:webpack && yarn build:i18n && yarn build:server",
|
|
||||||
"start": "node ./build/server/index.js",
|
"start": "node ./build/server/index.js",
|
||||||
"dev": "NODE_ENV=development yarn concurrently -n api,collaboration -c \"blue,magenta\" \"node --inspect=0.0.0.0 build/server/index.js --services=collaboration,websockets,admin,web,worker\"",
|
"dev": "NODE_ENV=development yarn concurrently -n api,collaboration -c \"blue,magenta\" \"node --inspect=0.0.0.0 build/server/index.js --services=collaboration,websockets,admin,web,worker\"",
|
||||||
"dev:watch": "nodemon --exec \"yarn build:server && yarn dev\" -e js,ts,tsx --ignore build/ --ignore app/ --ignore shared/editor",
|
"dev:backend": "nodemon --exec \"yarn build:server && yarn dev\" -e js,ts,tsx --ignore build/ --ignore app/ --ignore shared/editor",
|
||||||
|
"dev:watch": "yarn concurrently -n backend,frontend \"yarn dev:backend\" \"yarn vite:dev\"",
|
||||||
"lint": "eslint app server shared plugins",
|
"lint": "eslint app server shared plugins",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"postinstall": "rimraf node_modules/prosemirror-view/dist/index.d.ts",
|
"postinstall": "rimraf node_modules/prosemirror-view/dist/index.d.ts && patch-package",
|
||||||
"heroku-postbuild": "yarn build && yarn db:migrate",
|
"heroku-postbuild": "yarn build && yarn db:migrate",
|
||||||
"db:create-migration": "sequelize migration:create",
|
"db:create-migration": "sequelize migration:create",
|
||||||
"db:create": "sequelize db:create",
|
"db:create": "sequelize db:create",
|
||||||
@@ -26,7 +26,10 @@
|
|||||||
"test": "jest --config=.jestconfig.json --forceExit",
|
"test": "jest --config=.jestconfig.json --forceExit",
|
||||||
"test:app": "jest --config=.jestconfig.json --selectProjects app",
|
"test:app": "jest --config=.jestconfig.json --selectProjects app",
|
||||||
"test:shared": "jest --config=.jestconfig.json --selectProjects shared-node shared-jsdom",
|
"test:shared": "jest --config=.jestconfig.json --selectProjects shared-node shared-jsdom",
|
||||||
"test:server": "jest --config=.jestconfig.json --selectProjects server"
|
"test:server": "jest --config=.jestconfig.json --selectProjects server",
|
||||||
|
"vite:dev": "vite",
|
||||||
|
"vite:build": "vite build",
|
||||||
|
"vite:preview": "vite preview"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "GitHub Sponsors ❤",
|
"type": "GitHub Sponsors ❤",
|
||||||
@@ -69,6 +72,7 @@
|
|||||||
"@tippyjs/react": "^4.2.6",
|
"@tippyjs/react": "^4.2.6",
|
||||||
"@tommoor/remove-markdown": "^0.3.2",
|
"@tommoor/remove-markdown": "^0.3.2",
|
||||||
"@types/mermaid": "^9.1.0",
|
"@types/mermaid": "^9.1.0",
|
||||||
|
"@vitejs/plugin-react": "^3.0.0",
|
||||||
"autotrack": "^2.4.1",
|
"autotrack": "^2.4.1",
|
||||||
"aws-sdk": "^2.1290.0",
|
"aws-sdk": "^2.1290.0",
|
||||||
"babel-plugin-lodash": "^3.3.4",
|
"babel-plugin-lodash": "^3.3.4",
|
||||||
@@ -218,6 +222,8 @@
|
|||||||
"utility-types": "^3.10.0",
|
"utility-types": "^3.10.0",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"validator": "13.7.0",
|
"validator": "13.7.0",
|
||||||
|
"vite": "^4.1.1",
|
||||||
|
"vite-plugin-pwa": "^0.14.4",
|
||||||
"winston": "^3.3.3",
|
"winston": "^3.3.3",
|
||||||
"ws": "^7.5.3",
|
"ws": "^7.5.3",
|
||||||
"y-indexeddb": "^9.0.9",
|
"y-indexeddb": "^9.0.9",
|
||||||
@@ -229,7 +235,6 @@
|
|||||||
"@babel/cli": "^7.10.5",
|
"@babel/cli": "^7.10.5",
|
||||||
"@babel/preset-typescript": "^7.18.6",
|
"@babel/preset-typescript": "^7.18.6",
|
||||||
"@getoutline/jest-runner-serial": "^2.0.0",
|
"@getoutline/jest-runner-serial": "^2.0.0",
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
|
|
||||||
"@relative-ci/agent": "^3.0.0",
|
"@relative-ci/agent": "^3.0.0",
|
||||||
"@types/body-scroll-lock": "^3.1.0",
|
"@types/body-scroll-lock": "^3.1.0",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
@@ -309,6 +314,7 @@
|
|||||||
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
|
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
|
||||||
"babel-plugin-transform-typescript-metadata": "^0.3.2",
|
"babel-plugin-transform-typescript-metadata": "^0.3.2",
|
||||||
"babel-plugin-tsconfig-paths-module-resolver": "^1.0.3",
|
"babel-plugin-tsconfig-paths-module-resolver": "^1.0.3",
|
||||||
|
"browserslist-to-esbuild": "^1.2.0",
|
||||||
"concurrently": "^7.4.0",
|
"concurrently": "^7.4.0",
|
||||||
"css-loader": "5.2.6",
|
"css-loader": "5.2.6",
|
||||||
"enzyme": "^3.11.0",
|
"enzyme": "^3.11.0",
|
||||||
@@ -324,28 +330,24 @@
|
|||||||
"eslint-plugin-react": "^7.20.0",
|
"eslint-plugin-react": "^7.20.0",
|
||||||
"eslint-plugin-react-hooks": "^4.1.0",
|
"eslint-plugin-react-hooks": "^4.1.0",
|
||||||
"fetch-test-server": "^1.1.0",
|
"fetch-test-server": "^1.1.0",
|
||||||
"html-webpack-plugin": "4.5.2",
|
|
||||||
"husky": "^8.0.2",
|
"husky": "^8.0.2",
|
||||||
"i18next-parser": "^7.1.0",
|
"i18next-parser": "^7.1.0",
|
||||||
"jest-cli": "^29.4.1",
|
"jest-cli": "^29.4.1",
|
||||||
"jest-environment-jsdom": "^28.1.3",
|
"jest-environment-jsdom": "^28.1.3",
|
||||||
"jest-fetch-mock": "^3.0.3",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"koa-webpack-dev-middleware": "^1.4.5",
|
|
||||||
"koa-webpack-hot-middleware": "^1.0.3",
|
|
||||||
"lint-staged": "^13.1.0",
|
"lint-staged": "^13.1.0",
|
||||||
"nodemon": "^2.0.20",
|
"nodemon": "^2.0.20",
|
||||||
|
"patch-package": "^6.5.1",
|
||||||
|
"postinstall-postinstall": "^2.1.0",
|
||||||
"prettier": "^2.0.5",
|
"prettier": "^2.0.5",
|
||||||
"react-refresh": "^0.14.0",
|
"react-refresh": "^0.14.0",
|
||||||
"rimraf": "^2.5.4",
|
"rimraf": "^2.5.4",
|
||||||
|
"rollup-plugin-webpack-stats": "^0.2.0",
|
||||||
"style-loader": "2.0.0",
|
"style-loader": "2.0.0",
|
||||||
"terser-webpack-plugin": "^4.1.0",
|
"terser": "^5.16.3",
|
||||||
"typescript": "^4.7.4",
|
"typescript": "^4.7.4",
|
||||||
"url-loader": "^4.1.1",
|
"url-loader": "^4.1.1",
|
||||||
"webpack": "4.46.0",
|
"vite-plugin-static-copy": "^0.13.0",
|
||||||
"webpack-cli": "^4.10.0",
|
|
||||||
"webpack-manifest-plugin": "^3.0.0",
|
|
||||||
"webpack-pwa-manifest": "^4.3.0",
|
|
||||||
"workbox-webpack-plugin": "^6.5.3",
|
|
||||||
"yarn-deduplicate": "^6.0.1"
|
"yarn-deduplicate": "^6.0.1"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
@@ -356,7 +358,8 @@
|
|||||||
"dot-prop": "^5.2.0",
|
"dot-prop": "^5.2.0",
|
||||||
"js-yaml": "^3.14.1",
|
"js-yaml": "^3.14.1",
|
||||||
"jpeg-js": "0.4.4",
|
"jpeg-js": "0.4.4",
|
||||||
"qs": "6.9.7"
|
"qs": "6.9.7",
|
||||||
|
"rollup": "^3.14.0"
|
||||||
},
|
},
|
||||||
"version": "0.67.2"
|
"version": "0.67.2"
|
||||||
}
|
}
|
||||||
|
|||||||
22
patches/@benrbray+prosemirror-math+0.2.2.patch
Normal file
22
patches/@benrbray+prosemirror-math+0.2.2.patch
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
diff --git a/node_modules/@benrbray/prosemirror-math/dist/index.es.js b/node_modules/@benrbray/prosemirror-math/dist/index.es.js
|
||||||
|
index b52d589..0b4cb17 100644
|
||||||
|
--- a/node_modules/@benrbray/prosemirror-math/dist/index.es.js
|
||||||
|
+++ b/node_modules/@benrbray/prosemirror-math/dist/index.es.js
|
||||||
|
@@ -3,7 +3,7 @@ import { EditorView, DecorationSet, Decoration } from 'prosemirror-view';
|
||||||
|
import { StepMap } from 'prosemirror-transform';
|
||||||
|
import { keymap } from 'prosemirror-keymap';
|
||||||
|
import { chainCommands, deleteSelection, newlineInCode } from 'prosemirror-commands';
|
||||||
|
-import katex, { ParseError } from 'katex';
|
||||||
|
+import katex from 'katex';
|
||||||
|
import { Fragment, Schema } from 'prosemirror-model';
|
||||||
|
import { InputRule } from 'prosemirror-inputrules';
|
||||||
|
|
||||||
|
@@ -213,7 +213,7 @@ class MathView {
|
||||||
|
this.dom.setAttribute("title", "");
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
- if (err instanceof ParseError) {
|
||||||
|
+ if (err instanceof katex.ParseError) {
|
||||||
|
console.error(err);
|
||||||
|
this._mathRenderElt.classList.add("parse-error");
|
||||||
|
this.dom.setAttribute("title", err.toString());
|
||||||
7
relativeci.config.js
Normal file
7
relativeci.config.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
module.exports = {
|
||||||
|
includeCommitMessage: true,
|
||||||
|
webpack: {
|
||||||
|
stats: "./build/app/webpack-stats.json",
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -13,7 +13,13 @@
|
|||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"babel-plugin-transform-typescript-metadata",
|
"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",
|
"transform-inline-environment-variables",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,45 +12,34 @@ import { Integration } from "@server/models";
|
|||||||
import presentEnv from "@server/presenters/env";
|
import presentEnv from "@server/presenters/env";
|
||||||
import { getTeamFromContext } from "@server/utils/passport";
|
import { getTeamFromContext } from "@server/utils/passport";
|
||||||
import prefetchTags from "@server/utils/prefetchTags";
|
import prefetchTags from "@server/utils/prefetchTags";
|
||||||
|
import readManifestFile from "@server/utils/readManifestFile";
|
||||||
|
|
||||||
const isProduction = env.ENVIRONMENT === "production";
|
const isProduction = env.ENVIRONMENT === "production";
|
||||||
|
const isDevelopment = env.ENVIRONMENT === "development";
|
||||||
const isTest = env.ENVIRONMENT === "test";
|
const isTest = env.ENVIRONMENT === "test";
|
||||||
const readFile = util.promisify(fs.readFile);
|
const readFile = util.promisify(fs.readFile);
|
||||||
let indexHtmlCache: Buffer | undefined;
|
let indexHtmlCache: Buffer | undefined;
|
||||||
|
|
||||||
const readIndexFile = async (ctx: Context): Promise<Buffer> => {
|
const readIndexFile = async (): Promise<Buffer> => {
|
||||||
if (isProduction) {
|
if (isProduction || isTest) {
|
||||||
return (
|
if (indexHtmlCache) {
|
||||||
indexHtmlCache ??
|
return indexHtmlCache;
|
||||||
(indexHtmlCache = await readFile(
|
}
|
||||||
path.join(__dirname, "../../app/index.html")
|
|
||||||
))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTest) {
|
if (isTest) {
|
||||||
return (
|
return await readFile(path.join(__dirname, "../static/index.html"));
|
||||||
indexHtmlCache ??
|
}
|
||||||
(indexHtmlCache = await readFile(
|
|
||||||
path.join(__dirname, "../static/index.html")
|
if (isDevelopment) {
|
||||||
))
|
return await readFile(
|
||||||
|
path.join(__dirname, "../../../server/static/index.html")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const middleware = ctx.devMiddleware;
|
return (indexHtmlCache = await readFile(
|
||||||
await new Promise((resolve) => middleware.waitUntilValid(resolve));
|
path.join(__dirname, "../../app/index.html")
|
||||||
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);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const renderApp = async (
|
export const renderApp = async (
|
||||||
@@ -74,10 +63,26 @@ export const renderApp = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { shareId } = ctx.params;
|
const { shareId } = ctx.params;
|
||||||
const page = await readIndexFile(ctx);
|
const page = await readIndexFile();
|
||||||
const environment = `
|
const environment = `
|
||||||
window.env = ${JSON.stringify(presentEnv(env, options.analytics))};
|
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
|
ctx.body = page
|
||||||
.toString()
|
.toString()
|
||||||
.replace(/\/\/inject-env\/\//g, environment)
|
.replace(/\/\/inject-env\/\//g, environment)
|
||||||
@@ -85,7 +90,8 @@ export const renderApp = async (
|
|||||||
.replace(/\/\/inject-description\/\//g, escape(description))
|
.replace(/\/\/inject-description\/\//g, escape(description))
|
||||||
.replace(/\/\/inject-canonical\/\//g, canonical)
|
.replace(/\/\/inject-canonical\/\//g, canonical)
|
||||||
.replace(/\/\/inject-prefetch\/\//g, shareId ? "" : prefetchTags)
|
.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) => {
|
export const renderShare = async (ctx: Context, next: Next) => {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import api from "../routes/api";
|
|||||||
import auth from "../routes/auth";
|
import auth from "../routes/auth";
|
||||||
|
|
||||||
const isProduction = env.ENVIRONMENT === "production";
|
const isProduction = env.ENVIRONMENT === "production";
|
||||||
const isTest = env.ENVIRONMENT === "test";
|
|
||||||
|
|
||||||
// Construct scripts CSP based on services in use by this installation
|
// Construct scripts CSP based on services in use by this installation
|
||||||
const defaultSrc = ["'self'"];
|
const defaultSrc = ["'self'"];
|
||||||
@@ -31,6 +30,12 @@ const scriptSrc = [
|
|||||||
"cdn.zapier.com",
|
"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) {
|
if (env.GOOGLE_ANALYTICS_ID) {
|
||||||
scriptSrc.push("www.google-analytics.com");
|
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
|
// trust header fields set by our proxy. eg X-Forwarded-For
|
||||||
app.proxy = true;
|
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));
|
app.use(mount("/auth", auth));
|
||||||
|
|||||||
@@ -70,5 +70,6 @@
|
|||||||
.setAttribute("content", color);
|
.setAttribute("content", color);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
//inject-script-tags//
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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 "fetch-test-server";
|
||||||
|
|
||||||
|
declare module "dotenv";
|
||||||
|
|
||||||
declare module "email-providers" {
|
declare module "email-providers" {
|
||||||
const list: string[];
|
const list: string[];
|
||||||
export default list;
|
export default list;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import ReactDOMServer from "react-dom/server";
|
import ReactDOMServer from "react-dom/server";
|
||||||
import env from "@server/env";
|
import env from "@server/env";
|
||||||
|
import readManifestFile, { ManifestStructure } from "./readManifestFile";
|
||||||
|
|
||||||
|
const isProduction = env.ENVIRONMENT === "production";
|
||||||
|
|
||||||
const prefetchTags = [];
|
const prefetchTags = [];
|
||||||
|
|
||||||
@@ -16,45 +17,47 @@ if (process.env.AWS_S3_UPLOAD_BUCKET_URL) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let manifestData = {};
|
if (isProduction) {
|
||||||
|
const manifest = readManifestFile();
|
||||||
|
|
||||||
try {
|
const returnFileAndImportsFromManifest = (
|
||||||
const manifest = fs.readFileSync(
|
manifest: ManifestStructure,
|
||||||
path.join(__dirname, "../../app/manifest.json"),
|
file: string
|
||||||
"utf8"
|
): string[] => {
|
||||||
);
|
return [
|
||||||
manifestData = JSON.parse(manifest);
|
manifest[file]["file"],
|
||||||
} catch (err) {
|
...manifest[file]["imports"].map((entry: string) => {
|
||||||
// no-op
|
return manifest[entry]["file"];
|
||||||
}
|
}),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
Object.values(manifestData).forEach((filename) => {
|
Array.from([
|
||||||
if (typeof filename !== "string") {
|
...returnFileAndImportsFromManifest(manifest, "app/index.tsx"),
|
||||||
return;
|
...returnFileAndImportsFromManifest(manifest, "app/editor/index.tsx"),
|
||||||
}
|
]).forEach((file) => {
|
||||||
if (!env.CDN_URL) {
|
if (file.endsWith(".js")) {
|
||||||
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) {
|
|
||||||
prefetchTags.push(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isProduction = env.ENVIRONMENT === "production";
|
||||||
const teams = await Team.count();
|
const teams = await Team.count();
|
||||||
const providers = await AuthenticationProvider.count();
|
const providers = await AuthenticationProvider.count();
|
||||||
|
|
||||||
if (teams && !providers) {
|
if (isProduction && teams && !providers) {
|
||||||
Logger.warn(`
|
Logger.warn(`
|
||||||
This version of Outline cannot start until a data migration is complete.
|
This version of Outline cannot start until a data migration is complete.
|
||||||
Backup your database, run the database migrations and the following script:
|
Backup your database, run the database migrations and the following script:
|
||||||
|
|||||||
@@ -18,10 +18,7 @@ export function createMathView(displayMode: boolean) {
|
|||||||
getPos: boolean | (() => number)
|
getPos: boolean | (() => number)
|
||||||
): MathView => {
|
): MathView => {
|
||||||
// dynamically load katex styles and fonts
|
// dynamically load katex styles and fonts
|
||||||
import(
|
import("katex/dist/katex.min.css");
|
||||||
/* webpackChunkName: "katex" */
|
|
||||||
"katex/dist/katex.min.css"
|
|
||||||
);
|
|
||||||
|
|
||||||
const pluginState = MATH_PLUGIN_KEY.getState(view.state);
|
const pluginState = MATH_PLUGIN_KEY.getState(view.state);
|
||||||
if (!pluginState) {
|
if (!pluginState) {
|
||||||
|
|||||||
@@ -57,10 +57,7 @@ function getNewState({
|
|||||||
return diagramWrapper;
|
return diagramWrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
import(
|
import("mermaid").then((module) => {
|
||||||
/* webpackChunkName: "mermaid" */
|
|
||||||
"mermaid"
|
|
||||||
).then((module) => {
|
|
||||||
module.default.initialize({
|
module.default.initialize({
|
||||||
startOnLoad: true,
|
startOnLoad: true,
|
||||||
flowchart: {
|
flowchart: {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
"suppressImplicitAnyIndexErrors": true,
|
"suppressImplicitAnyIndexErrors": true,
|
||||||
"target": "es2020",
|
"target": "es2020",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
|||||||
120
vite.config.ts
Normal file
120
vite.config.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import path from "path";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import browserslistToEsbuild from "browserslist-to-esbuild";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import { webpackStats } from "rollup-plugin-webpack-stats";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import { VitePWA } from "vite-plugin-pwa";
|
||||||
|
import { viteStaticCopy } from "vite-plugin-static-copy";
|
||||||
|
|
||||||
|
// Load the process environment variables
|
||||||
|
dotenv.config({
|
||||||
|
silent: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return defineConfig({
|
||||||
|
root: "./",
|
||||||
|
publicDir: "./server/static",
|
||||||
|
base: "/static/",
|
||||||
|
server: {
|
||||||
|
port: 3001,
|
||||||
|
host: true,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
// https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#readme
|
||||||
|
react({
|
||||||
|
babel: {
|
||||||
|
parserOpts: {
|
||||||
|
plugins: ["decorators-legacy", "classProperties"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
// https://github.com/sapphi-red/vite-plugin-static-copy#readme
|
||||||
|
viteStaticCopy({
|
||||||
|
targets: [
|
||||||
|
{
|
||||||
|
src: "./public/images",
|
||||||
|
dest: "./",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
// https://vite-pwa-org.netlify.app/
|
||||||
|
VitePWA({
|
||||||
|
injectRegister: "inline",
|
||||||
|
registerType: "autoUpdate",
|
||||||
|
workbox: {
|
||||||
|
globPatterns: ["**/*.{js,css,ico,png,svg}"],
|
||||||
|
navigateFallback: null,
|
||||||
|
modifyURLPrefix: {
|
||||||
|
"": `${process.env.CDN_URL ?? ""}/static/`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
manifest: {
|
||||||
|
name: "Outline",
|
||||||
|
short_name: "Outline",
|
||||||
|
// description: "My Awesome App description",
|
||||||
|
theme_color: "#fff",
|
||||||
|
background_color: "#fff",
|
||||||
|
start_url: "/",
|
||||||
|
display: "standalone",
|
||||||
|
// For Chrome, you must provide at least a 192x192 pixel icon, and a 512x512 pixel icon.
|
||||||
|
// If only those two icon sizes are provided, Chrome will automatically scale the icons
|
||||||
|
// to fit the device. If you'd prefer to scale your own icons, and adjust them for
|
||||||
|
// pixel-perfection, provide icons in increments of 48dp.
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: "images/icon-512.png",
|
||||||
|
sizes: "192x192",
|
||||||
|
type: "image/png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "images/icon-512.png",
|
||||||
|
sizes: "512x512",
|
||||||
|
type: "image/png",
|
||||||
|
},
|
||||||
|
// last one duplicated for purpose: 'any maskable'
|
||||||
|
{
|
||||||
|
src: "images/icon-512.png",
|
||||||
|
sizes: "512x512",
|
||||||
|
type: "image/png",
|
||||||
|
purpose: "any maskable",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
optimizeDeps: {
|
||||||
|
esbuildOptions: {
|
||||||
|
keepNames: true,
|
||||||
|
define: {
|
||||||
|
global: "globalThis",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"~": path.resolve(__dirname, "./app"),
|
||||||
|
"@shared": path.resolve(__dirname, "./shared"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: "./build/app",
|
||||||
|
manifest: true,
|
||||||
|
sourcemap: true,
|
||||||
|
minify: "terser",
|
||||||
|
target: browserslistToEsbuild(),
|
||||||
|
reportCompressedSize: false,
|
||||||
|
terserOptions: {
|
||||||
|
keep_classnames: true,
|
||||||
|
keep_fnames: true,
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
index: "./app/index.tsx",
|
||||||
|
},
|
||||||
|
plugins: [webpackStats()],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
const webpack = require("webpack");
|
|
||||||
const commonWebpackConfig = require("./webpack.config");
|
|
||||||
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
|
|
||||||
|
|
||||||
const isTest = process.env.NODE_ENV === "test";
|
|
||||||
|
|
||||||
const developmentWebpackConfig = Object.assign(commonWebpackConfig, {
|
|
||||||
cache: true,
|
|
||||||
mode: "development",
|
|
||||||
devtool: "eval-cheap-module-source-map",
|
|
||||||
entry: [
|
|
||||||
"webpack-hot-middleware/client",
|
|
||||||
"./app/index",
|
|
||||||
],
|
|
||||||
optimization: {
|
|
||||||
usedExports: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isTest) {
|
|
||||||
developmentWebpackConfig.plugins = [
|
|
||||||
...developmentWebpackConfig.plugins,
|
|
||||||
new webpack.HotModuleReplacementPlugin(),
|
|
||||||
new ReactRefreshWebpackPlugin({
|
|
||||||
overlay: {
|
|
||||||
sockIntegration: 'whm',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = developmentWebpackConfig;
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
const path = require('path');
|
|
||||||
const webpack = require('webpack');
|
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
||||||
const { RelativeCiAgentWebpackPlugin } = require('@relative-ci/agent');
|
|
||||||
const WebpackPwaManifest = require("webpack-pwa-manifest");
|
|
||||||
const WorkboxPlugin = require("workbox-webpack-plugin");
|
|
||||||
|
|
||||||
require('dotenv').config({ silent: true });
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
output: {
|
|
||||||
path: path.join(__dirname, 'build/app'),
|
|
||||||
filename: '[name].[hash].js',
|
|
||||||
publicPath: '/static/',
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.[jt]sx?$/,
|
|
||||||
loader: 'babel-loader',
|
|
||||||
exclude: [
|
|
||||||
path.join(__dirname, 'node_modules')
|
|
||||||
],
|
|
||||||
include: [
|
|
||||||
path.join(__dirname, 'app'),
|
|
||||||
path.join(__dirname, 'shared'),
|
|
||||||
path.join(__dirname, 'plugins'),
|
|
||||||
],
|
|
||||||
options: {
|
|
||||||
cacheDirectory: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// inline base64 URLs for <=8k images, direct URLs for the rest
|
|
||||||
{ test: /\.(png|jpg|svg)$/, loader: 'url-loader' },
|
|
||||||
{
|
|
||||||
test: /\.(woff|woff2|ttf|eot)$/,
|
|
||||||
loader:
|
|
||||||
'url-loader?limit=1&mimetype=application/font-woff&name=public/fonts/[name].[ext]',
|
|
||||||
},
|
|
||||||
{ test: /\.md/, loader: 'raw-loader' },
|
|
||||||
{
|
|
||||||
test: /\.css$/i,
|
|
||||||
use: ['style-loader', 'css-loader'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.ts', '.tsx', '.js', '.json'],
|
|
||||||
modules: [
|
|
||||||
path.resolve(__dirname, 'app'),
|
|
||||||
'node_modules'
|
|
||||||
],
|
|
||||||
alias: {
|
|
||||||
"~": path.resolve(__dirname, 'app'),
|
|
||||||
"@shared": path.resolve(__dirname, 'shared'),
|
|
||||||
"@server": path.resolve(__dirname, 'server'),
|
|
||||||
'boundless-popover': 'boundless-popover/build',
|
|
||||||
'boundless-utils-omit-keys': 'boundless-utils-omit-keys/build',
|
|
||||||
'boundless-utils-uuid': 'boundless-utils-uuid/build'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new webpack.IgnorePlugin(/unicode\/category\/So/),
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
template: 'server/static/index.html',
|
|
||||||
}),
|
|
||||||
new WebpackPwaManifest({
|
|
||||||
name: "Outline",
|
|
||||||
short_name: "Outline",
|
|
||||||
background_color: "#fff",
|
|
||||||
theme_color: "#fff",
|
|
||||||
start_url: "/",
|
|
||||||
publicPath: "/static/",
|
|
||||||
display: "standalone",
|
|
||||||
icons: [
|
|
||||||
{
|
|
||||||
src: path.resolve("public/images/icon-512.png"),
|
|
||||||
// For Chrome, you must provide at least a 192x192 pixel icon, and a 512x512 pixel icon.
|
|
||||||
// If only those two icon sizes are provided, Chrome will automatically scale the icons
|
|
||||||
// to fit the device. If you'd prefer to scale your own icons, and adjust them for
|
|
||||||
// pixel-perfection, provide icons in increments of 48dp.
|
|
||||||
sizes: [512, 192],
|
|
||||||
purpose: "any maskable",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}),
|
|
||||||
new WorkboxPlugin.GenerateSW({
|
|
||||||
clientsClaim: true,
|
|
||||||
skipWaiting: true,
|
|
||||||
maximumFileSizeToCacheInBytes: 15 * 1024 * 1024, // For large bundles
|
|
||||||
}),
|
|
||||||
new RelativeCiAgentWebpackPlugin(),
|
|
||||||
],
|
|
||||||
stats: {
|
|
||||||
assets: false,
|
|
||||||
},
|
|
||||||
optimization: {
|
|
||||||
runtimeChunk: 'single',
|
|
||||||
moduleIds: 'hashed',
|
|
||||||
chunkIds: 'named',
|
|
||||||
splitChunks: {
|
|
||||||
chunks: 'async',
|
|
||||||
minSize: 20000,
|
|
||||||
minChunks: 1,
|
|
||||||
maxAsyncRequests: 30,
|
|
||||||
maxInitialRequests: 30,
|
|
||||||
enforceSizeThreshold: 50000,
|
|
||||||
cacheGroups: {
|
|
||||||
defaultVendors: {
|
|
||||||
test: /[\\/]node_modules[\\/]/,
|
|
||||||
priority: -10,
|
|
||||||
reuseExistingChunk: true,
|
|
||||||
},
|
|
||||||
default: {
|
|
||||||
minChunks: 2,
|
|
||||||
priority: -20,
|
|
||||||
reuseExistingChunk: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
const path = require('path');
|
|
||||||
const webpack = require('webpack');
|
|
||||||
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
|
|
||||||
const TerserPlugin = require('terser-webpack-plugin');
|
|
||||||
|
|
||||||
commonWebpackConfig = require('./webpack.config');
|
|
||||||
|
|
||||||
productionWebpackConfig = Object.assign(commonWebpackConfig, {
|
|
||||||
output: {
|
|
||||||
path: path.join(__dirname, 'build/app'),
|
|
||||||
filename: '[name].[contenthash].js',
|
|
||||||
publicPath: `${process.env.CDN_URL || ""}/static/`,
|
|
||||||
},
|
|
||||||
cache: true,
|
|
||||||
mode: "production",
|
|
||||||
devtool: 'source-map',
|
|
||||||
entry: ['./app/index'],
|
|
||||||
stats: "normal",
|
|
||||||
optimization: {
|
|
||||||
...commonWebpackConfig.optimization,
|
|
||||||
minimizer: [
|
|
||||||
new TerserPlugin({
|
|
||||||
terserOptions: {
|
|
||||||
ecma: undefined,
|
|
||||||
parse: {},
|
|
||||||
compress: {},
|
|
||||||
mangle: true, // Note `mangle.properties` is `false` by default.
|
|
||||||
module: false,
|
|
||||||
output: null,
|
|
||||||
toplevel: false,
|
|
||||||
nameCache: null,
|
|
||||||
ie8: false,
|
|
||||||
keep_classnames: undefined,
|
|
||||||
keep_fnames: true,
|
|
||||||
safari10: false,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
productionWebpackConfig.plugins = [
|
|
||||||
...productionWebpackConfig.plugins,
|
|
||||||
new WebpackManifestPlugin()
|
|
||||||
];
|
|
||||||
|
|
||||||
module.exports = productionWebpackConfig;
|
|
||||||
Reference in New Issue
Block a user