fimo 0.7.0-experimental.10 → 0.7.0-experimental.11

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,7 +1,7 @@
1
1
  {
2
2
  "bundled": true,
3
3
  "bundler": "esbuild",
4
- "bundledAt": "2026-07-23T12:45:08.799Z",
4
+ "bundledAt": "2026-07-23T13:02:24.798Z",
5
5
  "cliVersion": "0.6.0",
6
6
  "external": [
7
7
  "oxc-parser",
package/dist/cli/index.js CHANGED
@@ -46647,6 +46647,60 @@ var init_staged_labels = __esm({
46647
46647
  });
46648
46648
 
46649
46649
  // src/scripts/i18n-namespace.ts
46650
+ function translationCallNamespace(node) {
46651
+ if (node?.type !== "CallExpression" || node.callee?.type !== "Identifier") {
46652
+ return null;
46653
+ }
46654
+ if (node.callee.name !== "useTranslations" && node.callee.name !== "getTranslations") {
46655
+ return null;
46656
+ }
46657
+ const arg = node.arguments?.[0];
46658
+ if (!arg) {
46659
+ return "";
46660
+ }
46661
+ if (arg.type === "Literal" && typeof arg.value === "string") {
46662
+ return arg.value;
46663
+ }
46664
+ if (arg.type === "ObjectExpression") {
46665
+ for (const prop2 of arg.properties ?? []) {
46666
+ const keyName = prop2?.key?.name ?? prop2?.key?.value;
46667
+ if (keyName === "namespace" && prop2.value?.type === "Literal" && typeof prop2.value.value === "string") {
46668
+ return prop2.value.value;
46669
+ }
46670
+ }
46671
+ }
46672
+ return "";
46673
+ }
46674
+ function markParamBindings(node, bindings) {
46675
+ if (!node) {
46676
+ return;
46677
+ }
46678
+ switch (node.type) {
46679
+ case "Identifier":
46680
+ if (!bindings.has(node.name)) {
46681
+ bindings.set(node.name, null);
46682
+ }
46683
+ break;
46684
+ case "AssignmentPattern":
46685
+ markParamBindings(node.left, bindings);
46686
+ break;
46687
+ case "RestElement":
46688
+ markParamBindings(node.argument, bindings);
46689
+ break;
46690
+ case "ArrayPattern":
46691
+ for (const element of node.elements ?? []) {
46692
+ markParamBindings(element, bindings);
46693
+ }
46694
+ break;
46695
+ case "ObjectPattern":
46696
+ for (const prop2 of node.properties ?? []) {
46697
+ markParamBindings(prop2.type === "RestElement" ? prop2.argument : prop2.value, bindings);
46698
+ }
46699
+ break;
46700
+ default:
46701
+ break;
46702
+ }
46703
+ }
46650
46704
  function collectScopedBindings(fnNode) {
46651
46705
  const bindings = /* @__PURE__ */ new Map();
46652
46706
  const body = fnNode.body;
@@ -46656,32 +46710,56 @@ function collectScopedBindings(fnNode) {
46656
46710
  continue;
46657
46711
  }
46658
46712
  for (const decl of stmt.declarations ?? []) {
46659
- if (decl?.id?.type !== "Identifier") {
46713
+ const init = unwrapAwait(decl?.init);
46714
+ if (decl?.id?.type === "Identifier") {
46715
+ const namespace = translationCallNamespace(init);
46716
+ if (namespace !== null) {
46717
+ bindings.set(decl.id.name, namespace);
46718
+ }
46660
46719
  continue;
46661
46720
  }
46662
- const init = decl.init?.type === "AwaitExpression" ? decl.init.argument : decl.init;
46663
- if (init?.type === "CallExpression" && init.callee?.type === "Identifier" && (init.callee.name === "useTranslations" || init.callee.name === "getTranslations")) {
46664
- const nsArg = init.arguments?.[0];
46665
- const namespace = nsArg?.type === "Literal" && typeof nsArg.value === "string" ? nsArg.value : "";
46666
- bindings.set(decl.id.name, namespace);
46721
+ if (decl?.id?.type === "ArrayPattern") {
46722
+ const source = init?.type === "CallExpression" && init.callee?.type === "Identifier" && init.callee.name === "use" ? unwrapAwait(init.arguments?.[0]) : init;
46723
+ const promiseAll = source?.type === "CallExpression" && source.callee?.type === "MemberExpression" && source.callee.object?.name === "Promise" && source.callee.property?.name === "all";
46724
+ const elements = promiseAll ? source.arguments?.[0]?.type === "ArrayExpression" ? source.arguments[0].elements : null : source?.type === "ArrayExpression" ? source.elements : null;
46725
+ if (elements) {
46726
+ decl.id.elements?.forEach((el, index2) => {
46727
+ if (el?.type !== "Identifier") {
46728
+ return;
46729
+ }
46730
+ const namespace = translationCallNamespace(unwrapAwait(elements[index2]));
46731
+ if (namespace !== null) {
46732
+ bindings.set(el.name, namespace);
46733
+ }
46734
+ });
46735
+ }
46667
46736
  }
46668
46737
  }
46669
46738
  }
46739
+ for (const param of fnNode.params ?? []) {
46740
+ markParamBindings(param, bindings);
46741
+ }
46670
46742
  return bindings;
46671
46743
  }
46672
46744
  function resolveScopedKey(calleeName, leafKey, scopes) {
46673
46745
  for (let i = scopes.length - 1; i >= 0; i -= 1) {
46674
46746
  if (scopes[i].has(calleeName)) {
46675
46747
  const namespace = scopes[i].get(calleeName);
46676
- return { key: namespace ? `${namespace}.${leafKey}` : leafKey, scoped: true };
46748
+ if (namespace === null || namespace === void 0) {
46749
+ return { key: leafKey, scoped: false, unresolvable: true };
46750
+ }
46751
+ return { key: namespace ? `${namespace}.${leafKey}` : leafKey, scoped: true, unresolvable: false };
46677
46752
  }
46678
46753
  }
46679
- return { key: leafKey, scoped: false };
46754
+ return { key: leafKey, scoped: false, unresolvable: false };
46680
46755
  }
46681
- var isFunctionLike;
46756
+ var isFunctionLike, unwrapAwait;
46682
46757
  var init_i18n_namespace = __esm({
46683
46758
  "src/scripts/i18n-namespace.ts"() {
46684
46759
  isFunctionLike = /* @__PURE__ */ __name((node) => node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression", "isFunctionLike");
46760
+ unwrapAwait = /* @__PURE__ */ __name((node) => node?.type === "AwaitExpression" ? node.argument : node, "unwrapAwait");
46761
+ __name(translationCallNamespace, "translationCallNamespace");
46762
+ __name(markParamBindings, "markParamBindings");
46685
46763
  __name(collectScopedBindings, "collectScopedBindings");
46686
46764
  __name(resolveScopedKey, "resolveScopedKey");
46687
46765
  }
@@ -49534,7 +49612,10 @@ function scanWrappableFile(filePath, code2, relPath, opts = {}) {
49534
49612
  wrapped += 1;
49535
49613
  const firstArg = node.arguments?.[0];
49536
49614
  if (firstArg?.type === "Literal" && typeof firstArg.value === "string") {
49537
- wrappedIds.labels.push(resolveScopedKey(node.callee.name, firstArg.value, childScopes).key);
49615
+ const resolved = resolveScopedKey(node.callee.name, firstArg.value, childScopes);
49616
+ if (!resolved.unresolvable) {
49617
+ wrappedIds.labels.push(resolved.key);
49618
+ }
49538
49619
  }
49539
49620
  }
49540
49621
  }
@@ -106835,8 +106916,8 @@ function extractFromFile(filePath) {
106835
106916
  const name = node.callee.name;
106836
106917
  const firstArg = (node.arguments ?? [])[0];
106837
106918
  if (firstArg?.type === "Literal" && typeof firstArg.value === "string") {
106838
- const { key, scoped } = resolveScopedKey(name, firstArg.value, childScopes);
106839
- if (scoped || name === "t") {
106919
+ const { key, scoped, unresolvable } = resolveScopedKey(name, firstArg.value, childScopes);
106920
+ if (!unresolvable && (scoped || name === "t")) {
106840
106921
  declarations.push({ key, file: filePath });
106841
106922
  }
106842
106923
  }
@@ -46,10 +46,10 @@ function extractFromFile(filePath) {
46
46
  const name = node.callee.name;
47
47
  const firstArg = (node.arguments ?? [])[0];
48
48
  if (firstArg?.type === 'Literal' && typeof firstArg.value === 'string') {
49
- const { key, scoped } = resolveScopedKey(name, firstArg.value, childScopes);
50
- // A namespace-scoped var (any name) OR a bare Fimo `t(...)` — record the
51
- // resolved key (scoped prepends the namespace; unscoped `t` is the leaf).
52
- if (scoped || name === 't') {
49
+ const { key, scoped, unresolvable } = resolveScopedKey(name, firstArg.value, childScopes);
50
+ // A namespace-scoped var (any name) OR a bare Fimo `t(...)`. A passed-in
51
+ // param `t` is unresolvable (the caller owns the namespace) — skip it.
52
+ if (!unresolvable && (scoped || name === 't')) {
53
53
  declarations.push({ key, file: filePath });
54
54
  }
55
55
  }
@@ -63,4 +63,42 @@ export default async function Page() {
63
63
  });
64
64
  expect(keysOf(cwd)).toEqual(['home.title']);
65
65
  });
66
+ it('resolves the object-arg namespace: getTranslations({ namespace })', () => {
67
+ const cwd = project({
68
+ 'src/meta.tsx': `import { getTranslations } from 'next-intl/server';
69
+ export async function generateMetadata({ params }) {
70
+ const t = await getTranslations({ locale: params.locale, namespace: 'seo' });
71
+ return { title: t('metaTitle'), keywords: t('keywords') };
72
+ }`,
73
+ });
74
+ expect(keysOf(cwd)).toEqual(['seo.keywords', 'seo.metaTitle']);
75
+ });
76
+ it('resolves a t destructured from Promise.all (with or without a use() wrapper)', () => {
77
+ const cwd = project({
78
+ 'src/a.tsx': `import { getTranslations } from 'next-intl/server';
79
+ export async function A() {
80
+ const [data, t] = await Promise.all([fetchData(), getTranslations('cms')]);
81
+ return <p>{t('compare')}</p>;
82
+ }`,
83
+ 'src/b.tsx': `import { use } from 'react';
84
+ import { getTranslations } from 'next-intl/server';
85
+ export function B() {
86
+ const [data, t] = use(Promise.all([fetchData(), getTranslations('cms')]));
87
+ return <p>{t('disclaimer')}</p>;
88
+ }`,
89
+ });
90
+ expect(keysOf(cwd)).toEqual(['cms.compare', 'cms.disclaimer']);
91
+ });
92
+ it('skips a t passed in as a parameter — plain or destructured (caller owns the namespace)', () => {
93
+ const cwd = project({
94
+ 'src/defaults.tsx': `export function getDefaults(siteUrl, t) {
95
+ return { title: t('metaTitle'), keywords: t('keywords') };
96
+ }`,
97
+ 'src/grid.tsx': `export function Grid({ t, items }) {
98
+ return <button>{t('loadMore')}</button>;
99
+ }`,
100
+ });
101
+ // Unresolvable param keys are dropped, not recorded as bare leaves.
102
+ expect(keysOf(cwd)).toEqual([]);
103
+ });
66
104
  });
@@ -10,16 +10,18 @@
10
10
  * with no binding (Fimo's own label call) keeps its key unchanged.
11
11
  */
12
12
  export declare const isFunctionLike: (node: any) => boolean;
13
- /** Collect `useTranslations`/`getTranslations` bindings declared in a function's
14
- * direct body: variable name → namespace (`""` for root scope). */
15
- export declare function collectScopedBindings(fnNode: any): Map<string, string>;
13
+ export declare function collectScopedBindings(fnNode: any): Map<string, string | null>;
16
14
  /**
17
15
  * Resolve a `<name>("leaf")` call to its full key given the scope stack
18
- * (innermost first). Returns `{ key, scoped }`: `scoped` is true when the callee
19
- * is a resolved `useTranslations` binding (so the caller can decide whether an
20
- * unbound name still counts). The namespace is prepended unless it is root (`""`).
16
+ * (innermost first).
17
+ * - `scoped`: the callee is a resolved `useTranslations` binding — namespace
18
+ * prepended (unless root `""`).
19
+ * - `unresolvable`: the callee is a passed-in parameter — its namespace lives in
20
+ * the caller and cannot be known here, so the caller should count the call as
21
+ * covered but NOT demand the (bare) key.
21
22
  */
22
- export declare function resolveScopedKey(calleeName: string, leafKey: string, scopes: Array<Map<string, string>>): {
23
+ export declare function resolveScopedKey(calleeName: string, leafKey: string, scopes: Array<Map<string, string | null>>): {
23
24
  key: string;
24
25
  scoped: boolean;
26
+ unresolvable: boolean;
25
27
  };
@@ -12,6 +12,69 @@
12
12
  export const isFunctionLike = (node) => node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression';
13
13
  /** Collect `useTranslations`/`getTranslations` bindings declared in a function's
14
14
  * direct body: variable name → namespace (`""` for root scope). */
15
+ const unwrapAwait = (node) => (node?.type === 'AwaitExpression' ? node.argument : node);
16
+ /**
17
+ * The namespace of a `useTranslations`/`getTranslations` call, or null if the
18
+ * node is not such a call. Handles the string arg (`getTranslations("seo")`),
19
+ * the object arg (`getTranslations({ locale, namespace: "seo" })`), and no arg
20
+ * (root scope → `""`). A dynamic namespace resolves to `""` (best effort).
21
+ */
22
+ function translationCallNamespace(node) {
23
+ if (node?.type !== 'CallExpression' || node.callee?.type !== 'Identifier') {
24
+ return null;
25
+ }
26
+ if (node.callee.name !== 'useTranslations' && node.callee.name !== 'getTranslations') {
27
+ return null;
28
+ }
29
+ const arg = node.arguments?.[0];
30
+ if (!arg) {
31
+ return '';
32
+ }
33
+ if (arg.type === 'Literal' && typeof arg.value === 'string') {
34
+ return arg.value;
35
+ }
36
+ if (arg.type === 'ObjectExpression') {
37
+ for (const prop of arg.properties ?? []) {
38
+ const keyName = prop?.key?.name ?? prop?.key?.value;
39
+ if (keyName === 'namespace' && prop.value?.type === 'Literal' && typeof prop.value.value === 'string') {
40
+ return prop.value.value;
41
+ }
42
+ }
43
+ }
44
+ return '';
45
+ }
46
+ /** Mark every identifier bound by a parameter pattern (plain, defaulted, or
47
+ * destructured — `t`, `t = x`, `{ t }`, `[t]`) as unresolvable (null). */
48
+ function markParamBindings(node, bindings) {
49
+ if (!node) {
50
+ return;
51
+ }
52
+ switch (node.type) {
53
+ case 'Identifier':
54
+ if (!bindings.has(node.name)) {
55
+ bindings.set(node.name, null);
56
+ }
57
+ break;
58
+ case 'AssignmentPattern':
59
+ markParamBindings(node.left, bindings);
60
+ break;
61
+ case 'RestElement':
62
+ markParamBindings(node.argument, bindings);
63
+ break;
64
+ case 'ArrayPattern':
65
+ for (const element of node.elements ?? []) {
66
+ markParamBindings(element, bindings);
67
+ }
68
+ break;
69
+ case 'ObjectPattern':
70
+ for (const prop of node.properties ?? []) {
71
+ markParamBindings(prop.type === 'RestElement' ? prop.argument : prop.value, bindings);
72
+ }
73
+ break;
74
+ default:
75
+ break;
76
+ }
77
+ }
15
78
  export function collectScopedBindings(fnNode) {
16
79
  const bindings = new Map();
17
80
  const body = fnNode.body;
@@ -21,33 +84,72 @@ export function collectScopedBindings(fnNode) {
21
84
  continue;
22
85
  }
23
86
  for (const decl of stmt.declarations ?? []) {
24
- if (decl?.id?.type !== 'Identifier') {
87
+ const init = unwrapAwait(decl?.init);
88
+ // `const t = [await] useTranslations/getTranslations(...)`
89
+ if (decl?.id?.type === 'Identifier') {
90
+ const namespace = translationCallNamespace(init);
91
+ if (namespace !== null) {
92
+ bindings.set(decl.id.name, namespace);
93
+ }
25
94
  continue;
26
95
  }
27
- const init = decl.init?.type === 'AwaitExpression' ? decl.init.argument : decl.init;
28
- if (init?.type === 'CallExpression' &&
29
- init.callee?.type === 'Identifier' &&
30
- (init.callee.name === 'useTranslations' || init.callee.name === 'getTranslations')) {
31
- const nsArg = init.arguments?.[0];
32
- const namespace = nsArg?.type === 'Literal' && typeof nsArg.value === 'string' ? nsArg.value : '';
33
- bindings.set(decl.id.name, namespace);
96
+ // `const [a, t] = await Promise.all([...])` or `= use(Promise.all([...]))`
97
+ if (decl?.id?.type === 'ArrayPattern') {
98
+ // Unwrap React's `use()` wrapper around the promise.
99
+ const source = init?.type === 'CallExpression' && init.callee?.type === 'Identifier' && init.callee.name === 'use'
100
+ ? unwrapAwait(init.arguments?.[0])
101
+ : init;
102
+ const promiseAll = source?.type === 'CallExpression' &&
103
+ source.callee?.type === 'MemberExpression' &&
104
+ source.callee.object?.name === 'Promise' &&
105
+ source.callee.property?.name === 'all';
106
+ const elements = promiseAll
107
+ ? source.arguments?.[0]?.type === 'ArrayExpression'
108
+ ? source.arguments[0].elements
109
+ : null
110
+ : source?.type === 'ArrayExpression'
111
+ ? source.elements
112
+ : null;
113
+ if (elements) {
114
+ decl.id.elements?.forEach((el, index) => {
115
+ if (el?.type !== 'Identifier') {
116
+ return;
117
+ }
118
+ const namespace = translationCallNamespace(unwrapAwait(elements[index]));
119
+ if (namespace !== null) {
120
+ bindings.set(el.name, namespace);
121
+ }
122
+ });
123
+ }
34
124
  }
35
125
  }
36
126
  }
127
+ // Function parameters are bound by the CALLER: a `t` passed in carries the
128
+ // caller's namespace, unknowable here. Mark them null so their t() calls are
129
+ // counted as covered but never demanded by the seeding gate as a bare leaf.
130
+ for (const param of fnNode.params ?? []) {
131
+ markParamBindings(param, bindings);
132
+ }
37
133
  return bindings;
38
134
  }
39
135
  /**
40
136
  * Resolve a `<name>("leaf")` call to its full key given the scope stack
41
- * (innermost first). Returns `{ key, scoped }`: `scoped` is true when the callee
42
- * is a resolved `useTranslations` binding (so the caller can decide whether an
43
- * unbound name still counts). The namespace is prepended unless it is root (`""`).
137
+ * (innermost first).
138
+ * - `scoped`: the callee is a resolved `useTranslations` binding — namespace
139
+ * prepended (unless root `""`).
140
+ * - `unresolvable`: the callee is a passed-in parameter — its namespace lives in
141
+ * the caller and cannot be known here, so the caller should count the call as
142
+ * covered but NOT demand the (bare) key.
44
143
  */
45
144
  export function resolveScopedKey(calleeName, leafKey, scopes) {
46
145
  for (let i = scopes.length - 1; i >= 0; i -= 1) {
47
146
  if (scopes[i].has(calleeName)) {
48
147
  const namespace = scopes[i].get(calleeName);
49
- return { key: namespace ? `${namespace}.${leafKey}` : leafKey, scoped: true };
148
+ if (namespace === null || namespace === undefined) {
149
+ return { key: leafKey, scoped: false, unresolvable: true };
150
+ }
151
+ return { key: namespace ? `${namespace}.${leafKey}` : leafKey, scoped: true, unresolvable: false };
50
152
  }
51
153
  }
52
- return { key: leafKey, scoped: false };
154
+ return { key: leafKey, scoped: false, unresolvable: false };
53
155
  }
@@ -578,8 +578,13 @@ export function scanWrappableFile(filePath, code, relPath, opts = {}) {
578
578
  const firstArg = node.arguments?.[0];
579
579
  if (firstArg?.type === 'Literal' && typeof firstArg.value === 'string') {
580
580
  // Resolve the next-intl namespace so the recorded key matches the
581
- // ingested (namespaced) label in the DB, not the bare leaf.
582
- wrappedIds.labels.push(resolveScopedKey(node.callee.name, firstArg.value, childScopes).key);
581
+ // ingested (namespaced) label in the DB. A passed-in param `t` is
582
+ // unresolvable (the caller owns the namespace) — the call still counts
583
+ // as wrapped, but we do not demand its bare key from the seeding gate.
584
+ const resolved = resolveScopedKey(node.callee.name, firstArg.value, childScopes);
585
+ if (!resolved.unresolvable) {
586
+ wrappedIds.labels.push(resolved.key);
587
+ }
583
588
  }
584
589
  }
585
590
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fimo",
3
- "version": "0.7.0-experimental.10",
3
+ "version": "0.7.0-experimental.11",
4
4
  "description": "Fimo CLI - create, deploy, and manage Fimo projects",
5
5
  "bin": {
6
6
  "fimo": "dist/cli/index.js"
@@ -21,7 +21,7 @@
21
21
  "cmdk": "^1.1.1",
22
22
  "date-fns": "^4.1.0",
23
23
  "embla-carousel-react": "^8.6.0",
24
- "fimo": "0.7.0-experimental.10",
24
+ "fimo": "0.7.0-experimental.11",
25
25
  "input-otp": "^1.4.2",
26
26
  "isbot": "^5",
27
27
  "lucide-react": "^0.577.0",