cloudflare-next-intl 0.9.40 → 0.9.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,7 @@
1
+ import { type DynamicApiCheck } from './detect_dynamic_usage.js';
2
+ import { type DynamicSignal } from './trace_dynamic_usage.js';
1
3
  import { type SyncErrorReportingAuthUserReport } from './sync_error_reporting_auth_user.js';
4
+ import { type PageLabelStyle } from './derive_page_label.js';
2
5
  import type { AliasConfig } from './resolve_local_imports.js';
3
6
  export type DynamicPagesCheckMode = 'off' | 'report' | 'fix';
4
7
  export interface CheckDynamicPagesOptions {
@@ -8,11 +11,17 @@ export interface CheckDynamicPagesOptions {
8
11
  skip?: readonly string[];
9
12
  resolveImports?: boolean;
10
13
  aliases?: readonly AliasConfig[];
14
+ extraChecks?: readonly DynamicApiCheck[];
11
15
  syncErrorReportingAuthUser?: boolean;
16
+ verbose?: boolean | {
17
+ pageLabel?: PageLabelStyle | ((file: string, appDir: string) => string);
18
+ };
12
19
  }
13
20
  export interface CheckDynamicPagesReport {
14
21
  file: string;
15
22
  action: 'added-force-dynamic' | 'would-add-force-dynamic' | 'added-force-static' | 'would-add-force-static' | 'already-declared' | 'no-dynamic-usage-detected' | 'skipped';
23
+ signals?: DynamicSignal[];
24
+ explicitValue?: 'force-static' | 'force-dynamic' | 'auto' | 'error' | null;
16
25
  }
17
26
  export interface CheckDynamicPagesIo {
18
27
  findPageFiles?: (appDir: string) => string[];
@@ -1,10 +1,73 @@
1
1
  import { readFileSync, statSync, writeFileSync } from 'node:fs';
2
- import { resolve } from 'node:path';
2
+ import { relative, resolve } from 'node:path';
3
3
  import { findPageFiles as findPageFilesImpl } from './find_page_files.js';
4
- import { detectDynamicUsage } from './detect_dynamic_usage.js';
4
+ import { detectDynamicUsage, readExplicitDynamicValue } from './detect_dynamic_usage.js';
5
5
  import { traceDynamicUsage } from './trace_dynamic_usage.js';
6
6
  import { insertDynamicExport } from './insert_dynamic_export.js';
7
7
  import { syncErrorReportingAuthUser } from './sync_error_reporting_auth_user.js';
8
+ import { deriveRoute, isApiRoute, makePageLabeler } from './derive_page_label.js';
9
+ const LEGEND = 'λ API ƒ Dynamic (SSR) ○ Static (SSG) = Already declared - Unclear (framework decides) · Skipped';
10
+ function actionGlyph(report, isApi) {
11
+ if (isApi && report.action !== 'skipped')
12
+ return 'λ';
13
+ switch (report.action) {
14
+ case 'added-force-dynamic':
15
+ case 'would-add-force-dynamic':
16
+ return 'ƒ';
17
+ case 'added-force-static':
18
+ case 'would-add-force-static':
19
+ return '○';
20
+ case 'no-dynamic-usage-detected': return '-';
21
+ case 'skipped': return '·';
22
+ case 'already-declared':
23
+ if (report.explicitValue === 'force-dynamic')
24
+ return 'ƒ';
25
+ if (report.explicitValue === 'force-static')
26
+ return '○';
27
+ return '=';
28
+ }
29
+ }
30
+ function actionDetail(report, isApi) {
31
+ if (isApi && report.action !== 'skipped')
32
+ return 'API route';
33
+ switch (report.action) {
34
+ case 'added-force-dynamic': return 'Dynamic (SSR) — added export const dynamic = "force-dynamic"';
35
+ case 'would-add-force-dynamic': return 'Dynamic (SSR) — would add export const dynamic = "force-dynamic"';
36
+ case 'added-force-static': return 'Static (SSG) — added export const dynamic = "force-static"';
37
+ case 'would-add-force-static': return 'Static (SSG) — would add export const dynamic = "force-static"';
38
+ case 'no-dynamic-usage-detected': return 'Unclear — no dynamic-API usage detected, left to the framework';
39
+ case 'skipped': return 'Skipped — excluded from this scan';
40
+ case 'already-declared':
41
+ switch (report.explicitValue) {
42
+ case 'force-dynamic': return 'Dynamic (SSR) — export const dynamic = "force-dynamic" already set';
43
+ case 'force-static': return 'Static (SSG) — export const dynamic = "force-static" already set';
44
+ case 'auto': return 'export const dynamic = "auto" already set';
45
+ case 'error': return 'export const dynamic = "error" already set';
46
+ default: return 'export const dynamic already set';
47
+ }
48
+ }
49
+ }
50
+ function displayPath(file) {
51
+ const rel = relative(process.cwd(), file);
52
+ return rel === '' || rel.startsWith('..') ? file : rel;
53
+ }
54
+ function logReports(reports, appDir, pageLabel) {
55
+ console.log(`[cloudflare-next-intl] dynamic-pages check\n${LEGEND}\n`);
56
+ reports.forEach((report, index) => {
57
+ const isLast = index === reports.length - 1;
58
+ const branch = isLast ? '└' : '├';
59
+ const isApi = isApiRoute(report.file);
60
+ const glyph = actionGlyph(report, isApi);
61
+ const route = deriveRoute(appDir, report.file);
62
+ console.log(`${branch} ${glyph} ${route} ${pageLabel(report.file)} — ${actionDetail(report, isApi)}`);
63
+ const continuation = isLast ? ' ' : '│';
64
+ for (const signal of report.signals ?? []) {
65
+ const location = `${displayPath(signal.file)}:${signal.line}`;
66
+ const where = signal.file === report.file ? `at ${location}` : `via ${location}`;
67
+ console.log(`${continuation} ↳ ${signal.api} ${where}`);
68
+ }
69
+ });
70
+ }
8
71
  function defaultIsFile(path) {
9
72
  try {
10
73
  return statSync(path).isFile();
@@ -27,6 +90,7 @@ export async function checkDynamicPages(options, io = {}) {
27
90
  const aliases = options.aliases ?? [
28
91
  { prefix: '@/', replacement: resolve(options.appDir, '..') },
29
92
  ];
93
+ const extraChecks = options.extraChecks ?? [];
30
94
  const reports = [];
31
95
  for (const file of findPageFiles(options.appDir)) {
32
96
  if (skipSet.has(file)) {
@@ -35,10 +99,13 @@ export async function checkDynamicPages(options, io = {}) {
35
99
  }
36
100
  const source = readFile(file);
37
101
  const detection = resolveImports
38
- ? traceDynamicUsage(file, source, aliases, { readFile, isFile })
39
- : detectDynamicUsage(source);
102
+ ? traceDynamicUsage(file, source, aliases, { readFile, isFile }, extraChecks)
103
+ : { ...detectDynamicUsage(source, extraChecks), signals: [] };
104
+ const signals = resolveImports
105
+ ? detection.signals
106
+ : detection.matches.map(({ name, line }) => ({ api: name, file, line }));
40
107
  if (detection.hasExplicitDynamicExport) {
41
- reports.push({ file, action: 'already-declared' });
108
+ reports.push({ file, action: 'already-declared', explicitValue: readExplicitDynamicValue(source) });
42
109
  continue;
43
110
  }
44
111
  if (detection.detectedDynamicApis.length === 0) {
@@ -57,12 +124,17 @@ export async function checkDynamicPages(options, io = {}) {
57
124
  }
58
125
  if (mode === 'fix') {
59
126
  writeFile(file, insertDynamicExport(source, 'force-dynamic'));
60
- reports.push({ file, action: 'added-force-dynamic' });
127
+ reports.push({ file, action: 'added-force-dynamic', signals });
61
128
  }
62
129
  else {
63
- reports.push({ file, action: 'would-add-force-dynamic' });
130
+ reports.push({ file, action: 'would-add-force-dynamic', signals });
64
131
  }
65
132
  }
133
+ if (options.verbose) {
134
+ const pageLabelStyle = typeof options.verbose === 'object' ? options.verbose.pageLabel : undefined;
135
+ const pageLabel = makePageLabeler(options.appDir, pageLabelStyle, displayPath);
136
+ logReports(reports, options.appDir, pageLabel);
137
+ }
66
138
  if (options.syncErrorReportingAuthUser === true) {
67
139
  const syncReports = await syncErrorReportingAuthUser({ appDir: options.appDir, mode: options.mode, target: options.target, skip: options.skip, aliases: options.aliases }, { findPageFiles, readFile, writeFile, isFile });
68
140
  reports.push(...syncReports);
@@ -1,5 +1,16 @@
1
- import { extractImportSpecifiers, resolveLocalImport } from './resolve_local_imports.js';
1
+ import { extractImportBindings, resolveLocalImport } from './resolve_local_imports.js';
2
+ import { stripComments, USE_CLIENT_DIRECTIVE } from './detect_dynamic_usage.js';
2
3
  export const MAX_FILES_VISITED = 300;
4
+ function blankSpans(code, spans) {
5
+ let out = code;
6
+ for (const { start, end } of spans) {
7
+ out = out.slice(0, start) + [...out.slice(start, end)].map((c) => (c === '\n' ? '\n' : ' ')).join('') + out.slice(end);
8
+ }
9
+ return out;
10
+ }
11
+ function isWordUsed(name, text) {
12
+ return new RegExp(`\\b${name}\\b`).test(text);
13
+ }
3
14
  export function collectReachableFiles(entryFile, entrySource, aliases, io) {
4
15
  const isFile = io.isFile ?? (() => false);
5
16
  const files = new Map([[entryFile, entrySource]]);
@@ -9,9 +20,17 @@ export function collectReachableFiles(entryFile, entrySource, aliases, io) {
9
20
  if (files.size >= MAX_FILES_VISITED)
10
21
  continue;
11
22
  const source = files.get(current);
12
- for (const specifier of extractImportSpecifiers(source)) {
23
+ if (USE_CLIENT_DIRECTIVE.test(source))
24
+ continue;
25
+ const code = stripComments(source);
26
+ const imports = extractImportBindings(code);
27
+ const usageText = blankSpans(code, imports);
28
+ for (const { specifier, bindings, alwaysFollow } of imports) {
13
29
  if (files.size >= MAX_FILES_VISITED)
14
30
  break;
31
+ if (!alwaysFollow && bindings.length > 0 && !bindings.some((name) => isWordUsed(name, usageText))) {
32
+ continue;
33
+ }
15
34
  const resolved = resolveLocalImport(specifier, current, aliases, isFile);
16
35
  if (resolved === null || files.has(resolved))
17
36
  continue;
@@ -0,0 +1,5 @@
1
+ export declare function derivePageLabel(appDir: string, file: string): string;
2
+ export declare function deriveRoute(appDir: string, file: string): string;
3
+ export declare function isApiRoute(file: string): boolean;
4
+ export type PageLabelStyle = 'title' | 'path';
5
+ export declare function makePageLabeler(appDir: string, style: PageLabelStyle | ((file: string, appDir: string) => string) | undefined, displayPath: (file: string) => string): (file: string) => string;
@@ -0,0 +1,57 @@
1
+ import { relative, sep } from 'node:path';
2
+ function splitWords(segment) {
3
+ return segment
4
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
5
+ .split(/[-_\s]+/)
6
+ .filter((word) => word.length > 0);
7
+ }
8
+ function titleCaseWords(words) {
9
+ return words.map((word) => word[0].toUpperCase() + word.slice(1)).join(' ');
10
+ }
11
+ export function derivePageLabel(appDir, file) {
12
+ const rel = relative(appDir, file);
13
+ const segments = rel.split(sep).filter((s) => s.length > 0);
14
+ segments.pop();
15
+ let literal = null;
16
+ let dynamicSuffix = null;
17
+ for (const segment of segments) {
18
+ const dynamicMatch = /^\[+\.{0,3}([^\]]+)\]+$/.exec(segment);
19
+ if (dynamicMatch) {
20
+ if (literal !== null)
21
+ dynamicSuffix = dynamicMatch[1];
22
+ continue;
23
+ }
24
+ if (/^\(.+\)$/.test(segment))
25
+ continue;
26
+ literal = segment;
27
+ dynamicSuffix = null;
28
+ }
29
+ if (literal === null)
30
+ return 'Home';
31
+ const label = titleCaseWords(splitWords(literal));
32
+ return dynamicSuffix ? `${label} (:${dynamicSuffix})` : label;
33
+ }
34
+ export function deriveRoute(appDir, file) {
35
+ const rel = relative(appDir, file);
36
+ const segments = rel.split(sep).filter((s) => s.length > 0);
37
+ segments.pop();
38
+ const urlSegments = segments
39
+ .filter((segment) => !/^\(.+\)$/.test(segment))
40
+ .map((segment) => {
41
+ const dynamicMatch = /^\[+(\.{3})?([^\]]+)\]+$/.exec(segment);
42
+ if (!dynamicMatch)
43
+ return segment;
44
+ return `:${dynamicMatch[1] ? '...' : ''}${dynamicMatch[2]}`;
45
+ });
46
+ return urlSegments.length > 0 ? `/${urlSegments.join('/')}` : '/';
47
+ }
48
+ export function isApiRoute(file) {
49
+ return /(^|[\\/])route\.(ts|js)$/.test(file);
50
+ }
51
+ export function makePageLabeler(appDir, style, displayPath) {
52
+ if (typeof style === 'function')
53
+ return (file) => style(file, appDir);
54
+ if (style === 'path')
55
+ return displayPath;
56
+ return (file) => derivePageLabel(appDir, file);
57
+ }
@@ -1,6 +1,17 @@
1
+ export interface DynamicApiMatch {
2
+ name: string;
3
+ line: number;
4
+ }
1
5
  export interface DynamicDetectionResult {
2
6
  hasExplicitDynamicExport: boolean;
3
7
  detectedDynamicApis: string[];
8
+ matches: DynamicApiMatch[];
9
+ }
10
+ export declare function stripComments(sourceText: string): string;
11
+ export interface DynamicApiCheck {
12
+ name: string;
13
+ pattern: RegExp;
4
14
  }
5
- export declare function detectDynamicUsage(sourceText: string): DynamicDetectionResult;
15
+ export declare const USE_CLIENT_DIRECTIVE: RegExp;
16
+ export declare function detectDynamicUsage(sourceText: string, extraChecks?: readonly DynamicApiCheck[]): DynamicDetectionResult;
6
17
  export declare function readExplicitDynamicValue(sourceText: string): 'force-static' | 'force-dynamic' | 'auto' | 'error' | null;
@@ -1,3 +1,34 @@
1
+ function lineOf(sourceText, index) {
2
+ let line = 1;
3
+ for (let i = 0; i < index; i++) {
4
+ if (sourceText.charCodeAt(i) === 10)
5
+ line++;
6
+ }
7
+ return line;
8
+ }
9
+ export function stripComments(sourceText) {
10
+ let out = '';
11
+ for (let i = 0; i < sourceText.length; i++) {
12
+ if (sourceText[i] === '/' && sourceText[i + 1] === '*') {
13
+ const end = sourceText.indexOf('*/', i + 2);
14
+ const commentEnd = end === -1 ? sourceText.length : end + 2;
15
+ for (let j = i; j < commentEnd; j++)
16
+ out += sourceText[j] === '\n' ? '\n' : ' ';
17
+ i = commentEnd - 1;
18
+ continue;
19
+ }
20
+ if (sourceText[i] === '/' && sourceText[i + 1] === '/' && sourceText[i - 1] !== ':') {
21
+ let end = sourceText.indexOf('\n', i);
22
+ if (end === -1)
23
+ end = sourceText.length;
24
+ out += ' '.repeat(end - i);
25
+ i = end - 1;
26
+ continue;
27
+ }
28
+ out += sourceText[i];
29
+ }
30
+ return out;
31
+ }
1
32
  const DYNAMIC_API_CHECKS = [
2
33
  { name: 'cookies()', pattern: /\bcookies\s*\(/ },
3
34
  { name: 'headers()', pattern: /\bheaders\s*\(\s*\)/ },
@@ -10,21 +41,31 @@ const DYNAMIC_API_CHECKS = [
10
41
  { name: 'withUserDb()', pattern: /\bwithUserDb\s*\(/ },
11
42
  ];
12
43
  const USE_AUTH_USER_CALL = /\buseAuthUser\s*\(/;
13
- const USE_CLIENT_DIRECTIVE = /^(?:\s*['"]use \w[\w-]*['"]\s*;?\s*)*['"]use client['"]\s*;?/;
44
+ export const USE_CLIENT_DIRECTIVE = /^(?:\s*['"]use \w[\w-]*['"]\s*;?\s*)*['"]use client['"]\s*;?/;
14
45
  const EXPLICIT_DYNAMIC_EXPORT = /export\s+const\s+dynamic\s*=/;
15
- export function detectDynamicUsage(sourceText) {
16
- const detectedDynamicApis = DYNAMIC_API_CHECKS.filter(({ pattern }) => pattern.test(sourceText)).map(({ name }) => name);
17
- if (USE_AUTH_USER_CALL.test(sourceText) && !USE_CLIENT_DIRECTIVE.test(sourceText)) {
18
- detectedDynamicApis.push('useAuthUser()');
46
+ export function detectDynamicUsage(sourceText, extraChecks = []) {
47
+ const code = stripComments(sourceText);
48
+ const matches = [];
49
+ for (const { name, pattern } of [...DYNAMIC_API_CHECKS, ...extraChecks]) {
50
+ pattern.lastIndex = 0;
51
+ const found = pattern.exec(code);
52
+ if (found !== null)
53
+ matches.push({ name, line: lineOf(sourceText, found.index) });
54
+ }
55
+ if (!USE_CLIENT_DIRECTIVE.test(sourceText)) {
56
+ const found = USE_AUTH_USER_CALL.exec(code);
57
+ if (found !== null)
58
+ matches.push({ name: 'useAuthUser()', line: lineOf(sourceText, found.index) });
19
59
  }
20
60
  return {
21
- hasExplicitDynamicExport: EXPLICIT_DYNAMIC_EXPORT.test(sourceText),
22
- detectedDynamicApis,
61
+ hasExplicitDynamicExport: EXPLICIT_DYNAMIC_EXPORT.test(code),
62
+ detectedDynamicApis: matches.map((m) => m.name),
63
+ matches,
23
64
  };
24
65
  }
25
66
  const EXPLICIT_DYNAMIC_EXPORT_VALUE = /export\s+const\s+dynamic\s*=\s*['"]([^'"]+)['"]/;
26
67
  export function readExplicitDynamicValue(sourceText) {
27
- const match = EXPLICIT_DYNAMIC_EXPORT_VALUE.exec(sourceText);
68
+ const match = EXPLICIT_DYNAMIC_EXPORT_VALUE.exec(stripComments(sourceText));
28
69
  if (match === null)
29
70
  return null;
30
71
  const value = match[1];
@@ -1,3 +1,6 @@
1
1
  export { checkDynamicPages, type DynamicPagesCheckMode, type CheckDynamicPagesOptions, type CheckDynamicPagesReport, type CheckDynamicPagesIo } from './check_dynamic_pages.js';
2
2
  export { findPageFiles } from './find_page_files.js';
3
- export { detectDynamicUsage, type DynamicDetectionResult } from './detect_dynamic_usage.js';
3
+ export { detectDynamicUsage, stripComments, type DynamicApiCheck, type DynamicApiMatch, type DynamicDetectionResult } from './detect_dynamic_usage.js';
4
+ export { traceDynamicUsage, type DynamicSignal, type TraceDynamicUsageResult } from './trace_dynamic_usage.js';
5
+ export { collectReachableFiles, MAX_FILES_VISITED } from './collect_reachable_files.js';
6
+ export { derivePageLabel, deriveRoute, isApiRoute, makePageLabeler, type PageLabelStyle } from './derive_page_label.js';
@@ -1,3 +1,6 @@
1
1
  export { checkDynamicPages } from './check_dynamic_pages.js';
2
2
  export { findPageFiles } from './find_page_files.js';
3
- export { detectDynamicUsage } from './detect_dynamic_usage.js';
3
+ export { detectDynamicUsage, stripComments } from './detect_dynamic_usage.js';
4
+ export { traceDynamicUsage } from './trace_dynamic_usage.js';
5
+ export { collectReachableFiles, MAX_FILES_VISITED } from './collect_reachable_files.js';
6
+ export { derivePageLabel, deriveRoute, isApiRoute, makePageLabeler } from './derive_page_label.js';
@@ -3,4 +3,12 @@ export interface AliasConfig {
3
3
  replacement: string;
4
4
  }
5
5
  export declare function extractImportSpecifiers(sourceText: string): string[];
6
+ export interface ImportBindingInfo {
7
+ specifier: string;
8
+ bindings: string[];
9
+ alwaysFollow: boolean;
10
+ start: number;
11
+ end: number;
12
+ }
13
+ export declare function extractImportBindings(sourceText: string): ImportBindingInfo[];
6
14
  export declare function resolveLocalImport(specifier: string, fromFile: string, aliases: readonly AliasConfig[], isFile?: (file: string) => boolean): string | null;
@@ -15,6 +15,62 @@ export function extractImportSpecifiers(sourceText) {
15
15
  }
16
16
  return specifiers;
17
17
  }
18
+ const FROM_IMPORT_STATEMENT = /\b(import|export)\s+type\s+|\b(import|export)\s+([\s\S]*?)\s*from\s*(['"])([^'"]+)\4/g;
19
+ const BARE_IMPORT_STATEMENT = /(?:^|\n|;)\s*(import\s*(['"])([^'"]+)\2)/g;
20
+ function bindingsFromClause(clause) {
21
+ const bindings = [];
22
+ const namespaceMatch = /^\*\s*as\s+(\w+)$/.exec(clause.trim());
23
+ if (namespaceMatch)
24
+ return [namespaceMatch[1]];
25
+ if (clause.trim() === '*')
26
+ return [];
27
+ const braceMatch = /\{([^}]*)\}/.exec(clause);
28
+ if (braceMatch) {
29
+ for (const rawItem of braceMatch[1].split(',')) {
30
+ const item = rawItem.trim().replace(/^type\s+/, '');
31
+ if (item.length === 0)
32
+ continue;
33
+ const asMatch = /\bas\s+(\w+)$/.exec(item);
34
+ bindings.push(asMatch ? asMatch[1] : item);
35
+ }
36
+ }
37
+ const beforeBrace = clause.slice(0, braceMatch?.index ?? clause.length).replace(/,\s*$/, '').trim();
38
+ if (/^\w+$/.test(beforeBrace))
39
+ bindings.push(beforeBrace);
40
+ return bindings;
41
+ }
42
+ export function extractImportBindings(sourceText) {
43
+ const results = [];
44
+ FROM_IMPORT_STATEMENT.lastIndex = 0;
45
+ let match;
46
+ while ((match = FROM_IMPORT_STATEMENT.exec(sourceText)) !== null) {
47
+ if (match[1] !== undefined || match[2] === undefined)
48
+ continue;
49
+ const keyword = match[2];
50
+ const clause = match[3];
51
+ const specifier = match[5];
52
+ results.push({
53
+ specifier,
54
+ bindings: keyword === 'export' ? [] : bindingsFromClause(clause),
55
+ alwaysFollow: keyword === 'export',
56
+ start: match.index,
57
+ end: match.index + match[0].length,
58
+ });
59
+ }
60
+ BARE_IMPORT_STATEMENT.lastIndex = 0;
61
+ while ((match = BARE_IMPORT_STATEMENT.exec(sourceText)) !== null) {
62
+ const statement = match[1];
63
+ const statementStart = match.index + match[0].indexOf(statement);
64
+ results.push({
65
+ specifier: match[3],
66
+ bindings: [],
67
+ alwaysFollow: true,
68
+ start: statementStart,
69
+ end: statementStart + statement.length,
70
+ });
71
+ }
72
+ return results;
73
+ }
18
74
  const FILE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
19
75
  function defaultIsFile(path) {
20
76
  try {
@@ -1,5 +1,13 @@
1
- import { type DynamicDetectionResult } from './detect_dynamic_usage.js';
1
+ import { type DynamicApiCheck, type DynamicDetectionResult } from './detect_dynamic_usage.js';
2
2
  import { type CollectReachableFilesIo } from './collect_reachable_files.js';
3
3
  import type { AliasConfig } from './resolve_local_imports.js';
4
4
  export type TraceDynamicUsageIo = CollectReachableFilesIo;
5
- export declare function traceDynamicUsage(entryFile: string, entrySource: string, aliases: readonly AliasConfig[], io: TraceDynamicUsageIo): DynamicDetectionResult;
5
+ export interface DynamicSignal {
6
+ api: string;
7
+ file: string;
8
+ line: number;
9
+ }
10
+ export interface TraceDynamicUsageResult extends DynamicDetectionResult {
11
+ signals: DynamicSignal[];
12
+ }
13
+ export declare function traceDynamicUsage(entryFile: string, entrySource: string, aliases: readonly AliasConfig[], io: TraceDynamicUsageIo, extraChecks?: readonly DynamicApiCheck[]): TraceDynamicUsageResult;
@@ -1,17 +1,26 @@
1
1
  import { detectDynamicUsage } from './detect_dynamic_usage.js';
2
2
  import { collectReachableFiles } from './collect_reachable_files.js';
3
- export function traceDynamicUsage(entryFile, entrySource, aliases, io) {
3
+ export function traceDynamicUsage(entryFile, entrySource, aliases, io, extraChecks = []) {
4
4
  const files = collectReachableFiles(entryFile, entrySource, aliases, io);
5
5
  let hasExplicitDynamicExport = false;
6
6
  const detectedApis = new Set();
7
+ const signals = [];
7
8
  let first = true;
8
- for (const source of files.values()) {
9
- const detection = detectDynamicUsage(source);
9
+ for (const [file, source] of files.entries()) {
10
+ const detection = detectDynamicUsage(source, extraChecks);
10
11
  if (first) {
11
12
  hasExplicitDynamicExport = detection.hasExplicitDynamicExport;
12
13
  first = false;
13
14
  }
14
- detection.detectedDynamicApis.forEach((api) => detectedApis.add(api));
15
+ detection.matches.forEach(({ name, line }) => {
16
+ detectedApis.add(name);
17
+ signals.push({ api: name, file, line });
18
+ });
15
19
  }
16
- return { hasExplicitDynamicExport, detectedDynamicApis: [...detectedApis] };
20
+ return {
21
+ hasExplicitDynamicExport,
22
+ detectedDynamicApis: [...detectedApis],
23
+ matches: signals.map(({ api, line }) => ({ name: api, line })),
24
+ signals,
25
+ };
17
26
  }
@@ -35,6 +35,22 @@ function CustomLinkFunction({ href, prefetch, prefetchType = 'custom', hoverPref
35
35
  }
36
36
  window.dispatchEvent(new CustomEvent(PENDING_NAVIGATION_EVENT, { detail: null }));
37
37
  }, [pathname]);
38
+ const openedPendingEvent = useRef(false);
39
+ const wasPending = useRef(false);
40
+ useEffect(() => {
41
+ if (isPending) {
42
+ wasPending.current = true;
43
+ return;
44
+ }
45
+ if (!wasPending.current)
46
+ return;
47
+ wasPending.current = false;
48
+ setIsNavigating(false);
49
+ if (!openedPendingEvent.current)
50
+ return;
51
+ openedPendingEvent.current = false;
52
+ window.dispatchEvent(new CustomEvent(PENDING_NAVIGATION_EVENT, { detail: null }));
53
+ }, [isPending]);
38
54
  useEffect(() => {
39
55
  if (!isNavigating)
40
56
  return;
@@ -99,8 +115,10 @@ function CustomLinkFunction({ href, prefetch, prefetchType = 'custom', hoverPref
99
115
  }
100
116
  if (isCustom) {
101
117
  const targetPath = typeof pathnames === 'string' ? pathnames : urlString;
102
- if (pathname !== targetPath) {
118
+ const targetPathname = targetPath.replace(/[?#].*$/, '');
119
+ if (pathname !== targetPathname) {
103
120
  setIsNavigating(true);
121
+ openedPendingEvent.current = true;
104
122
  window.dispatchEvent(new CustomEvent(PENDING_NAVIGATION_EVENT, { detail: targetPath }));
105
123
  }
106
124
  startTransition(() => {
@@ -1,9 +1,14 @@
1
1
  import type { Plugin } from "vite";
2
- import { type DynamicPagesCheckMode } from "../dynamic_pages_check/index.js";
2
+ import { type DynamicApiCheck, type DynamicPagesCheckMode, type PageLabelStyle } from "../dynamic_pages_check/index.js";
3
3
  export interface AutoDynamicPagesPluginOptions {
4
4
  appDir?: string;
5
5
  mode?: DynamicPagesCheckMode;
6
6
  target?: 'next' | 'vinext';
7
7
  syncErrorReportingAuthUser?: boolean;
8
+ extraChecks?: readonly DynamicApiCheck[];
9
+ verbose?: boolean | {
10
+ pageLabel?: PageLabelStyle | ((file: string, appDir: string) => string);
11
+ };
12
+ restoreAfterBuild?: boolean;
8
13
  }
9
14
  export declare function autoDynamicPagesPlugin(options?: AutoDynamicPagesPluginOptions): Plugin;
@@ -1,6 +1,7 @@
1
- import { existsSync } from "node:fs";
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { checkDynamicPages } from "../dynamic_pages_check/index.js";
4
+ const RESTORABLE_ACTIONS = new Set(['added-force-dynamic', 'added-force-static']);
4
5
  export function autoDynamicPagesPlugin(options = {}) {
5
6
  let ran = false;
6
7
  return {
@@ -25,13 +26,41 @@ export function autoDynamicPagesPlugin(options = {}) {
25
26
  if (!appDir || !existsSync(appDir)) {
26
27
  return;
27
28
  }
29
+ const restoreAfterBuild = options.restoreAfterBuild ?? true;
30
+ const originals = new Map();
28
31
  try {
29
- await checkDynamicPages({
32
+ const reports = await checkDynamicPages({
30
33
  appDir,
31
34
  mode: options.mode ?? "fix",
32
35
  target: options.target ?? "vinext",
33
36
  syncErrorReportingAuthUser: options.syncErrorReportingAuthUser ?? false,
34
- });
37
+ extraChecks: options.extraChecks ?? [],
38
+ verbose: options.verbose ?? false,
39
+ }, restoreAfterBuild
40
+ ? {
41
+ writeFile: (file, contents) => {
42
+ if (!originals.has(file)) {
43
+ try {
44
+ originals.set(file, readFileSync(file, "utf8"));
45
+ }
46
+ catch {
47
+ }
48
+ }
49
+ writeFileSync(file, contents, "utf8");
50
+ },
51
+ }
52
+ : undefined);
53
+ if (!restoreAfterBuild)
54
+ return;
55
+ const restorable = new Set(reports
56
+ .filter((report) => RESTORABLE_ACTIONS.has(String(report.action)))
57
+ .map((report) => report.file));
58
+ for (const file of [...originals.keys()]) {
59
+ if (!restorable.has(file))
60
+ originals.delete(file);
61
+ }
62
+ if (originals.size > 0)
63
+ registerRestore(originals);
35
64
  }
36
65
  catch (err) {
37
66
  console.warn("[cloudflare-next-intl] autoDynamicPages check error:", err);
@@ -39,3 +68,25 @@ export function autoDynamicPagesPlugin(options = {}) {
39
68
  },
40
69
  };
41
70
  }
71
+ function registerRestore(originals) {
72
+ let restored = false;
73
+ const restore = () => {
74
+ if (restored)
75
+ return;
76
+ restored = true;
77
+ for (const [file, contents] of originals) {
78
+ try {
79
+ writeFileSync(file, contents, "utf8");
80
+ }
81
+ catch {
82
+ }
83
+ }
84
+ };
85
+ process.once("exit", restore);
86
+ for (const signal of ["SIGINT", "SIGTERM"]) {
87
+ process.once(signal, () => {
88
+ restore();
89
+ process.kill(process.pid, signal);
90
+ });
91
+ }
92
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.9.40",
3
+ "version": "0.9.43",
4
4
  "description": "Optimized Next Intl Package Special for App Router and Cloudflare",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",