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.
- package/dist/src/dynamic_pages_check/check_dynamic_pages.d.ts +9 -0
- package/dist/src/dynamic_pages_check/check_dynamic_pages.js +79 -7
- package/dist/src/dynamic_pages_check/collect_reachable_files.js +21 -2
- package/dist/src/dynamic_pages_check/derive_page_label.d.ts +5 -0
- package/dist/src/dynamic_pages_check/derive_page_label.js +57 -0
- package/dist/src/dynamic_pages_check/detect_dynamic_usage.d.ts +12 -1
- package/dist/src/dynamic_pages_check/detect_dynamic_usage.js +49 -8
- package/dist/src/dynamic_pages_check/index.d.ts +4 -1
- package/dist/src/dynamic_pages_check/index.js +4 -1
- package/dist/src/dynamic_pages_check/resolve_local_imports.d.ts +8 -0
- package/dist/src/dynamic_pages_check/resolve_local_imports.js +56 -0
- package/dist/src/dynamic_pages_check/trace_dynamic_usage.d.ts +10 -2
- package/dist/src/dynamic_pages_check/trace_dynamic_usage.js +14 -5
- package/dist/src/server/components/link.js +19 -1
- package/dist/src/vite/auto_dynamic_pages_plugin.d.ts +6 -1
- package/dist/src/vite/auto_dynamic_pages_plugin.js +54 -3
- package/package.json +1 -1
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
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(
|
|
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
|
|
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.
|
|
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.
|
|
15
|
+
detection.matches.forEach(({ name, line }) => {
|
|
16
|
+
detectedApis.add(name);
|
|
17
|
+
signals.push({ api: name, file, line });
|
|
18
|
+
});
|
|
15
19
|
}
|
|
16
|
-
return {
|
|
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
|
-
|
|
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
|
+
}
|