deploy: v0.12 存档迁移+考古日志滚动条

This commit is contained in:
2026-06-24 03:04:38 +00:00
parent 4831505ef9
commit b4c6111516
58986 changed files with 7569452 additions and 31667 deletions
@@ -0,0 +1,26 @@
import initExtractionCompiler from './extractor/initExtractionCompiler.js';
import getNextConfig from './getNextConfig.js';
import { warn } from './utils.js';
import createMessagesDeclaration from './declaration/createMessagesDeclaration.js';
function initPlugin(pluginConfig, nextConfig) {
if (nextConfig?.i18n != null) {
warn("An `i18n` property was found in your Next.js config. This likely causes conflicts and should therefore be removed if you use the App Router.\n\nIf you're in progress of migrating from the Pages Router, you can refer to this example: https://next-intl.dev/examples#app-router-migration\n");
}
const messagesPathOrPaths = pluginConfig.experimental?.createMessagesDeclaration;
if (messagesPathOrPaths) {
createMessagesDeclaration(typeof messagesPathOrPaths === 'string' ? [messagesPathOrPaths] : messagesPathOrPaths);
}
initExtractionCompiler(pluginConfig);
return getNextConfig(pluginConfig, nextConfig);
}
function createNextIntlPlugin(i18nPathOrConfig = {}) {
const config = typeof i18nPathOrConfig === 'string' ? {
requestConfig: i18nPathOrConfig
} : i18nPathOrConfig;
return function withNextIntl(nextConfig) {
return initPlugin(config, nextConfig);
};
}
export { createNextIntlPlugin as default };
@@ -0,0 +1,70 @@
import fs from 'fs';
import path from 'path';
import { once, throwError } from '../utils.js';
import watchFile from '../watchFile.js';
const runOnce = once('_NEXT_INTL_COMPILE_MESSAGES');
function createMessagesDeclaration(messagesPaths) {
// Instead of running _only_ in certain cases, it's
// safer to _avoid_ running for certain known cases.
// https://github.com/amannn/next-intl/issues/2006
const shouldBailOut = ['info', 'start'
// Note: These commands don't consult the
// Next.js config, so we can't detect them here.
// - telemetry
// - lint
//
// What remains are:
// - dev
// - build
// - typegen
].some(arg => process.argv.includes(arg));
if (shouldBailOut) {
return;
}
runOnce(() => {
for (const messagesPath of messagesPaths) {
const fullPath = path.resolve(messagesPath);
if (!fs.existsSync(fullPath)) {
throwError(`\`createMessagesDeclaration\` points to a non-existent file: ${fullPath}`);
}
if (!fullPath.endsWith('.json')) {
throwError(`\`createMessagesDeclaration\` needs to point to a JSON file. Received: ${fullPath}`);
}
// Keep this as a runtime check and don't replace
// this with a constant during the build process
const env = process.env['NODE_ENV'.trim()];
compileDeclaration(messagesPath);
if (env === 'development') {
startWatching(messagesPath);
}
}
});
}
function startWatching(messagesPath) {
const watcher = watchFile(messagesPath, () => {
compileDeclaration(messagesPath, true);
});
process.on('exit', () => {
watcher.close();
});
}
function compileDeclaration(messagesPath, async = false) {
const declarationPath = messagesPath.replace(/\.json$/, '.d.json.ts');
function createDeclaration(content) {
return `// This file is auto-generated by next-intl, do not edit directly.
// See: https://next-intl.dev/docs/workflows/typescript#messages-arguments
declare const messages: ${content.trim()};
export default messages;`;
}
if (async) {
return fs.promises.readFile(messagesPath, 'utf-8').then(content => fs.promises.writeFile(declarationPath, createDeclaration(content)));
}
const content = fs.readFileSync(messagesPath, 'utf-8');
fs.writeFileSync(declarationPath, createDeclaration(content));
}
export { createMessagesDeclaration as default };
@@ -0,0 +1,61 @@
import ExtractionCompiler from '../../extractor/ExtractionCompiler.js';
import { once } from '../utils.js';
// Single compiler instance, initialized once per process
let compiler;
const runOnce = once('_NEXT_INTL_EXTRACT');
function initExtractionCompiler(pluginConfig) {
const experimental = pluginConfig.experimental;
if (!experimental?.extract) {
return;
}
// Avoid rollup's `replace` plugin to compile this away
const isDevelopment = process.env['NODE_ENV'.trim()] === 'development';
// Avoid running for:
// - info
// - start
// - typegen
//
// Doesn't consult Next.js config anyway:
// - telemetry
// - lint
//
// What remains are:
// - dev (NODE_ENV=development)
// - build (NODE_ENV=production)
const shouldRun = isDevelopment || process.argv.includes('build');
if (!shouldRun) return;
runOnce(() => {
const extractorConfig = {
srcPath: experimental.srcPath,
sourceLocale: experimental.extract.sourceLocale,
messages: experimental.messages
};
compiler = new ExtractionCompiler(extractorConfig, {
isDevelopment,
projectRoot: process.cwd()
});
// Fire-and-forget: Start extraction, don't block config return.
// In dev mode, this also starts the file watcher.
// In prod, ideally we would wait until the extraction is complete,
// but we can't `await` anywhere (at least for Turbopack).
// The result is ok though, as if we encounter untranslated messages,
// we'll simply add empty messages to the catalog. So for actually
// running the app, there is no difference.
compiler.extractAll();
function cleanup() {
if (compiler) {
compiler[Symbol.dispose]();
compiler = undefined;
}
}
process.on('exit', cleanup);
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
});
}
export { initExtractionCompiler as default };
+212
View File
@@ -0,0 +1,212 @@
import fs from 'fs';
import path from 'path';
import { getFormatExtension } from '../extractor/format/index.js';
import SourceFileFilter from '../extractor/source/SourceFileFilter.js';
import { isNextJs16OrHigher, hasStableTurboConfig } from './nextFlags.js';
import { throwError } from './utils.js';
function withExtensions(localPath) {
return [`${localPath}.ts`, `${localPath}.tsx`, `${localPath}.js`, `${localPath}.jsx`];
}
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) {
if (!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;
}
}
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(
Alternatively, you can specify a custom location in your Next.js config:
const withNextIntl = createNextIntlPlugin(
'./path/to/i18n/request.tsx'
);`);
}
}
function getNextConfig(pluginConfig, nextConfig) {
const useTurbo = process.env.TURBOPACK != null;
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;
}
}
if (useTurbo) {
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 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
}
}
};
}
} else {
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 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 };
+32
View File
@@ -0,0 +1,32 @@
import { createRequire } from 'module';
function getCurrentVersion() {
try {
const require = createRequire(import.meta.url);
const pkg = require('next/package.json');
return pkg.version;
} catch (error) {
throw new Error('Failed to get current Next.js version. This can happen if next-intl/plugin is imported into your app code outside of your next.config.js.', {
cause: error
});
}
}
function compareVersions(version1, version2) {
const v1Parts = version1.split('.').map(Number);
const v2Parts = version2.split('.').map(Number);
for (let i = 0; i < 3; i++) {
const v1 = v1Parts[i] || 0;
const v2 = v2Parts[i] || 0;
if (v1 > v2) return 1;
if (v1 < v2) return -1;
}
return 0;
}
function hasStableTurboConfig() {
return compareVersions(getCurrentVersion(), '15.3.0') >= 0;
}
function isNextJs16OrHigher() {
return compareVersions(getCurrentVersion(), '16.0.0') >= 0;
}
export { hasStableTurboConfig, isNextJs16OrHigher };
+26
View File
@@ -0,0 +1,26 @@
function formatMessage(message) {
return `\n[next-intl] ${message}\n`;
}
function throwError(message) {
throw new Error(formatMessage(message));
}
function warn(message) {
console.warn(formatMessage(message));
}
/**
* Returns a function that runs the provided callback only once per process.
* Next.js can call the config multiple times - this ensures we only run once.
* Uses an environment variable to track execution across config loads.
*/
function once(namespace) {
return function runOnce(fn) {
if (process.env[namespace] === '1') {
return;
}
process.env[namespace] = '1';
fn();
};
}
export { once, throwError, warn };
+21
View File
@@ -0,0 +1,21 @@
import fs from 'fs';
import path from 'path';
/**
* Wrapper around `fs.watch` that provides a workaround
* for https://github.com/nodejs/node/issues/5039.
*/
function watchFile(filepath, callback) {
const directory = path.dirname(filepath);
const filename = path.basename(filepath);
return fs.watch(directory, {
persistent: false,
recursive: false
}, (event, changedFilename) => {
if (changedFilename === filename) {
callback();
}
});
}
export { watchFile as default };