@translation-cms/sync 1.2.9 → 1.2.12
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/README.md +304 -151
- package/dist/api.d.ts +79 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +151 -0
- package/dist/api.js.map +1 -0
- package/dist/bin.js +51 -17
- package/dist/bin.js.map +1 -1
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +18 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/pull.d.ts +7 -0
- package/dist/commands/pull.d.ts.map +1 -1
- package/dist/commands/pull.js +15 -2
- package/dist/commands/pull.js.map +1 -1
- package/dist/commands/status.d.ts +12 -0
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +16 -0
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/sync.d.ts +10 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +23 -4
- package/dist/commands/sync.js.map +1 -1
- package/dist/commands/watch.d.ts +14 -0
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +23 -3
- package/dist/commands/watch.js.map +1 -1
- package/dist/config/config-internals/args.d.ts +11 -0
- package/dist/config/config-internals/args.d.ts.map +1 -0
- package/dist/config/config-internals/args.js +22 -0
- package/dist/config/config-internals/args.js.map +1 -0
- package/dist/config/config-internals/env.d.ts +10 -0
- package/dist/config/config-internals/env.d.ts.map +1 -0
- package/dist/config/config-internals/env.js +35 -0
- package/dist/config/config-internals/env.js.map +1 -0
- package/dist/config/config-internals/file.d.ts +11 -0
- package/dist/config/config-internals/file.d.ts.map +1 -0
- package/dist/config/config-internals/file.js +28 -0
- package/dist/config/config-internals/file.js.map +1 -0
- package/dist/config/config-internals/resolve.d.ts +21 -0
- package/dist/config/config-internals/resolve.d.ts.map +1 -0
- package/dist/config/config-internals/resolve.js +73 -0
- package/dist/config/config-internals/resolve.js.map +1 -0
- package/dist/config/config-internals/root.d.ts +9 -0
- package/dist/config/config-internals/root.d.ts.map +1 -0
- package/dist/config/config-internals/root.js +22 -0
- package/dist/config/config-internals/root.js.map +1 -0
- package/dist/config/config-internals/types.d.ts +83 -0
- package/dist/config/config-internals/types.d.ts.map +1 -0
- package/dist/config/config-internals/types.js +9 -0
- package/dist/config/config-internals/types.js.map +1 -0
- package/dist/config/resolve-config.d.ts +9 -90
- package/dist/config/resolve-config.d.ts.map +1 -1
- package/dist/config/resolve-config.js +7 -123
- package/dist/config/resolve-config.js.map +1 -1
- package/dist/core/api-internals/pull.d.ts +9 -1
- package/dist/core/api-internals/pull.d.ts.map +1 -1
- package/dist/core/api-internals/pull.js +41 -28
- package/dist/core/api-internals/pull.js.map +1 -1
- package/dist/core/api-internals/sync.d.ts +9 -2
- package/dist/core/api-internals/sync.d.ts.map +1 -1
- package/dist/core/api-internals/sync.js +18 -2
- package/dist/core/api-internals/sync.js.map +1 -1
- package/dist/core/cache-internals/format.d.ts +16 -0
- package/dist/core/cache-internals/format.d.ts.map +1 -1
- package/dist/core/cache-internals/format.js +17 -0
- package/dist/core/cache-internals/format.js.map +1 -1
- package/dist/core/cache-internals/params.d.ts.map +1 -1
- package/dist/core/cache-internals/params.js +24 -24
- package/dist/core/cache-internals/params.js.map +1 -1
- package/dist/core/cache-internals/pull.d.ts +12 -0
- package/dist/core/cache-internals/pull.d.ts.map +1 -1
- package/dist/core/cache-internals/pull.js +13 -0
- package/dist/core/cache-internals/pull.js.map +1 -1
- package/dist/core/cache-internals/sync.d.ts +23 -0
- package/dist/core/cache-internals/sync.d.ts.map +1 -1
- package/dist/core/cache-internals/sync.js +33 -0
- package/dist/core/cache-internals/sync.js.map +1 -1
- package/dist/core/cache-internals/types.d.ts +20 -0
- package/dist/core/cache-internals/types.d.ts.map +1 -1
- package/dist/core/scanner-internals/ast.d.ts +23 -5
- package/dist/core/scanner-internals/ast.d.ts.map +1 -1
- package/dist/core/scanner-internals/ast.js +25 -5
- package/dist/core/scanner-internals/ast.js.map +1 -1
- package/dist/core/scanner-internals/file-walker.d.ts +3 -2
- package/dist/core/scanner-internals/file-walker.d.ts.map +1 -1
- package/dist/core/scanner-internals/file-walker.js +10 -12
- package/dist/core/scanner-internals/file-walker.js.map +1 -1
- package/dist/core/scanner-internals/import-resolver.d.ts +9 -1
- package/dist/core/scanner-internals/import-resolver.d.ts.map +1 -1
- package/dist/core/scanner-internals/import-resolver.js +58 -63
- package/dist/core/scanner-internals/import-resolver.js.map +1 -1
- package/dist/core/scanner-internals/key-extractor.d.ts +12 -8
- package/dist/core/scanner-internals/key-extractor.d.ts.map +1 -1
- package/dist/core/scanner-internals/key-extractor.js +125 -97
- package/dist/core/scanner-internals/key-extractor.js.map +1 -1
- package/dist/core/scanner-internals/route-detector.d.ts +16 -8
- package/dist/core/scanner-internals/route-detector.d.ts.map +1 -1
- package/dist/core/scanner-internals/route-detector.js +37 -33
- package/dist/core/scanner-internals/route-detector.js.map +1 -1
- package/dist/core/scanner.d.ts.map +1 -1
- package/dist/core/scanner.js +25 -8
- package/dist/core/scanner.js.map +1 -1
- package/dist/next.d.ts +28 -0
- package/dist/next.d.ts.map +1 -0
- package/dist/next.js +65 -0
- package/dist/next.js.map +1 -0
- package/dist/preview/internals/highlight.d.ts +16 -7
- package/dist/preview/internals/highlight.d.ts.map +1 -1
- package/dist/preview/internals/highlight.js +71 -60
- package/dist/preview/internals/highlight.js.map +1 -1
- package/dist/scaffold/intenrals/scaffold.d.ts +17 -0
- package/dist/scaffold/intenrals/scaffold.d.ts.map +1 -1
- package/dist/scaffold/intenrals/scaffold.js +19 -0
- package/dist/scaffold/intenrals/scaffold.js.map +1 -1
- package/package.json +16 -3
|
@@ -1,57 +1,113 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Extraction of translation keys from source files.
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Five passes over the AST per file:
|
|
5
|
+
* 1. Collect `t`-binding declarations (useTranslation / getTranslation / getTranslations)
|
|
6
|
+
* 2. Extract keys from `t('namespace:key')` call expressions
|
|
7
|
+
* 3. Extract keys from `<Trans i18nKey="namespace:key">` JSX elements
|
|
8
|
+
* 4. Extract keys from `I18nKey`-typed string literals in config/data files
|
|
9
|
+
* 5. Extract keys from object properties whose name ends with `Key`
|
|
4
10
|
*/
|
|
5
11
|
import fs from 'fs';
|
|
6
12
|
import path from 'path';
|
|
7
13
|
import { parseSource, walk, stringValue, namespacesFromArg } from './ast.js';
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Small helpers
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
8
17
|
/**
|
|
9
18
|
* Add a key to the namespace map with its associated routes.
|
|
10
|
-
* Skips keys that look like paths or are
|
|
19
|
+
* Skips keys that look like URL paths or are purely numeric (not translation keys).
|
|
20
|
+
* Merges routes with any previously registered set for the same key.
|
|
11
21
|
*/
|
|
12
22
|
function addKey(map, namespace, key, routes) {
|
|
13
|
-
// Skip keys that look like URL paths or are suspiciously short/numeric
|
|
14
23
|
if (key.startsWith('/') || key.startsWith('.') || /^\d+$/.test(key))
|
|
15
24
|
return;
|
|
16
25
|
if (!map[namespace])
|
|
17
26
|
map[namespace] = new Map();
|
|
18
27
|
const existing = map[namespace].get(key) ?? [];
|
|
19
28
|
if (routes.length > 0) {
|
|
29
|
+
// Union the new routes with whatever was already stored
|
|
20
30
|
const merged = new Set(existing);
|
|
21
31
|
for (const route of routes)
|
|
22
32
|
merged.add(route);
|
|
23
33
|
map[namespace].set(key, Array.from(merged));
|
|
24
34
|
}
|
|
25
35
|
else if (existing.length === 0) {
|
|
36
|
+
// Key seen with no routes yet — register it so it appears in the map
|
|
26
37
|
map[namespace].set(key, []);
|
|
27
38
|
}
|
|
28
39
|
}
|
|
29
40
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* - const { t } = useTranslation('ns') / getTranslation('ns')
|
|
33
|
-
* - const { t: myT } = useTranslation(...)
|
|
34
|
-
* - const t = await client.getTranslations(locale, 'ns')
|
|
35
|
-
* - t('namespace:key') calls
|
|
36
|
-
* - <Trans i18nKey="namespace:key"> components
|
|
41
|
+
* Split a `"namespace:key"` string into its two parts.
|
|
42
|
+
* Returns null when the colon is absent or at position 0 (invalid).
|
|
37
43
|
*/
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
function splitI18nKey(keyStr) {
|
|
45
|
+
const colonIdx = keyStr.indexOf(':');
|
|
46
|
+
if (colonIdx <= 0)
|
|
47
|
+
return null;
|
|
48
|
+
return [keyStr.slice(0, colonIdx), keyStr.slice(colonIdx + 1)];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Extract the string value of a JSX attribute value node.
|
|
52
|
+
* Handles plain string literals (`i18nKey="ns:key"`) and expression containers
|
|
53
|
+
* (`i18nKey={"ns:key"}`). Returns null for dynamic expressions.
|
|
54
|
+
*/
|
|
55
|
+
function jsxAttrStringValue(value) {
|
|
56
|
+
if (!value)
|
|
57
|
+
return null;
|
|
58
|
+
if (value.type === 'StringLiteral')
|
|
59
|
+
return value.value;
|
|
60
|
+
if (value.type === 'JSXExpressionContainer')
|
|
61
|
+
return stringValue(value.expression);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Bind translation function aliases from a destructuring pattern.
|
|
66
|
+
*
|
|
67
|
+
* Handles both direct binding and renaming:
|
|
68
|
+
* `const { t } = useTranslation()` → binds `t`
|
|
69
|
+
* `const { t: myT } = useTranslation()` → binds `myT`
|
|
70
|
+
* `const { tDynamic } = getTranslation()` → binds `tDynamic`
|
|
71
|
+
*
|
|
72
|
+
* Only the standard accessor names (`t`, `tDynamic`) are recognised as
|
|
73
|
+
* source keys; any other destructured property is ignored.
|
|
74
|
+
*/
|
|
75
|
+
function bindFromObjectPattern(id, namespaces, tBindings) {
|
|
76
|
+
for (const prop of id.properties) {
|
|
77
|
+
if (prop.type !== 'ObjectProperty')
|
|
78
|
+
continue;
|
|
79
|
+
const keyNode = prop.key;
|
|
80
|
+
const origName = keyNode.type === 'Identifier' ? keyNode.name : null;
|
|
81
|
+
if (!origName || (origName !== 't' && origName !== 'tDynamic'))
|
|
82
|
+
continue;
|
|
83
|
+
// Use the aliased name if present: `{ t: myT }` → bind `myT`
|
|
84
|
+
const valNode = prop.value;
|
|
85
|
+
const boundName = valNode.type === 'Identifier' ? valNode.name : origName;
|
|
86
|
+
tBindings.set(boundName, namespaces);
|
|
45
87
|
}
|
|
88
|
+
}
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Main export
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
/**
|
|
93
|
+
* Extract all translation keys from `filePath` and merge them into `result`.
|
|
94
|
+
*
|
|
95
|
+
* @param filePath Absolute path to the source file to scan.
|
|
96
|
+
* @param routes Preview routes associated with this file ([] for overlays).
|
|
97
|
+
* @param result Accumulator map updated in place.
|
|
98
|
+
*/
|
|
99
|
+
export function extractKeysFromFile(filePath, routes, result) {
|
|
46
100
|
let ast;
|
|
47
101
|
try {
|
|
48
|
-
|
|
102
|
+
// Single try/catch covers both the file read and the parse step
|
|
103
|
+
ast = parseSource(filePath, fs.readFileSync(filePath, 'utf-8'));
|
|
49
104
|
}
|
|
50
105
|
catch {
|
|
51
106
|
return;
|
|
52
107
|
}
|
|
53
108
|
// --- Step 1: collect namespace bindings ---
|
|
54
|
-
// Maps
|
|
109
|
+
// Maps each local variable name (e.g. `t`) to the namespace(s) it is bound to
|
|
110
|
+
// so that call-expression extraction in Step 2 can resolve the correct namespace.
|
|
55
111
|
const tBindings = new Map();
|
|
56
112
|
walk(ast, node => {
|
|
57
113
|
if (node.type !== 'VariableDeclaration')
|
|
@@ -60,7 +116,7 @@ export function extractKeysFromFile(filePath, routes, result) {
|
|
|
60
116
|
const init = decl.init;
|
|
61
117
|
if (!init)
|
|
62
118
|
continue;
|
|
63
|
-
// Unwrap await
|
|
119
|
+
// Unwrap `await` so `const t = await client.getTranslations(...)` works
|
|
64
120
|
const call = init.type === 'AwaitExpression'
|
|
65
121
|
? init.argument
|
|
66
122
|
: init;
|
|
@@ -70,17 +126,15 @@ export function extractKeysFromFile(filePath, routes, result) {
|
|
|
70
126
|
const args = call.arguments;
|
|
71
127
|
// useTranslation('ns') / getTranslation('ns')
|
|
72
128
|
const isHook = callee.type === 'Identifier' &&
|
|
73
|
-
(callee.name === 'useTranslation' ||
|
|
74
|
-
callee.name === 'getTranslation');
|
|
129
|
+
(callee.name === 'useTranslation' || callee.name === 'getTranslation');
|
|
75
130
|
// client.getTranslations(locale, 'ns') / client.getT(locale, 'ns')
|
|
76
131
|
const isCmsStyle = callee.type === 'MemberExpression' &&
|
|
77
132
|
callee.property.type === 'Identifier' &&
|
|
78
133
|
/^getTranslations(?:WithFallback)?$|^getT$/.test(callee.property.name);
|
|
79
134
|
if (!isHook && !isCmsStyle)
|
|
80
135
|
continue;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
: namespacesFromArg(args[1]); // CMS style: getTranslations(locale, ns)
|
|
136
|
+
// Namespace arg position differs: hook uses args[0], CMS style uses args[1]
|
|
137
|
+
const namespaces = isHook ? namespacesFromArg(args[0]) : namespacesFromArg(args[1]);
|
|
84
138
|
if (!namespaces.length)
|
|
85
139
|
continue;
|
|
86
140
|
const id = decl.id;
|
|
@@ -89,28 +143,14 @@ export function extractKeysFromFile(filePath, routes, result) {
|
|
|
89
143
|
tBindings.set(id.name, namespaces);
|
|
90
144
|
}
|
|
91
145
|
else if (id.type === 'ObjectPattern') {
|
|
92
|
-
// const { t } = useTranslation(...)
|
|
93
|
-
|
|
94
|
-
if (prop.type !== 'ObjectProperty')
|
|
95
|
-
continue;
|
|
96
|
-
const keyNode = prop.key;
|
|
97
|
-
const valNode = prop.value;
|
|
98
|
-
const origName = keyNode.type === 'Identifier'
|
|
99
|
-
? keyNode.name
|
|
100
|
-
: null;
|
|
101
|
-
if (!origName ||
|
|
102
|
-
(origName !== 't' && origName !== 'tDynamic'))
|
|
103
|
-
continue;
|
|
104
|
-
const boundName = valNode.type === 'Identifier'
|
|
105
|
-
? valNode.name
|
|
106
|
-
: origName;
|
|
107
|
-
tBindings.set(boundName, namespaces);
|
|
108
|
-
}
|
|
146
|
+
// const { t } = useTranslation(...) or const { t: myT } = ...
|
|
147
|
+
bindFromObjectPattern(id, namespaces, tBindings);
|
|
109
148
|
}
|
|
110
149
|
}
|
|
111
150
|
});
|
|
112
|
-
// --- Step 2: extract keys from t('
|
|
113
|
-
// Only
|
|
151
|
+
// --- Step 2: extract keys from t('…') call expressions ---
|
|
152
|
+
// Only variables explicitly bound in Step 1 are processed; unknown callee
|
|
153
|
+
// names are silently skipped to avoid false positives.
|
|
114
154
|
walk(ast, node => {
|
|
115
155
|
if (node.type !== 'CallExpression')
|
|
116
156
|
return;
|
|
@@ -118,27 +158,27 @@ export function extractKeysFromFile(filePath, routes, result) {
|
|
|
118
158
|
if (callee.type !== 'Identifier')
|
|
119
159
|
return;
|
|
120
160
|
const varName = callee.name;
|
|
121
|
-
// Only process if this variable is bound to a translation function
|
|
122
161
|
if (!tBindings.has(varName))
|
|
123
162
|
return;
|
|
124
|
-
const
|
|
125
|
-
const keyStr = stringValue(args[0]);
|
|
163
|
+
const keyStr = stringValue(node.arguments[0]);
|
|
126
164
|
if (!keyStr)
|
|
127
165
|
return;
|
|
128
|
-
const
|
|
129
|
-
if (
|
|
130
|
-
//
|
|
131
|
-
addKey(result,
|
|
166
|
+
const parts = splitI18nKey(keyStr);
|
|
167
|
+
if (parts) {
|
|
168
|
+
// t('namespace:key') — namespace is explicit in the key string
|
|
169
|
+
addKey(result, parts[0], parts[1], routes);
|
|
132
170
|
}
|
|
133
171
|
else {
|
|
134
|
-
// Bare key: t('key')
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
for (const ns of
|
|
172
|
+
// Bare key: t('key') — namespace comes from the binding; emit a warning
|
|
173
|
+
console.warn(`[WARN] Bare key in ${path.relative(process.cwd(), filePath)}: ` +
|
|
174
|
+
`${varName}('${keyStr}') — use ${varName}('namespace:${keyStr}') instead`);
|
|
175
|
+
for (const ns of tBindings.get(varName))
|
|
138
176
|
addKey(result, ns, keyStr, routes);
|
|
139
177
|
}
|
|
140
178
|
});
|
|
141
179
|
// --- Step 3: <Trans i18nKey="namespace:key"> ---
|
|
180
|
+
// The Trans component is the JSX equivalent of t(); only fully-qualified
|
|
181
|
+
// "namespace:key" values are accepted (bare keys cannot be resolved here).
|
|
142
182
|
walk(ast, node => {
|
|
143
183
|
if (node.type !== 'JSXOpeningElement')
|
|
144
184
|
return;
|
|
@@ -149,50 +189,37 @@ export function extractKeysFromFile(filePath, routes, result) {
|
|
|
149
189
|
if (attr.type !== 'JSXAttribute')
|
|
150
190
|
continue;
|
|
151
191
|
const attrName = attr.name;
|
|
152
|
-
if (attrName.type !== 'JSXIdentifier' ||
|
|
153
|
-
attrName.name !== 'i18nKey')
|
|
192
|
+
if (attrName.type !== 'JSXIdentifier' || attrName.name !== 'i18nKey')
|
|
154
193
|
continue;
|
|
155
|
-
const
|
|
156
|
-
let keyStr = null;
|
|
157
|
-
if (value?.type === 'StringLiteral') {
|
|
158
|
-
keyStr = value.value;
|
|
159
|
-
}
|
|
160
|
-
else if (value?.type === 'JSXExpressionContainer') {
|
|
161
|
-
keyStr = stringValue(value.expression);
|
|
162
|
-
}
|
|
194
|
+
const keyStr = jsxAttrStringValue(attr.value);
|
|
163
195
|
if (!keyStr)
|
|
164
196
|
continue;
|
|
165
|
-
const
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
addKey(result, keyStr.slice(0, colonIdx), keyStr.slice(colonIdx + 1), routes);
|
|
197
|
+
const parts = splitI18nKey(keyStr);
|
|
198
|
+
if (parts)
|
|
199
|
+
addKey(result, parts[0], parts[1], routes);
|
|
169
200
|
}
|
|
170
201
|
});
|
|
171
202
|
// --- Step 4: I18nKey string literals in config/data files ---
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
if (importsI18nKey) {
|
|
203
|
+
// When a file imports the `I18nKey` type it likely contains typed config
|
|
204
|
+
// objects (e.g. nav items with `label: 'sidebar:settings'`). Scan every
|
|
205
|
+
// string literal that matches "namespace:key" to catch these.
|
|
206
|
+
if (hasI18nKeyImport(ast)) {
|
|
177
207
|
walk(ast, node => {
|
|
178
208
|
if (node.type !== 'StringLiteral')
|
|
179
209
|
return;
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
if (colonIdx <= 0)
|
|
210
|
+
const parts = splitI18nKey(node.value);
|
|
211
|
+
if (!parts)
|
|
183
212
|
return;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
// Namespace and key must look like identifiers/dot-paths (no spaces, slashes, etc.)
|
|
187
|
-
if (!/^\w[\w-]*$/.test(ns) || !/^\w[\w.-]*$/.test(key))
|
|
213
|
+
// Namespace and key must look like identifiers/dot-paths (no spaces, slashes…)
|
|
214
|
+
if (!/^\w[\w-]*$/.test(parts[0]) || !/^\w[\w.-]*$/.test(parts[1]))
|
|
188
215
|
return;
|
|
189
|
-
addKey(result,
|
|
216
|
+
addKey(result, parts[0], parts[1], routes);
|
|
190
217
|
});
|
|
191
218
|
}
|
|
192
219
|
// --- Step 5: *Key object property string literals ---
|
|
193
|
-
// Catches translation keys passed as
|
|
220
|
+
// Catches translation keys passed as prop/config values where the property
|
|
194
221
|
// name ends with "Key" (e.g. `titleKey: 'subscription:expired'`).
|
|
195
|
-
//
|
|
222
|
+
// The naming convention is the only signal needed — no import sentinel required.
|
|
196
223
|
walk(ast, node => {
|
|
197
224
|
if (node.type !== 'ObjectProperty' && node.type !== 'Property')
|
|
198
225
|
return;
|
|
@@ -207,29 +234,30 @@ export function extractKeysFromFile(filePath, routes, result) {
|
|
|
207
234
|
const val = stringValue(node.value);
|
|
208
235
|
if (!val)
|
|
209
236
|
return;
|
|
210
|
-
const
|
|
211
|
-
if (
|
|
237
|
+
const parts = splitI18nKey(val);
|
|
238
|
+
if (!parts)
|
|
212
239
|
return;
|
|
213
|
-
|
|
214
|
-
const key = val.slice(colonIdx + 1);
|
|
215
|
-
if (!/^\w[\w-]*$/.test(ns) || !/^\w[\w.-]*$/.test(key))
|
|
240
|
+
if (!/^\w[\w-]*$/.test(parts[0]) || !/^\w[\w.-]*$/.test(parts[1]))
|
|
216
241
|
return;
|
|
217
|
-
addKey(result,
|
|
242
|
+
addKey(result, parts[0], parts[1], routes);
|
|
218
243
|
});
|
|
219
244
|
}
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// Internal helpers
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
220
248
|
/**
|
|
221
|
-
* Returns true if the AST contains an import of the `I18nKey` type
|
|
222
|
-
*
|
|
249
|
+
* Returns true if the AST contains an import of the `I18nKey` type.
|
|
250
|
+
* Used as a cheap sentinel to decide whether Step 4 should run for a file.
|
|
251
|
+
*
|
|
252
|
+
* The `found` flag pattern is necessary because `walk()` does not support
|
|
253
|
+
* early termination — we skip further work as soon as the import is spotted.
|
|
223
254
|
*/
|
|
224
255
|
function hasI18nKeyImport(ast) {
|
|
225
256
|
let found = false;
|
|
226
257
|
walk(ast, node => {
|
|
227
|
-
if (found)
|
|
228
|
-
return;
|
|
229
|
-
if (node.type !== 'ImportDeclaration')
|
|
258
|
+
if (found || node.type !== 'ImportDeclaration')
|
|
230
259
|
return;
|
|
231
|
-
const
|
|
232
|
-
for (const spec of specifiers) {
|
|
260
|
+
for (const spec of node.specifiers) {
|
|
233
261
|
if (spec.type === 'ImportSpecifier' &&
|
|
234
262
|
spec.imported?.name === 'I18nKey') {
|
|
235
263
|
found = true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"key-extractor.js","sourceRoot":"","sources":["../../../src/core/scanner-internals/key-extractor.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"key-extractor.js","sourceRoot":"","sources":["../../../src/core/scanner-internals/key-extractor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAG7E,8EAA8E;AAC9E,gBAAgB;AAChB,8EAA8E;AAE9E;;;;GAIG;AACH,SAAS,MAAM,CACX,GAAiB,EACjB,SAAiB,EACjB,GAAW,EACX,MAAgB;IAEhB,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO;IAC5E,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC;QAAE,GAAG,CAAC,SAAS,CAAC,GAAG,IAAI,GAAG,EAAE,CAAC;IAChD,MAAM,QAAQ,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;IAE/C,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpB,wDAAwD;QACxD,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC;QACjC,KAAK,MAAM,KAAK,IAAI,MAAM;YAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAC9C,GAAG,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IAChD,CAAC;SAAM,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/B,qEAAqE;QACrE,GAAG,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAChC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,SAAS,YAAY,CAAC,MAAc;IAChC,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACrC,IAAI,QAAQ,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC;AACnE,CAAC;AAED;;;;GAIG;AACH,SAAS,kBAAkB,CAAC,KAAqB;IAC7C,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,IAAI,KAAK,CAAC,IAAI,KAAK,eAAe;QAAE,OAAO,KAAK,CAAC,KAAe,CAAC;IACjE,IAAI,KAAK,CAAC,IAAI,KAAK,wBAAwB;QAAE,OAAO,WAAW,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IAClF,OAAO,IAAI,CAAC;AAChB,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,qBAAqB,CAC1B,EAAW,EACX,UAAoB,EACpB,SAAgC;IAEhC,KAAK,MAAM,IAAI,IAAI,EAAE,CAAC,UAAuB,EAAE,CAAC;QAC5C,IAAI,IAAI,CAAC,IAAI,KAAK,gBAAgB;YAAE,SAAS;QAE7C,MAAM,OAAO,GAAG,IAAI,CAAC,GAAc,CAAC;QACpC,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,KAAK,YAAY,CAAC,CAAC,CAAE,OAAO,CAAC,IAAe,CAAC,CAAC,CAAC,IAAI,CAAC;QACjF,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ,KAAK,GAAG,IAAI,QAAQ,KAAK,UAAU,CAAC;YAAE,SAAS;QAEzE,6DAA6D;QAC7D,MAAM,OAAO,GAAG,IAAI,CAAC,KAAgB,CAAC;QACtC,MAAM,SAAS,GACX,OAAO,CAAC,IAAI,KAAK,YAAY,CAAC,CAAC,CAAE,OAAO,CAAC,IAAe,CAAC,CAAC,CAAC,QAAQ,CAAC;QAExE,SAAS,CAAC,GAAG,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IACzC,CAAC;AACL,CAAC;AAED,8EAA8E;AAC9E,cAAc;AACd,8EAA8E;AAE9E;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAC/B,QAAgB,EAChB,MAAgB,EAChB,MAAoB;IAEpB,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QACD,gEAAgE;QAChE,GAAG,GAAG,WAAW,CAAC,QAAQ,EAAE,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;IACpE,CAAC;IAAC,MAAM,CAAC;QACL,OAAO;IACX,CAAC;IAED,6CAA6C;IAC7C,8EAA8E;IAC9E,kFAAkF;IAClF,MAAM,SAAS,GAAG,IAAI,GAAG,EAAoB,CAAC;IAE9C,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE;QACb,IAAI,IAAI,CAAC,IAAI,KAAK,qBAAqB;YAAE,OAAO;QAEhD,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,YAAyB,EAAE,CAAC;YAChD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAkC,CAAC;YACrD,IAAI,CAAC,IAAI;gBAAE,SAAS;YAEpB,wEAAwE;YACxE,MAAM,IAAI,GACN,IAAI,CAAC,IAAI,KAAK,iBAAiB;gBAC3B,CAAC,CAAE,IAAI,CAAC,QAAgC;gBACxC,CAAC,CAAC,IAAI,CAAC;YACf,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,gBAAgB;gBAAE,SAAS;YAEtD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAiB,CAAC;YACtC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAsB,CAAC;YAEzC,8CAA8C;YAC9C,MAAM,MAAM,GACR,MAAM,CAAC,IAAI,KAAK,YAAY;gBAC5B,CAAC,MAAM,CAAC,IAAI,KAAK,gBAAgB,IAAI,MAAM,CAAC,IAAI,KAAK,gBAAgB,CAAC,CAAC;YAE3E,mEAAmE;YACnE,MAAM,UAAU,GACZ,MAAM,CAAC,IAAI,KAAK,kBAAkB;gBACjC,MAAM,CAAC,QAAoB,CAAC,IAAI,KAAK,YAAY;gBAClD,2CAA2C,CAAC,IAAI,CAC3C,MAAM,CAAC,QAAoB,CAAC,IAAc,CAC9C,CAAC;YAEN,IAAI,CAAC,MAAM,IAAI,CAAC,UAAU;gBAAE,SAAS;YAErC,4EAA4E;YAC5E,MAAM,UAAU,GAAG,MAAM,CAAC,CAAC,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;YACpF,IAAI,CAAC,UAAU,CAAC,MAAM;gBAAE,SAAS;YAEjC,MAAM,EAAE,GAAG,IAAI,CAAC,EAAa,CAAC;YAC9B,IAAI,EAAE,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;gBAC3B,gCAAgC;gBAChC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,IAAc,EAAE,UAAU,CAAC,CAAC;YACjD,CAAC;iBAAM,IAAI,EAAE,CAAC,IAAI,KAAK,eAAe,EAAE,CAAC;gBACrC,gEAAgE;gBAChE,qBAAqB,CAAC,EAAE,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC;YACrD,CAAC;QACL,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,4DAA4D;IAC5D,0EAA0E;IAC1E,uDAAuD;IACvD,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE;QACb,IAAI,IAAI,CAAC,IAAI,KAAK,gBAAgB;YAAE,OAAO;QAE3C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAiB,CAAC;QACtC,IAAI,MAAM,CAAC,IAAI,KAAK,YAAY;YAAE,OAAO;QAEzC,MAAM,OAAO,GAAG,MAAM,CAAC,IAAc,CAAC;QACtC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,OAAO;QAEpC,MAAM,MAAM,GAAG,WAAW,CAAE,IAAI,CAAC,SAAuB,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7D,IAAI,CAAC,MAAM;YAAE,OAAO;QAEpB,MAAM,KAAK,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;QACnC,IAAI,KAAK,EAAE,CAAC;YACR,+DAA+D;YAC/D,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QAC/C,CAAC;aAAM,CAAC;YACJ,wEAAwE;YACxE,OAAO,CAAC,IAAI,CACR,sBAAsB,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,QAAQ,CAAC,IAAI;gBAChE,GAAG,OAAO,KAAK,MAAM,YAAY,OAAO,eAAe,MAAM,YAAY,CAC5E,CAAC;YACF,KAAK,MAAM,EAAE,IAAI,SAAS,CAAC,GAAG,CAAC,OAAO,CAAE;gBAAE,MAAM,CAAC,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;QACjF,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,kDAAkD;IAClD,yEAAyE;IACzE,2EAA2E;IAC3E,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE;QACb,IAAI,IAAI,CAAC,IAAI,KAAK,mBAAmB;YAAE,OAAO;QAC9C,MAAM,IAAI,GAAG,IAAI,CAAC,IAAe,CAAC;QAClC,IAAI,IAAI,CAAC,IAAI,KAAK,eAAe,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO;YAAE,OAAO;QAEnE,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,UAAuB,EAAE,CAAC;YAC9C,IAAI,IAAI,CAAC,IAAI,KAAK,cAAc;gBAAE,SAAS;YAE3C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAe,CAAC;YACtC,IAAI,QAAQ,CAAC,IAAI,KAAK,eAAe,IAAI,QAAQ,CAAC,IAAI,KAAK,SAAS;gBAAE,SAAS;YAE/E,MAAM,MAAM,GAAG,kBAAkB,CAAC,IAAI,CAAC,KAAuB,CAAC,CAAC;YAChE,IAAI,CAAC,MAAM;gBAAE,SAAS;YAEtB,MAAM,KAAK,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;YACnC,IAAI,KAAK;gBAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QAC1D,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,+DAA+D;IAC/D,yEAAyE;IACzE,yEAAyE;IACzE,8DAA8D;IAC9D,IAAI,gBAAgB,CAAC,GAAG,CAAC,EAAE,CAAC;QACxB,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE;YACb,IAAI,IAAI,CAAC,IAAI,KAAK,eAAe;gBAAE,OAAO;YAC1C,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,KAAe,CAAC,CAAC;YACjD,IAAI,CAAC,KAAK;gBAAE,OAAO;YACnB,+EAA+E;YAC/E,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;gBAAE,OAAO;YAC1E,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QAC/C,CAAC,CAAC,CAAC;IACP,CAAC;IAED,uDAAuD;IACvD,2EAA2E;IAC3E,kEAAkE;IAClE,iFAAiF;IACjF,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE;QACb,IAAI,IAAI,CAAC,IAAI,KAAK,gBAAgB,IAAI,IAAI,CAAC,IAAI,KAAK,UAAU;YAAE,OAAO;QAEvE,MAAM,OAAO,GAAG,IAAI,CAAC,GAAc,CAAC;QACpC,MAAM,QAAQ,GACV,OAAO,CAAC,IAAI,KAAK,YAAY;YACzB,CAAC,CAAE,OAAO,CAAC,IAAe;YAC1B,CAAC,CAAC,OAAO,CAAC,IAAI,KAAK,eAAe;gBAChC,CAAC,CAAE,OAAO,CAAC,KAAgB;gBAC3B,CAAC,CAAC,IAAI,CAAC;QACjB,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,KAAK,CAAC;YAAE,OAAO;QAEvC,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,KAAgB,CAAC,CAAC;QAC/C,IAAI,CAAC,GAAG;YAAE,OAAO;QAEjB,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;QAChC,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO;QAC1E,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;AACP,CAAC;AAED,8EAA8E;AAC9E,mBAAmB;AACnB,8EAA8E;AAE9E;;;;;;GAMG;AACH,SAAS,gBAAgB,CAAC,GAAY;IAClC,IAAI,KAAK,GAAG,KAAK,CAAC;IAClB,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE;QACb,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,KAAK,mBAAmB;YAAE,OAAO;QACvD,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,UAAuB,EAAE,CAAC;YAC9C,IACI,IAAI,CAAC,IAAI,KAAK,iBAAiB;gBAC9B,IAAI,CAAC,QAAoB,EAAE,IAAI,KAAK,SAAS,EAChD,CAAC;gBACC,KAAK,GAAG,IAAI,CAAC;gBACb,OAAO;YACX,CAAC;QACL,CAAC;IACL,CAAC,CAAC,CAAC;IACH,OAAO,KAAK,CAAC;AACjB,CAAC"}
|
|
@@ -3,17 +3,25 @@
|
|
|
3
3
|
* Maps source files (page.tsx, layout.tsx) to their corresponding route strings.
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
|
-
* Returns true when a page file only calls redirect() without rendering any JSX.
|
|
7
|
-
*
|
|
6
|
+
* Returns true when a page file only calls `redirect()` without rendering any JSX.
|
|
7
|
+
*
|
|
8
|
+
* Such pages have no visible UI, so they are excluded from the preview route list —
|
|
9
|
+
* there is nothing for a translator to preview.
|
|
8
10
|
*/
|
|
9
11
|
export declare function isRedirectOnlyPage(filePath: string): boolean;
|
|
10
12
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* Map a source file path to the Next.js route string(s) it represents.
|
|
14
|
+
*
|
|
15
|
+
* Rules:
|
|
16
|
+
* - Only `page.[jt]sx?` files produce a route; all other files return `[]`.
|
|
17
|
+
* - Pages that only call `redirect()` (no JSX) return `[]` — no UI to preview.
|
|
18
|
+
* - Route groups `(groupName)/` are stripped from the route path.
|
|
19
|
+
* - Dynamic segments `[param]` are kept as-is; they are filled in at preview
|
|
20
|
+
* time using the project's `preview_params` configuration.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* filePathToRoute(root, 'src/app/[locale]/blog/[slug]/page.tsx')
|
|
24
|
+
* // → ['/[locale]/blog/[slug]']
|
|
17
25
|
*/
|
|
18
26
|
export declare function filePathToRoute(root: string, filePath: string): string[];
|
|
19
27
|
//# sourceMappingURL=route-detector.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-detector.d.ts","sourceRoot":"","sources":["../../../src/core/scanner-internals/route-detector.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH
|
|
1
|
+
{"version":3,"file":"route-detector.d.ts","sourceRoot":"","sources":["../../../src/core/scanner-internals/route-detector.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAoC5D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAgBxE"}
|
|
@@ -6,20 +6,16 @@ import fs from 'fs';
|
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import { parseSource, walk } from './ast.js';
|
|
8
8
|
/**
|
|
9
|
-
* Returns true when a page file only calls redirect() without rendering any JSX.
|
|
10
|
-
*
|
|
9
|
+
* Returns true when a page file only calls `redirect()` without rendering any JSX.
|
|
10
|
+
*
|
|
11
|
+
* Such pages have no visible UI, so they are excluded from the preview route list —
|
|
12
|
+
* there is nothing for a translator to preview.
|
|
11
13
|
*/
|
|
12
14
|
export function isRedirectOnlyPage(filePath) {
|
|
13
|
-
let source;
|
|
14
|
-
try {
|
|
15
|
-
source = fs.readFileSync(filePath, 'utf-8');
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
15
|
let ast;
|
|
21
16
|
try {
|
|
22
|
-
|
|
17
|
+
// Single try/catch covers both file read and parse
|
|
18
|
+
ast = parseSource(filePath, fs.readFileSync(filePath, 'utf-8'));
|
|
23
19
|
}
|
|
24
20
|
catch {
|
|
25
21
|
return false;
|
|
@@ -27,39 +23,47 @@ export function isRedirectOnlyPage(filePath) {
|
|
|
27
23
|
let hasRedirect = false;
|
|
28
24
|
let hasJsxReturn = false;
|
|
29
25
|
walk(ast, node => {
|
|
26
|
+
// Detect redirect() calls
|
|
30
27
|
if (node.type === 'CallExpression') {
|
|
31
28
|
const callee = node.callee;
|
|
32
|
-
if (callee.type === 'Identifier' && callee.name === 'redirect')
|
|
29
|
+
if (callee.type === 'Identifier' && callee.name === 'redirect')
|
|
33
30
|
hasRedirect = true;
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
if (node.type === 'ReturnStatement') {
|
|
37
|
-
const arg = node.argument;
|
|
38
|
-
if (!arg)
|
|
39
|
-
return;
|
|
40
|
-
const expr = arg.type === 'ParenthesizedExpression'
|
|
41
|
-
? arg.expression
|
|
42
|
-
: arg;
|
|
43
|
-
if (expr?.type === 'JSXElement' || expr?.type === 'JSXFragment') {
|
|
44
|
-
hasJsxReturn = true;
|
|
45
|
-
}
|
|
31
|
+
return;
|
|
46
32
|
}
|
|
33
|
+
// Detect return statements that yield JSX (page has visible UI)
|
|
34
|
+
if (node.type !== 'ReturnStatement')
|
|
35
|
+
return;
|
|
36
|
+
const arg = node.argument;
|
|
37
|
+
if (!arg)
|
|
38
|
+
return;
|
|
39
|
+
// Unwrap parenthesised expressions: return (<div>…</div>)
|
|
40
|
+
const expr = arg.type === 'ParenthesizedExpression'
|
|
41
|
+
? arg.expression
|
|
42
|
+
: arg;
|
|
43
|
+
if (expr?.type === 'JSXElement' || expr?.type === 'JSXFragment')
|
|
44
|
+
hasJsxReturn = true;
|
|
47
45
|
});
|
|
48
46
|
return hasRedirect && !hasJsxReturn;
|
|
49
47
|
}
|
|
50
48
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
49
|
+
* Map a source file path to the Next.js route string(s) it represents.
|
|
50
|
+
*
|
|
51
|
+
* Rules:
|
|
52
|
+
* - Only `page.[jt]sx?` files produce a route; all other files return `[]`.
|
|
53
|
+
* - Pages that only call `redirect()` (no JSX) return `[]` — no UI to preview.
|
|
54
|
+
* - Route groups `(groupName)/` are stripped from the route path.
|
|
55
|
+
* - Dynamic segments `[param]` are kept as-is; they are filled in at preview
|
|
56
|
+
* time using the project's `preview_params` configuration.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* filePathToRoute(root, 'src/app/[locale]/blog/[slug]/page.tsx')
|
|
60
|
+
* // → ['/[locale]/blog/[slug]']
|
|
57
61
|
*/
|
|
58
62
|
export function filePathToRoute(root, filePath) {
|
|
59
|
-
|
|
60
|
-
if (
|
|
63
|
+
// Only page files map to routes; layouts, components, hooks, etc. do not
|
|
64
|
+
if (path.basename(filePath, path.extname(filePath)) !== 'page')
|
|
61
65
|
return [];
|
|
62
|
-
// Skip
|
|
66
|
+
// Skip redirect-only pages — no UI to preview
|
|
63
67
|
if (isRedirectOnlyPage(filePath))
|
|
64
68
|
return [];
|
|
65
69
|
const rel = path.relative(root, filePath).replace(/\\/g, '/');
|
|
@@ -67,7 +71,7 @@ export function filePathToRoute(root, filePath) {
|
|
|
67
71
|
if (!appMatch)
|
|
68
72
|
return [];
|
|
69
73
|
let routePath = appMatch[1] ?? '';
|
|
70
|
-
routePath = routePath.replace(/\([^)]+\)\//g, ''); // strip
|
|
74
|
+
routePath = routePath.replace(/\([^)]+\)\//g, ''); // strip (routeGroup)/ segments
|
|
71
75
|
routePath = routePath.replace(/\/$/, ''); // strip trailing slash
|
|
72
76
|
return ['/' + routePath];
|
|
73
77
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-detector.js","sourceRoot":"","sources":["../../../src/core/scanner-internals/route-detector.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAG7C
|
|
1
|
+
{"version":3,"file":"route-detector.js","sourceRoot":"","sources":["../../../src/core/scanner-internals/route-detector.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAG7C;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAgB;IAC/C,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QACD,mDAAmD;QACnD,GAAG,GAAG,WAAW,CAAC,QAAQ,EAAE,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;IACpE,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,KAAK,CAAC;IACjB,CAAC;IAED,IAAI,WAAW,GAAG,KAAK,CAAC;IACxB,IAAI,YAAY,GAAG,KAAK,CAAC;IAEzB,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE;QACb,0BAA0B;QAC1B,IAAI,IAAI,CAAC,IAAI,KAAK,gBAAgB,EAAE,CAAC;YACjC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAiB,CAAC;YACtC,IAAI,MAAM,CAAC,IAAI,KAAK,YAAY,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU;gBAC1D,WAAW,GAAG,IAAI,CAAC;YACvB,OAAO;QACX,CAAC;QAED,gEAAgE;QAChE,IAAI,IAAI,CAAC,IAAI,KAAK,iBAAiB;YAAE,OAAO;QAC5C,MAAM,GAAG,GAAG,IAAI,CAAC,QAAsC,CAAC;QACxD,IAAI,CAAC,GAAG;YAAE,OAAO;QAEjB,0DAA0D;QAC1D,MAAM,IAAI,GACN,GAAG,CAAC,IAAI,KAAK,yBAAyB;YAClC,CAAC,CAAE,GAAG,CAAC,UAAsB;YAC7B,CAAC,CAAC,GAAG,CAAC;QACd,IAAI,IAAI,EAAE,IAAI,KAAK,YAAY,IAAI,IAAI,EAAE,IAAI,KAAK,aAAa;YAC3D,YAAY,GAAG,IAAI,CAAC;IAC5B,CAAC,CAAC,CAAC;IAEH,OAAO,WAAW,IAAI,CAAC,YAAY,CAAC;AACxC,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,QAAgB;IAC1D,yEAAyE;IACzE,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,KAAK,MAAM;QAAE,OAAO,EAAE,CAAC;IAE1E,8CAA8C;IAC9C,IAAI,kBAAkB,CAAC,QAAQ,CAAC;QAAE,OAAO,EAAE,CAAC;IAE5C,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAC9D,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,qCAAqC,CAAC,CAAC;IAClE,IAAI,CAAC,QAAQ;QAAE,OAAO,EAAE,CAAC;IAEzB,IAAI,SAAS,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAClC,SAAS,GAAG,SAAS,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,CAAC,+BAA+B;IAClF,SAAS,GAAG,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAW,uBAAuB;IAE3E,OAAO,CAAC,GAAG,GAAG,SAAS,CAAC,CAAC;AAC7B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../src/core/scanner.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EACH,qBAAqB,EACrB,yBAAyB,EACzB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,QAAQ,GACX,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EAAE,SAAS,EAAE,MAAM,oCAAoC,CAAC;AAC/D,OAAO,EACH,kBAAkB,EAClB,eAAe,GAClB,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAG3E,OAAO,EAGH,KAAK,WAAW,EAChB,KAAK,YAAY,EAEpB,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../src/core/scanner.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EACH,qBAAqB,EACrB,yBAAyB,EACzB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,QAAQ,GACX,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EAAE,SAAS,EAAE,MAAM,oCAAoC,CAAC;AAC/D,OAAO,EACH,kBAAkB,EAClB,eAAe,GAClB,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAG3E,OAAO,EAGH,KAAK,WAAW,EAChB,KAAK,YAAY,EAEpB,MAAM,8BAA8B,CAAC;AAwCtC;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,YAAY,CAgH1E"}
|
package/dist/core/scanner.js
CHANGED
|
@@ -16,10 +16,19 @@ import { DEFAULT_EXCLUDED_DIRS, DEFAULT_SOURCE_EXTENSIONS, routeKey, } from './s
|
|
|
16
16
|
* Returns true for component files that require user interaction to appear
|
|
17
17
|
* (dialogs, modals, drawers, menus, …). Keys in these files are synced to the
|
|
18
18
|
* CMS but get no previewRoutes because simply navigating to a route won't show them.
|
|
19
|
+
*
|
|
20
|
+
* Matches any file whose name *contains* an overlay keyword (prefix, suffix, or middle),
|
|
21
|
+
* and any file inside a directory named after an overlay pattern (e.g. dialogs/, modals/).
|
|
19
22
|
*/
|
|
20
23
|
function isInteractiveOverlay(filePath) {
|
|
21
24
|
const lower = filePath.toLowerCase();
|
|
22
|
-
|
|
25
|
+
// File whose name contains an overlay keyword
|
|
26
|
+
if (/[\\/][^\\/]*(dialog|modal|drawer|sheet|popover|dropdown|menu|combobox|tooltip)[^\\/]*\.[jt]sx?$/.test(lower))
|
|
27
|
+
return true;
|
|
28
|
+
// File inside a directory named after an overlay pattern (dialogs/, modals/, menus/, …)
|
|
29
|
+
if (/[\\/](dialogs?|modals?|drawers?|sheets?|popovers?|dropdowns?|menus?|comboboxes?|tooltips?)[\\/]/.test(lower))
|
|
30
|
+
return true;
|
|
31
|
+
return false;
|
|
23
32
|
}
|
|
24
33
|
import { walkFiles } from './scanner-internals/file-walker.js';
|
|
25
34
|
import { filePathToRoute } from './scanner-internals/route-detector.js';
|
|
@@ -76,15 +85,21 @@ export function scanProject(root, opts) {
|
|
|
76
85
|
extractKeysFromFile(layout, routes, result);
|
|
77
86
|
}
|
|
78
87
|
}
|
|
79
|
-
|
|
88
|
+
// Queue carries the effective routes that each file was scanned with, so that
|
|
89
|
+
// sub-imports of overlays (dialogs, menus, …) inherit [] routes rather than
|
|
90
|
+
// the page routes from the outer loop.
|
|
91
|
+
const queue = [
|
|
92
|
+
{ file, currentRoutes: routes },
|
|
93
|
+
...layoutChain.map(l => ({ file: l, currentRoutes: routes })),
|
|
94
|
+
];
|
|
80
95
|
while (queue.length > 0) {
|
|
81
|
-
const current = queue.shift();
|
|
96
|
+
const { file: current, currentRoutes } = queue.shift();
|
|
82
97
|
const imports = extractLocalImports(current, sourceExtensions, projectRoot, pathMappings);
|
|
83
98
|
if (imports.length > 0) {
|
|
84
|
-
console.log(`[Pass 2] ${path.relative(root, current)} (${
|
|
99
|
+
console.log(`[Pass 2] ${path.relative(root, current)} (${currentRoutes.join(', ') || 'overlay'}) imports: ${imports.map(i => path.relative(root, i)).join(', ')}`);
|
|
85
100
|
}
|
|
86
101
|
for (const imported of imports) {
|
|
87
|
-
const key = `${imported}::${
|
|
102
|
+
const key = `${imported}::${currentRoutes.join('|')}`;
|
|
88
103
|
if (scannedWithRoute.has(key))
|
|
89
104
|
continue;
|
|
90
105
|
scannedWithRoute.add(key);
|
|
@@ -94,13 +109,15 @@ export function scanProject(root, opts) {
|
|
|
94
109
|
}
|
|
95
110
|
if (routeByFile.get(imported).length > 0)
|
|
96
111
|
continue;
|
|
97
|
-
// Don't propagate routes into components that need user interaction to appear
|
|
112
|
+
// Don't propagate routes into components that need user interaction to appear.
|
|
113
|
+
// Also: if the parent was already an overlay (currentRoutes = []), keep [] so
|
|
114
|
+
// that all transitive imports of an overlay stay route-free.
|
|
98
115
|
const effectiveRoutes = isInteractiveOverlay(imported)
|
|
99
116
|
? []
|
|
100
|
-
:
|
|
117
|
+
: currentRoutes;
|
|
101
118
|
console.log(`[Pass 2] Re-scanning ${path.relative(root, imported)} with routes: ${effectiveRoutes.join(', ') || '(none — interactive overlay)'}`);
|
|
102
119
|
extractKeysFromFile(imported, effectiveRoutes, result);
|
|
103
|
-
queue.push(imported);
|
|
120
|
+
queue.push({ file: imported, currentRoutes: effectiveRoutes });
|
|
104
121
|
}
|
|
105
122
|
}
|
|
106
123
|
}
|