Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
268 lines
10 KiB
Plaintext
268 lines
10 KiB
Plaintext
import fs from 'fs';
|
||
import { createRequire } from 'module';
|
||
import path from 'path';
|
||
import { getFormatExtension } from '../extractor/format/index.js';
|
||
import SourceFileFilter from '../extractor/source/SourceFileFilter.js';
|
||
import { isDevelopmentOrNextBuild } from './config.js';
|
||
import { isNextJs16OrHigher, hasStableTurboConfig } from './nextFlags.js';
|
||
import { throwError } from './utils.js';
|
||
|
||
const require$1 = createRequire(import.meta.url);
|
||
function withExtensions(localPath) {
|
||
return [`${localPath}.ts`, `${localPath}.tsx`, `${localPath}.js`, `${localPath}.jsx`];
|
||
}
|
||
function normalizeTurbopackAliasPath(pathname) {
|
||
// Turbopack alias targets should use forward slashes; Windows backslashes can
|
||
// break resolution in dev (see `next-intl/config` alias path style).
|
||
return pathname.replace(/\\/g, '/');
|
||
}
|
||
function resolveI18nPath(providedPath, cwd) {
|
||
function resolvePath(pathname) {
|
||
const parts = [];
|
||
if (cwd) parts.push(cwd);
|
||
parts.push(pathname);
|
||
return path.resolve(...parts);
|
||
}
|
||
function pathExists(pathname) {
|
||
return fs.existsSync(resolvePath(pathname));
|
||
}
|
||
if (providedPath) {
|
||
// We use the `isNextDevOrBuild` condition to avoid throwing errors
|
||
// if `next.config.ts` is read by a non-Next.js process.
|
||
// https://github.com/amannn/next-intl/discussions/2209#discussioncomment-15650927
|
||
if (isDevelopmentOrNextBuild && !pathExists(providedPath)) {
|
||
throwError(`Could not find i18n config at ${providedPath}, please provide a valid path.`);
|
||
}
|
||
return providedPath;
|
||
} else {
|
||
for (const candidate of [...withExtensions('./i18n/request'), ...withExtensions('./src/i18n/request')]) {
|
||
if (pathExists(candidate)) {
|
||
return candidate;
|
||
}
|
||
}
|
||
if (isDevelopmentOrNextBuild) {
|
||
throwError(`Could not locate request configuration module.\n\nThis path is supported by default: ./(src/)i18n/request.{js,jsx,ts,tsx}\n\nAlternatively, you can specify a custom location in your Next.js config:\n\nconst withNextIntl = createNextIntlPlugin(\n './path/to/i18n/request.tsx'\n);`);
|
||
}
|
||
|
||
// Default as fallback
|
||
if (pathExists('./src')) {
|
||
return './src/i18n/request.ts';
|
||
} else {
|
||
return './i18n/request.ts';
|
||
}
|
||
}
|
||
}
|
||
function getNextConfig(pluginConfig, nextConfig) {
|
||
const useTurbo = process.env.TURBOPACK != null;
|
||
|
||
// `experimental-analyze` doesn’t set the TURBOPACK env param. Since Next.js
|
||
// 16 doesn't print a warning when we configure both Turbo- and Webpack, just
|
||
// always configure Turbopack just in case.
|
||
const shouldConfigureTurbo = useTurbo || isNextJs16OrHigher();
|
||
const nextIntlConfig = {};
|
||
function getExtractMessagesLoaderConfig() {
|
||
const experimental = pluginConfig.experimental;
|
||
if (!experimental.srcPath || !pluginConfig.experimental?.messages) {
|
||
throwError('`srcPath` and `messages` are required when using `extractor`.');
|
||
}
|
||
return {
|
||
loader: 'next-intl/extractor/extractionLoader',
|
||
options: {
|
||
srcPath: experimental.srcPath,
|
||
sourceLocale: experimental.extract.sourceLocale,
|
||
messages: pluginConfig.experimental.messages
|
||
}
|
||
};
|
||
}
|
||
function getCatalogLoaderConfig() {
|
||
return {
|
||
loader: 'next-intl/extractor/catalogLoader',
|
||
options: {
|
||
messages: pluginConfig.experimental.messages
|
||
}
|
||
};
|
||
}
|
||
function getTurboRules() {
|
||
return nextConfig?.turbopack?.rules ||
|
||
// @ts-expect-error -- For Next.js <16
|
||
nextConfig?.experimental?.turbo?.rules || {};
|
||
}
|
||
function addTurboRule(rules, glob, rule) {
|
||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||
if (rules[glob]) {
|
||
if (Array.isArray(rules[glob])) {
|
||
rules[glob].push(rule);
|
||
} else {
|
||
rules[glob] = [rules[glob], rule];
|
||
}
|
||
} else {
|
||
rules[glob] = rule;
|
||
}
|
||
}
|
||
|
||
// Validate messages config
|
||
if (pluginConfig.experimental?.messages) {
|
||
const messages = pluginConfig.experimental.messages;
|
||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- For non-TS consumers
|
||
if (!messages.format) {
|
||
throwError('`format` is required when using `messages`.');
|
||
}
|
||
if (!messages.path) {
|
||
throwError('`path` is required when using `messages`.');
|
||
}
|
||
}
|
||
if (shouldConfigureTurbo) {
|
||
if (pluginConfig.requestConfig && path.isAbsolute(pluginConfig.requestConfig)) {
|
||
throwError("Turbopack support for next-intl currently does not support absolute paths, please provide a relative one (e.g. './src/i18n/config.ts').\n\nFound: " + pluginConfig.requestConfig);
|
||
}
|
||
|
||
// Assign alias for `next-intl/config`
|
||
const resolveAlias = {
|
||
// Turbo aliases don't work with absolute
|
||
// paths (see error handling above)
|
||
'next-intl/config': resolveI18nPath(pluginConfig.requestConfig)
|
||
};
|
||
|
||
// Add alias for precompiled message formatting
|
||
if (pluginConfig.experimental?.messages?.precompile) {
|
||
// Workaround for https://github.com/vercel/next.js/issues/88540
|
||
let formatOnlyPath = path.relative(process.cwd(), require$1.resolve('use-intl/format-message/format-only'));
|
||
|
||
// Turbopack seems to require this, otherwise `use-intl/format-message` is
|
||
// still bundled (despite the code correctly calling into `format-only`).
|
||
// Note that in this monorepo this is not necessary, because we'll end
|
||
// up with a path like `../…` — but for actual consumers this is required.
|
||
if (!formatOnlyPath.startsWith('.')) {
|
||
formatOnlyPath = `./${formatOnlyPath}`;
|
||
}
|
||
resolveAlias['use-intl/format-message'] = normalizeTurbopackAliasPath(formatOnlyPath);
|
||
}
|
||
|
||
// Add loaders
|
||
let rules;
|
||
|
||
// Add loader for extractor
|
||
if (pluginConfig.experimental?.extract) {
|
||
if (!isNextJs16OrHigher()) {
|
||
throwError('Message extraction requires Next.js 16 or higher.');
|
||
}
|
||
rules ??= getTurboRules();
|
||
const srcPaths = (Array.isArray(pluginConfig.experimental.srcPath) ? pluginConfig.experimental.srcPath : [pluginConfig.experimental.srcPath]).map(srcPath => srcPath.endsWith('/') ? srcPath.slice(0, -1) : srcPath);
|
||
addTurboRule(rules, `*.{${SourceFileFilter.EXTENSIONS.join(',')}}`, {
|
||
loaders: [getExtractMessagesLoaderConfig()],
|
||
condition: {
|
||
// Note: We don't need `not: 'foreign'`, because this is
|
||
// implied by the filter based on `srcPath`.
|
||
path: `{${srcPaths.join(',')}}` + '/**/*',
|
||
content: /(useExtracted|getExtracted)/
|
||
}
|
||
});
|
||
}
|
||
|
||
// Add loader for catalog
|
||
if (pluginConfig.experimental?.messages) {
|
||
if (!isNextJs16OrHigher()) {
|
||
throwError('Message catalog loading requires Next.js 16 or higher.');
|
||
}
|
||
rules ??= getTurboRules();
|
||
const extension = getFormatExtension(pluginConfig.experimental.messages.format);
|
||
addTurboRule(rules, `*${extension}`, {
|
||
loaders: [getCatalogLoaderConfig()],
|
||
condition: {
|
||
path: `${pluginConfig.experimental.messages.path}/**/*`
|
||
},
|
||
as: '*.js'
|
||
});
|
||
}
|
||
if (hasStableTurboConfig() &&
|
||
// @ts-expect-error -- For Next.js <16
|
||
!nextConfig?.experimental?.turbo) {
|
||
nextIntlConfig.turbopack = {
|
||
...nextConfig?.turbopack,
|
||
...(rules && {
|
||
rules
|
||
}),
|
||
resolveAlias: {
|
||
...nextConfig?.turbopack?.resolveAlias,
|
||
...resolveAlias
|
||
}
|
||
};
|
||
} else {
|
||
nextIntlConfig.experimental = {
|
||
...nextConfig?.experimental,
|
||
// @ts-expect-error -- For Next.js <16
|
||
turbo: {
|
||
// @ts-expect-error -- For Next.js <16
|
||
...nextConfig?.experimental?.turbo,
|
||
...(rules && {
|
||
rules
|
||
}),
|
||
resolveAlias: {
|
||
// @ts-expect-error -- For Next.js <16
|
||
...nextConfig?.experimental?.turbo?.resolveAlias,
|
||
...resolveAlias
|
||
}
|
||
}
|
||
};
|
||
}
|
||
}
|
||
if (!useTurbo) {
|
||
nextIntlConfig.webpack = function webpack(config, context) {
|
||
if (!config.resolve) config.resolve = {};
|
||
if (!config.resolve.alias) config.resolve.alias = {};
|
||
|
||
// Assign alias for `next-intl/config`
|
||
// (Webpack requires absolute paths)
|
||
config.resolve.alias['next-intl/config'] = path.resolve(config.context, resolveI18nPath(pluginConfig.requestConfig, config.context));
|
||
|
||
// Add alias for precompiled message formatting
|
||
if (pluginConfig.experimental?.messages?.precompile) {
|
||
// Use require.resolve to get the actual file path, since
|
||
// bundlers don't properly resolve package subpath exports
|
||
// when used as alias targets
|
||
config.resolve.alias['use-intl/format-message'] = require$1.resolve('use-intl/format-message/format-only');
|
||
}
|
||
|
||
// Add loader for extractor
|
||
if (pluginConfig.experimental?.extract) {
|
||
if (!config.module) config.module = {};
|
||
if (!config.module.rules) config.module.rules = [];
|
||
const srcPath = pluginConfig.experimental.srcPath;
|
||
config.module.rules.push({
|
||
test: new RegExp(`\\.(${SourceFileFilter.EXTENSIONS.join('|')})$`),
|
||
include: Array.isArray(srcPath) ? srcPath.map(cur => path.resolve(config.context, cur)) : path.resolve(config.context, srcPath || ''),
|
||
use: [getExtractMessagesLoaderConfig()]
|
||
});
|
||
}
|
||
|
||
// Add loader for catalog
|
||
if (pluginConfig.experimental?.messages) {
|
||
if (!config.module) config.module = {};
|
||
if (!config.module.rules) config.module.rules = [];
|
||
const extension = getFormatExtension(pluginConfig.experimental.messages.format);
|
||
config.module.rules.push({
|
||
test: new RegExp(`${extension.replace(/\./g, '\\.')}$`),
|
||
include: path.resolve(config.context, pluginConfig.experimental.messages.path),
|
||
use: [getCatalogLoaderConfig()],
|
||
type: 'javascript/auto'
|
||
});
|
||
}
|
||
if (typeof nextConfig?.webpack === 'function') {
|
||
return nextConfig.webpack(config, context);
|
||
}
|
||
return config;
|
||
};
|
||
}
|
||
|
||
// Forward config
|
||
if (nextConfig?.trailingSlash) {
|
||
nextIntlConfig.env = {
|
||
...nextConfig.env,
|
||
_next_intl_trailing_slash: 'true'
|
||
};
|
||
}
|
||
return Object.assign({}, nextConfig, nextIntlConfig);
|
||
}
|
||
|
||
export { getNextConfig as default };
|