mirror of
https://github.com/facebook/docusaurus.git
synced 2025-08-03 16:59:06 +02:00
chore: upgrade TypeScript & other ESLint related deps (#5963)
* chore: upgrade ESLint related deps * Upgrade TS * Fix lock * Bump Babel * Update config
This commit is contained in:
parent
2f7d6fea1e
commit
0374426ce3
104 changed files with 2662 additions and 2487 deletions
|
@ -91,7 +91,7 @@ export default async function choosePort(
|
|||
(port) =>
|
||||
new Promise((resolve) => {
|
||||
if (port === defaultPort) {
|
||||
return resolve(port);
|
||||
resolve(port);
|
||||
}
|
||||
const message =
|
||||
process.platform !== 'win32' && defaultPort < 1024 && !isRoot()
|
||||
|
@ -121,7 +121,6 @@ export default async function choosePort(
|
|||
console.log(chalk.red(message));
|
||||
resolve(null);
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
(err) => {
|
||||
throw new Error(
|
||||
|
|
|
@ -22,9 +22,7 @@ export const createStatefulLinksCollector = (): StatefulLinksCollector => {
|
|||
collectLink: (link: string): void => {
|
||||
allLinks.add(link);
|
||||
},
|
||||
getCollectedLinks: (): string[] => {
|
||||
return [...allLinks];
|
||||
},
|
||||
getCollectedLinks: (): string[] => [...allLinks],
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -35,16 +33,14 @@ const Context = createContext<LinksCollector>({
|
|||
},
|
||||
});
|
||||
|
||||
export const useLinksCollector = (): LinksCollector => {
|
||||
return useContext(Context);
|
||||
};
|
||||
export const useLinksCollector = (): LinksCollector => useContext(Context);
|
||||
|
||||
export const ProvideLinksCollector = ({
|
||||
export function ProvideLinksCollector({
|
||||
children,
|
||||
linksCollector,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
linksCollector: LinksCollector;
|
||||
}): JSX.Element => {
|
||||
}): JSX.Element {
|
||||
return <Context.Provider value={linksCollector}>{children}</Context.Provider>;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -49,19 +49,18 @@ const flatten = <T>(arrays: T[][]): T[] =>
|
|||
// output: /blog/2018/12/14/Happy-First-Birthday-Slash
|
||||
const removeRouteNameHash = (str: string) => str.replace(/(-[^-]+)$/, '');
|
||||
|
||||
const getChunkNamesToLoad = (path: string): string[] => {
|
||||
return flatten(
|
||||
const getChunkNamesToLoad = (path: string): string[] =>
|
||||
flatten(
|
||||
Object.entries(routesChunkNames)
|
||||
.filter(
|
||||
([routeNameWithHash]) =>
|
||||
removeRouteNameHash(routeNameWithHash) === path,
|
||||
)
|
||||
.map(([, routeChunks]) => {
|
||||
.map(([, routeChunks]) =>
|
||||
// flat() is useful for nested chunk names, it's not like array.flat()
|
||||
return Object.values(flat(routeChunks));
|
||||
}),
|
||||
Object.values(flat(routeChunks)),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const docusaurus = {
|
||||
prefetch: (routePath: string): boolean => {
|
||||
|
@ -82,6 +81,7 @@ const docusaurus = {
|
|||
chunkNamesNeeded.forEach((chunkName) => {
|
||||
// "__webpack_require__.gca" is a custom function provided by ChunkAssetPlugin.
|
||||
// Pass it the chunkName or chunkId you want to load and it will return the URL for that chunk.
|
||||
// eslint-disable-next-line camelcase
|
||||
const chunkAsset = __webpack_require__.gca(chunkName);
|
||||
|
||||
// In some cases, webpack might decide to optimize further & hence the chunk assets are merged to another chunk/previous chunk.
|
||||
|
|
|
@ -68,23 +68,24 @@ export function interpolate<Str extends string, Value extends ReactNode>(
|
|||
else if (elements.every((el) => typeof el === 'string')) {
|
||||
return processedText
|
||||
.split(ValueFoundMarker)
|
||||
.reduce<string>((str, value, index) => {
|
||||
return str.concat(value).concat((elements[index] as string) ?? '');
|
||||
}, '');
|
||||
.reduce<string>(
|
||||
(str, value, index) =>
|
||||
str.concat(value).concat((elements[index] as string) ?? ''),
|
||||
'',
|
||||
);
|
||||
}
|
||||
// JSX interpolation: returns ReactNode
|
||||
else {
|
||||
return processedText
|
||||
.split(ValueFoundMarker)
|
||||
.reduce<ReactNode[]>((array, value, index) => {
|
||||
return [
|
||||
...array,
|
||||
<React.Fragment key={index}>
|
||||
{value}
|
||||
{elements[index]}
|
||||
</React.Fragment>,
|
||||
];
|
||||
}, []);
|
||||
return processedText.split(ValueFoundMarker).reduce<ReactNode[]>(
|
||||
(array, value, index) => [
|
||||
...array,
|
||||
<React.Fragment key={index}>
|
||||
{value}
|
||||
{elements[index]}
|
||||
</React.Fragment>,
|
||||
],
|
||||
[],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -45,9 +45,7 @@ export function useBaseUrlUtils(): BaseUrlUtils {
|
|||
const {siteConfig: {baseUrl = '/', url: siteUrl} = {}} =
|
||||
useDocusaurusContext();
|
||||
return {
|
||||
withBaseUrl: (url, options) => {
|
||||
return addBaseUrl(siteUrl, baseUrl, url, options);
|
||||
},
|
||||
withBaseUrl: (url, options) => addBaseUrl(siteUrl, baseUrl, url, options),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -28,11 +28,11 @@ import chalk from 'chalk';
|
|||
// eslint-disable-next-line no-restricted-imports
|
||||
import {memoize} from 'lodash';
|
||||
|
||||
const getCompiledSSRTemplate = memoize((template) => {
|
||||
return eta.compile(template.trim(), {
|
||||
const getCompiledSSRTemplate = memoize((template) =>
|
||||
eta.compile(template.trim(), {
|
||||
rmWhitespace: true,
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
function renderSSRTemplate(ssrTemplate, data) {
|
||||
const compiled = getCompiledSSRTemplate(ssrTemplate);
|
||||
|
|
|
@ -20,7 +20,7 @@ function Layout(props) {
|
|||
<>
|
||||
<Head defaultTitle={`${defaultTitle}${tagline ? ` · ${tagline}` : ''}`}>
|
||||
{title && <title>{`${title} · ${tagline}`}</title>}
|
||||
{favicon && <link rel="shortcut icon" href={faviconUrl} />}
|
||||
{favicon && <link rel="icon" href={faviconUrl} />}
|
||||
{description && <meta name="description" content={description} />}
|
||||
{description && (
|
||||
<meta property="og:description" content={description} />
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
export default ({error, retry, pastDelay}) => {
|
||||
export default function Loading({error, retry, pastDelay}) {
|
||||
if (error) {
|
||||
return (
|
||||
<div
|
||||
|
@ -133,4 +133,4 @@ export default ({error, retry, pastDelay}) => {
|
|||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
// Wrapper at the very top of the app, that is applied constantly
|
||||
// and does not depend on current route (unlike the layout)
|
||||
//
|
||||
|
@ -15,7 +13,7 @@ import React from 'react';
|
|||
//
|
||||
// See https://github.com/facebook/docusaurus/issues/3919
|
||||
function Root({children}) {
|
||||
return <>{children}</>;
|
||||
return children;
|
||||
}
|
||||
|
||||
export default Root;
|
||||
|
|
|
@ -75,9 +75,9 @@ export function getAllBrokenLinks({
|
|||
}): Record<string, BrokenLink[]> {
|
||||
const filteredRoutes = filterIntermediateRoutes(routes);
|
||||
|
||||
const allBrokenLinks = mapValues(allCollectedLinks, (pageLinks, pagePath) => {
|
||||
return getPageBrokenLinks({pageLinks, pagePath, routes: filteredRoutes});
|
||||
});
|
||||
const allBrokenLinks = mapValues(allCollectedLinks, (pageLinks, pagePath) =>
|
||||
getPageBrokenLinks({pageLinks, pagePath, routes: filteredRoutes}),
|
||||
);
|
||||
|
||||
// remove pages without any broken link
|
||||
return pickBy(allBrokenLinks, (brokenLinks) => brokenLinks.length > 0);
|
||||
|
@ -186,9 +186,9 @@ export async function filterExistingFileLinks({
|
|||
return filePathsToTry.some(isExistingFile);
|
||||
}
|
||||
|
||||
return mapValues(allCollectedLinks, (links) => {
|
||||
return links.filter((link) => !linkFileExists(link));
|
||||
});
|
||||
return mapValues(allCollectedLinks, (links) =>
|
||||
links.filter((link) => !linkFileExists(link)),
|
||||
);
|
||||
}
|
||||
|
||||
export async function handleBrokenLinks({
|
||||
|
|
|
@ -108,7 +108,7 @@ const I18N_CONFIG_SCHEMA = Joi.object<I18nConfig>({
|
|||
.optional()
|
||||
.default(DEFAULT_I18N_CONFIG);
|
||||
|
||||
const SiteUrlSchema = URISchema.required().custom(function (value, helpers) {
|
||||
const SiteUrlSchema = URISchema.required().custom((value, helpers) => {
|
||||
try {
|
||||
const {pathname} = new URL(value);
|
||||
if (pathname !== '/') {
|
||||
|
@ -124,7 +124,7 @@ const SiteUrlSchema = URISchema.required().custom(function (value, helpers) {
|
|||
export const ConfigSchema = Joi.object({
|
||||
baseUrl: Joi.string()
|
||||
.required()
|
||||
.regex(new RegExp('/$', 'm'))
|
||||
.regex(/\/$/m)
|
||||
.message('{{#label}} must be a string with a trailing slash.'),
|
||||
baseUrlIssueBanner: Joi.boolean().default(DEFAULT_CONFIG.baseUrlIssueBanner),
|
||||
favicon: Joi.string().optional(),
|
||||
|
|
|
@ -14,9 +14,7 @@ import chalk from 'chalk';
|
|||
function getDefaultLocaleLabel(locale: string) {
|
||||
// Intl.DisplayNames is ES2021 - Node14+
|
||||
// https://v8.dev/features/intl-displaynames
|
||||
// @ts-expect-error: wait for TS support of ES2021 feature
|
||||
if (typeof Intl.DisplayNames !== 'undefined') {
|
||||
// @ts-expect-error: wait for TS support of ES2021 feature
|
||||
return new Intl.DisplayNames([locale], {type: 'language'}).of(locale);
|
||||
}
|
||||
return locale;
|
||||
|
@ -76,9 +74,10 @@ Note: Docusaurus only support running one locale at a time.`,
|
|||
};
|
||||
}
|
||||
|
||||
const localeConfigs = locales.reduce((acc, locale) => {
|
||||
return {...acc, [locale]: getLocaleConfig(locale)};
|
||||
}, {});
|
||||
const localeConfigs = locales.reduce(
|
||||
(acc, locale) => ({...acc, [locale]: getLocaleConfig(locale)}),
|
||||
{},
|
||||
);
|
||||
|
||||
return {
|
||||
defaultLocale: i18nConfig.defaultLocale,
|
||||
|
|
|
@ -124,12 +124,12 @@ export async function loadPlugins({
|
|||
|
||||
const allContent: AllContent = chain(loadedPlugins)
|
||||
.groupBy((item) => item.name)
|
||||
.mapValues((nameItems) => {
|
||||
return chain(nameItems)
|
||||
.mapValues((nameItems) =>
|
||||
chain(nameItems)
|
||||
.groupBy((item) => item.options.id ?? DEFAULT_PLUGIN_ID)
|
||||
.mapValues((idItems) => idItems[0].content)
|
||||
.value();
|
||||
})
|
||||
.value(),
|
||||
)
|
||||
.value();
|
||||
|
||||
// 3. Plugin Lifecycle - contentLoaded.
|
||||
|
|
|
@ -19,9 +19,9 @@ export function sortAliases(aliases: ThemeAliases): ThemeAliases {
|
|||
// Alphabetical order by default
|
||||
const entries = sortBy(Object.entries(aliases), ([alias]) => alias);
|
||||
// @theme/NavbarItem should be after @theme/NavbarItem/LocaleDropdown
|
||||
entries.sort(([alias1], [alias2]) => {
|
||||
return alias1.includes(`${alias2}/`) ? -1 : 0;
|
||||
});
|
||||
entries.sort(([alias1], [alias2]) =>
|
||||
alias1.includes(`${alias2}/`) ? -1 : 0,
|
||||
);
|
||||
return Object.fromEntries(entries);
|
||||
}
|
||||
|
||||
|
|
|
@ -271,9 +271,10 @@ export async function getPluginsDefaultCodeTranslationMessages(
|
|||
plugins.map((plugin) => plugin.getDefaultCodeTranslationMessages?.() ?? {}),
|
||||
);
|
||||
|
||||
return pluginsMessages.reduce((allMessages, pluginMessages) => {
|
||||
return {...allMessages, ...pluginMessages};
|
||||
}, {});
|
||||
return pluginsMessages.reduce(
|
||||
(allMessages, pluginMessages) => ({...allMessages, ...pluginMessages}),
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
export function applyDefaultCodeTranslations({
|
||||
|
@ -298,11 +299,9 @@ Please report this Docusaurus issue.
|
|||
|
||||
return mapValues(
|
||||
extractedCodeTranslations,
|
||||
(messageTranslation, messageId) => {
|
||||
return {
|
||||
...messageTranslation,
|
||||
message: defaultCodeMessages[messageId] ?? messageTranslation.message,
|
||||
};
|
||||
},
|
||||
(messageTranslation, messageId) => ({
|
||||
...messageTranslation,
|
||||
message: defaultCodeMessages[messageId] ?? messageTranslation.message,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -85,9 +85,10 @@ export async function extractSiteSourceCodeTranslations(
|
|||
function toTranslationFileContent(
|
||||
sourceCodeFileTranslations: SourceCodeFileTranslations[],
|
||||
): TranslationFileContent {
|
||||
return sourceCodeFileTranslations.reduce((acc, item) => {
|
||||
return {...acc, ...item.translations};
|
||||
}, {});
|
||||
return sourceCodeFileTranslations.reduce(
|
||||
(acc, item) => ({...acc, ...item.translations}),
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
const sourceCodeFilePaths = await getSourceCodeFilePaths(siteDir, plugins);
|
||||
|
|
|
@ -250,35 +250,38 @@ describe('extending PostCSS', () => {
|
|||
}
|
||||
|
||||
// Run multiple times: ensure last run does not override previous runs
|
||||
webpackConfig = applyConfigurePostCss((postCssOptions) => {
|
||||
return {
|
||||
webpackConfig = applyConfigurePostCss(
|
||||
(postCssOptions) => ({
|
||||
...postCssOptions,
|
||||
plugins: [
|
||||
...postCssOptions.plugins,
|
||||
createFakePlugin('postcss-plugin-1'),
|
||||
],
|
||||
};
|
||||
}, webpackConfig);
|
||||
}),
|
||||
webpackConfig,
|
||||
);
|
||||
|
||||
webpackConfig = applyConfigurePostCss((postCssOptions) => {
|
||||
return {
|
||||
webpackConfig = applyConfigurePostCss(
|
||||
(postCssOptions) => ({
|
||||
...postCssOptions,
|
||||
plugins: [
|
||||
createFakePlugin('postcss-plugin-2'),
|
||||
...postCssOptions.plugins,
|
||||
],
|
||||
};
|
||||
}, webpackConfig);
|
||||
}),
|
||||
webpackConfig,
|
||||
);
|
||||
|
||||
webpackConfig = applyConfigurePostCss((postCssOptions) => {
|
||||
return {
|
||||
webpackConfig = applyConfigurePostCss(
|
||||
(postCssOptions) => ({
|
||||
...postCssOptions,
|
||||
plugins: [
|
||||
...postCssOptions.plugins,
|
||||
createFakePlugin('postcss-plugin-3'),
|
||||
],
|
||||
};
|
||||
}, webpackConfig);
|
||||
}),
|
||||
webpackConfig,
|
||||
);
|
||||
|
||||
// @ts-expect-error: relax type
|
||||
const postCssLoader1 = webpackConfig.module?.rules[0].use[2];
|
||||
|
|
|
@ -176,18 +176,16 @@ class CleanWebpackPlugin {
|
|||
all: false,
|
||||
assets: true,
|
||||
}).assets || [];
|
||||
const assets = statsAssets.map((asset: {name: string}) => {
|
||||
return asset.name;
|
||||
});
|
||||
const assets = statsAssets.map((asset: {name: string}) => asset.name);
|
||||
|
||||
/**
|
||||
* Get all files that were in the previous build but not the current
|
||||
*
|
||||
* (relies on del's cwd: outputPath option)
|
||||
*/
|
||||
const staleFiles = this.currentAssets.filter((previousAsset) => {
|
||||
return assets.includes(previousAsset) === false;
|
||||
});
|
||||
const staleFiles = this.currentAssets.filter(
|
||||
(previousAsset) => assets.includes(previousAsset) === false,
|
||||
);
|
||||
|
||||
/**
|
||||
* Save assets for next compilation
|
||||
|
|
|
@ -164,7 +164,7 @@ export const getCustomizableJSLoader =
|
|||
: jsLoader(isServer);
|
||||
|
||||
// TODO remove this before end of 2021?
|
||||
const warnBabelLoaderOnce = memoize(function () {
|
||||
const warnBabelLoaderOnce = memoize(() => {
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
'Docusaurus plans to support multiple JS loader strategies (Babel, esbuild...): "getBabelLoader(isServer)" is now deprecated in favor of "getJSLoader({isServer})".',
|
||||
|
@ -180,7 +180,7 @@ const getBabelLoaderDeprecated = function getBabelLoaderDeprecated(
|
|||
};
|
||||
|
||||
// TODO remove this before end of 2021 ?
|
||||
const warnCacheLoaderOnce = memoize(function () {
|
||||
const warnCacheLoaderOnce = memoize(() => {
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
'Docusaurus uses Webpack 5 and getCacheLoader() usage is now deprecated.',
|
||||
|
@ -335,24 +335,20 @@ export function getFileLoaderUtils(): FileLoaderUtils {
|
|||
`${OUTPUT_STATIC_ASSETS_DIR_NAME}/${folder}/[name]-[hash].[ext]`;
|
||||
|
||||
const loaders: FileLoaderUtils['loaders'] = {
|
||||
file: (options: {folder: AssetFolder}) => {
|
||||
return {
|
||||
loader: require.resolve(`file-loader`),
|
||||
options: {
|
||||
name: fileLoaderFileName(options.folder),
|
||||
},
|
||||
};
|
||||
},
|
||||
url: (options: {folder: AssetFolder}) => {
|
||||
return {
|
||||
loader: require.resolve(`url-loader`),
|
||||
options: {
|
||||
limit: urlLoaderLimit,
|
||||
name: fileLoaderFileName(options.folder),
|
||||
fallback: require.resolve(`file-loader`),
|
||||
},
|
||||
};
|
||||
},
|
||||
file: (options: {folder: AssetFolder}) => ({
|
||||
loader: require.resolve(`file-loader`),
|
||||
options: {
|
||||
name: fileLoaderFileName(options.folder),
|
||||
},
|
||||
}),
|
||||
url: (options: {folder: AssetFolder}) => ({
|
||||
loader: require.resolve(`url-loader`),
|
||||
options: {
|
||||
limit: urlLoaderLimit,
|
||||
name: fileLoaderFileName(options.folder),
|
||||
fallback: require.resolve(`file-loader`),
|
||||
},
|
||||
}),
|
||||
|
||||
// TODO find a better solution to avoid conflicts with the ideal-image plugin
|
||||
// TODO this may require a little breaking change for ideal-image users?
|
||||
|
@ -372,69 +368,59 @@ export function getFileLoaderUtils(): FileLoaderUtils {
|
|||
* Loads image assets, inlines images via a data URI if they are below
|
||||
* the size threshold
|
||||
*/
|
||||
images: () => {
|
||||
return {
|
||||
use: [loaders.url({folder: 'images'})],
|
||||
test: /\.(ico|jpg|jpeg|png|gif|webp)(\?.*)?$/,
|
||||
};
|
||||
},
|
||||
images: () => ({
|
||||
use: [loaders.url({folder: 'images'})],
|
||||
test: /\.(ico|jpg|jpeg|png|gif|webp)(\?.*)?$/,
|
||||
}),
|
||||
|
||||
fonts: () => {
|
||||
return {
|
||||
use: [loaders.url({folder: 'fonts'})],
|
||||
test: /\.(woff|woff2|eot|ttf|otf)$/,
|
||||
};
|
||||
},
|
||||
fonts: () => ({
|
||||
use: [loaders.url({folder: 'fonts'})],
|
||||
test: /\.(woff|woff2|eot|ttf|otf)$/,
|
||||
}),
|
||||
|
||||
/**
|
||||
* Loads audio and video and inlines them via a data URI if they are below
|
||||
* the size threshold
|
||||
*/
|
||||
media: () => {
|
||||
return {
|
||||
use: [loaders.url({folder: 'medias'})],
|
||||
test: /\.(mp4|webm|ogv|wav|mp3|m4a|aac|oga|flac)$/,
|
||||
};
|
||||
},
|
||||
media: () => ({
|
||||
use: [loaders.url({folder: 'medias'})],
|
||||
test: /\.(mp4|webm|ogv|wav|mp3|m4a|aac|oga|flac)$/,
|
||||
}),
|
||||
|
||||
svg: () => {
|
||||
return {
|
||||
test: /\.svg?$/,
|
||||
oneOf: [
|
||||
{
|
||||
use: [
|
||||
{
|
||||
loader: '@svgr/webpack',
|
||||
options: {
|
||||
prettier: false,
|
||||
svgo: true,
|
||||
svgoConfig: {
|
||||
plugins: [{removeViewBox: false}],
|
||||
},
|
||||
titleProp: true,
|
||||
ref: ![path],
|
||||
svg: () => ({
|
||||
test: /\.svg?$/,
|
||||
oneOf: [
|
||||
{
|
||||
use: [
|
||||
{
|
||||
loader: '@svgr/webpack',
|
||||
options: {
|
||||
prettier: false,
|
||||
svgo: true,
|
||||
svgoConfig: {
|
||||
plugins: [{removeViewBox: false}],
|
||||
},
|
||||
titleProp: true,
|
||||
ref: ![path],
|
||||
},
|
||||
],
|
||||
// We don't want to use SVGR loader for non-React source code
|
||||
// ie we don't want to use SVGR for CSS files...
|
||||
issuer: {
|
||||
and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
|
||||
},
|
||||
],
|
||||
// We don't want to use SVGR loader for non-React source code
|
||||
// ie we don't want to use SVGR for CSS files...
|
||||
issuer: {
|
||||
and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
|
||||
},
|
||||
{
|
||||
use: [loaders.url({folder: 'images'})],
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
use: [loaders.url({folder: 'images'})],
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
otherAssets: () => {
|
||||
return {
|
||||
use: [loaders.file({folder: 'files'})],
|
||||
test: /\.(pdf|doc|docx|xls|xlsx|zip|rar)$/,
|
||||
};
|
||||
},
|
||||
otherAssets: () => ({
|
||||
use: [loaders.file({folder: 'files'})],
|
||||
test: /\.(pdf|doc|docx|xls|xlsx|zip|rar)$/,
|
||||
}),
|
||||
};
|
||||
|
||||
return {loaders, rules};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue