From 13c449fde2b5fa8c0bb1a55f83fcdd1107c833d1 Mon Sep 17 00:00:00 2001 From: cpojer Date: Sun, 22 Dec 2024 11:45:12 +0900 Subject: [PATCH] Add ``. --- .eslintrc.cjs | 9 +- .gitignore | 6 +- .prettierignore | 1 + README.md | 2 +- example/src/example/Example.react.tsx | 24 ++- .../__tests__/CollectedString-test.tsx | 27 +++ .../__snapshots__/Example.react-test.tsx.snap | 5 + package.json | 8 +- .../babel-plugin-fbtee/src/FbtConstants.tsx | 5 +- .../babel-plugin-fbtee/src/FbtNodeChecker.tsx | 4 - packages/babel-plugin-fbtee/src/FbtUtil.tsx | 23 ++- .../__snapshots__/fbt-list-test.tsx.snap | 76 +++++++ .../__snapshots__/fbtEnum-test.tsx.snap | 4 +- .../src/__tests__/fbt-list-test.tsx | 64 ++++++ .../src/__tests__/fbtEnum-test.tsx | 58 +++--- .../src/__tests__/fbtFunctional-test.tsx | 13 +- .../src/__tests__/fbtInnerOuter-test.tsx | 42 ++-- .../src/__tests__/jsx-test.tsx | 4 +- .../FbtFunctionCallProcessor.tsx | 32 ++- .../src/babel-processors/JSXFbtProcessor.tsx | 65 +++--- packages/babel-plugin-fbtee/src/bin.tsx | 2 +- .../src/bin/FbtCollector.tsx | 42 ++-- .../src/bin/__mocks__/CustomFbtCollector.tsx | 5 +- .../src/bin/__tests__/translate-test.tsx | 9 + .../babel-plugin-fbtee/src/bin/collect.tsx | 158 ++++++++------- .../src/bin/collectFbtUtils.tsx | 4 +- .../babel-plugin-fbtee/src/bin/translate.tsx | 2 +- .../src/bin/translateUtils.tsx | 2 +- .../src/fbt-nodes/FbtElementNode.tsx | 16 +- .../src/fbt-nodes/FbtEnumNode.tsx | 2 +- .../src/fbt-nodes/FbtImplicitParamNode.tsx | 2 +- .../src/fbt-nodes/FbtListNode.tsx | 90 +++++++++ .../src/fbt-nodes/FbtNode.tsx | 13 +- .../src/fbt-nodes/FbtNodeType.tsx | 2 + .../src/fbt-nodes/FbtNodeUtil.tsx | 4 +- .../src/fbt-nodes/FbtPluralNode.tsx | 2 +- .../src/fbt-nodes/FbtSameParamNode.tsx | 7 +- .../src/getNamespacedArgs.tsx | 142 +++++++------- packages/babel-plugin-fbtee/src/index.tsx | 19 +- packages/babel-preset-fbtee/src/bin.tsx | 2 +- packages/fbtee/.npmignore | 5 + packages/fbtee/ReactTypes.d.ts | 22 ++- packages/fbtee/babel-build.config.js | 11 ++ packages/fbtee/package.json | 6 +- packages/fbtee/scripts/rewrite-filepaths.ts | 12 ++ packages/fbtee/src/Hooks.tsx | 2 +- packages/fbtee/src/Types.d.ts | 11 +- ...ntlViewerContext.tsx => ViewerContext.tsx} | 0 packages/fbtee/src/__mocks__/getFbtResult.tsx | 2 +- .../__snapshots__/list-test.tsx.snap | 5 + packages/fbtee/src/__tests__/fbs-test.tsx | 2 +- .../fbtee/src/__tests__/fbt-runtime-test.tsx | 2 +- .../fbtee/src/__tests__/intlList-test.tsx | 66 ------- .../fbtee/src/__tests__/intlNumUtils-test.tsx | 14 -- packages/fbtee/src/__tests__/list-test.tsx | 80 ++++++++ .../fbtee/src/__tests__/mock-fbt-test.tsx | 2 +- packages/fbtee/src/fbt.tsx | 21 +- packages/fbtee/src/formatNumber.tsx | 5 + packages/fbtee/src/index.tsx | 2 +- packages/fbtee/src/intlList.tsx | 185 ------------------ packages/fbtee/src/list.tsx | 134 +++++++++++++ packages/fbtee/src/setupFbtee.tsx | 2 +- pnpm-lock.yaml | 91 +++++++++ tsconfig.json | 2 +- 64 files changed, 1030 insertions(+), 651 deletions(-) create mode 100644 example/src/example/__tests__/CollectedString-test.tsx create mode 100644 packages/babel-plugin-fbtee/src/__tests__/__snapshots__/fbt-list-test.tsx.snap create mode 100644 packages/babel-plugin-fbtee/src/__tests__/fbt-list-test.tsx create mode 100644 packages/babel-plugin-fbtee/src/fbt-nodes/FbtListNode.tsx create mode 100644 packages/fbtee/babel-build.config.js create mode 100755 packages/fbtee/scripts/rewrite-filepaths.ts rename packages/fbtee/src/{IntlViewerContext.tsx => ViewerContext.tsx} (100%) create mode 100644 packages/fbtee/src/__tests__/__snapshots__/list-test.tsx.snap delete mode 100644 packages/fbtee/src/__tests__/intlList-test.tsx create mode 100644 packages/fbtee/src/__tests__/list-test.tsx delete mode 100644 packages/fbtee/src/intlList.tsx create mode 100644 packages/fbtee/src/list.tsx diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 4191ebab..a31de399 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -3,10 +3,14 @@ const { existsSync, readFileSync } = require('node:fs'); module.exports = { extends: ['@nkzw'], - ignorePatterns: ['packages/*/lib'], + ignorePatterns: ['packages/*/lib', 'packages/fbtee/lib-tmp/'], overrides: [ { - files: ['**/__tests__/**/*.tsx'], + files: [ + './packages/babel-plugin-fbtee/src/bin/*.tsx', + './packages/fbtee/babel-build.config.js', + '**/__tests__/**/*.tsx', + ], rules: { 'no-console': 0, 'workspaces/no-relative-imports': 0, @@ -29,6 +33,7 @@ module.exports = { devDependencies: [ './example/vite.config.ts', './jest-preprocessor.js', + './packages/fbtee/babel-build.config.js', '**/__tests__/**/*.tsx', ], packageDir: [__dirname].concat( diff --git a/.gitignore b/.gitignore index ab6d2e11..7884dc72 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ node_modules packages/*/lib packages/*/LICENSE packages/*/README.md -packages/fbt/lib -packages/fbt/LICENSE +packages/fbtee/.enum_manifest.json +packages/fbtee/.src_manifest.json +packages/fbtee/lib-tmp +packages/fbtee/Strings.json tsconfig.tsbuildinfo diff --git a/.prettierignore b/.prettierignore index dd4c942f..b21036e4 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,4 +5,5 @@ example/source_strings.json example/src/translatedFbts example/src/translatedFbts.json packages/*/lib/ +packages/fbtee/lib-tmp/ pnpm-lock.yaml diff --git a/README.md b/README.md index 88b14919..3bf639e0 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ The open-source version of `fbt`, however, became unmaintained, difficult to set - **Easier Setup:** fbtee works with modern tools like Vite. - **Statically Typed:** The fbtee compiler ensures correct usage of fbtee, libary TypeScript types are provided, and an eslint plugin helps fix common mistakes. - **Improved React Compatibility:** Removed React-specific hacks and added support for implicit React fragments (`<>`). -- **Enhanced Features:** Fixed and exported `intlList`, which was not functional in the original `fbt`. +- **Enhanced Features:** Fixed and exported `inltList` as a new `` construt, which was not functional in the original `fbt`. - **Modernized Codebase:** Rewritten using TypeScript, ES modules (ESM), eslint, and modern JavaScript standards. Removed cruft and legacy code. - **Updated Tooling:** Uses modern tools like pnpm, Vite, and esbuild for faster and more efficient development of **fbtee**. diff --git a/example/src/example/Example.react.tsx b/example/src/example/Example.react.tsx index 1ee4f9e6..d63085d4 100644 --- a/example/src/example/Example.react.tsx +++ b/example/src/example/Example.react.tsx @@ -185,7 +185,6 @@ export default function Example() { -
@@ -202,7 +201,6 @@ export default function Example() {
-
+
+ +
+
+ +
; - export type CallExpressionArg = | Expression | SpreadElement @@ -130,6 +127,10 @@ export function checkOption( ): K { const optionName = option as K; + if (optionName === 'key') { + return optionName; + } + const validValues = validOptions[optionName]; if (!hasOwnProperty.call(validOptions, optionName) || validValues == null) { throw errorAt( @@ -506,27 +507,29 @@ const isJSXAttributeWithValue = ( ): node is JSXAttributeWithValue => node.value != null; export function getAttributeByNameOrThrow( - attributes: JSXAttributes, + node: JSXElement, name: string, - node: Node | null = null, ): JSXAttributeWithValue { - const attribute = getAttributeByName(attributes, name); + const attribute = getAttributeByName(node, name); if (attribute == null) { - throw errorAt(node, `Unable to find attribute "${name}".`); + throw errorAt(node, `This node requires a '${name}' attribute.`); } if (!isJSXAttributeWithValue(attribute)) { - throw errorAt(node, `Attribute "${name}" has no value.`); + throw errorAt( + node, + `This '${name}' attribute of this node requires a value.`, + ); } return attribute; } export function getAttributeByName( - attributes: JSXAttributes, + node: JSXElement, name: string, ): JSXAttribute | null { - for (const attribute of attributes) { + for (const attribute of node.openingElement.attributes) { if (isJSXAttribute(attribute) && attribute.name.name === name) { return attribute; } diff --git a/packages/babel-plugin-fbtee/src/__tests__/__snapshots__/fbt-list-test.tsx.snap b/packages/babel-plugin-fbtee/src/__tests__/__snapshots__/fbt-list-test.tsx.snap new file mode 100644 index 00000000..c3b55ff0 --- /dev/null +++ b/packages/babel-plugin-fbtee/src/__tests__/__snapshots__/fbt-list-test.tsx.snap @@ -0,0 +1,76 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` 1`] = ` +import { fbt } from "fbtee"; +const x = fbt._( + /* __FBT__ start */ { + jsfbt: { + m: [], + t: { desc: "Lists", text: "Available Locations: {locations}." }, + }, + project: "", + } /* __FBT__ end */, + [fbt._list("locations", ["Tokyo", "London", "Vienna"])] +); + +`; + +exports[` 2`] = ` +import { fbt } from "fbtee"; +const x = fbt._( + /* __FBT__ start */ { + jsfbt: { + m: [], + t: { desc: "Lists", text: "Available Locations: {locations}." }, + }, + project: "", + } /* __FBT__ end */, + [fbt._list("locations", ["Tokyo", "London", "Vienna"], "and")] +); + +`; + +exports[` 3`] = ` +import { fbt } from "fbtee"; +const x = fbt._( + /* __FBT__ start */ { + jsfbt: { + m: [], + t: { desc: "Lists", text: "Available Locations: {locations}." }, + }, + project: "", + } /* __FBT__ end */, + [fbt._list("locations", ["Tokyo", "London", "Vienna"], null, "bullet")] +); + +`; + +exports[` 4`] = ` +import { fbt } from "fbtee"; +const x = fbt._( + /* __FBT__ start */ { + jsfbt: { + m: [], + t: { desc: "Lists", text: "Available Locations: {locations}." }, + }, + project: "", + } /* __FBT__ end */, + [fbt._list("locations", ["Tokyo", "London", "Vienna"], "or", "bullet")] +); + +`; + +exports[`fbt.list() 1`] = ` +import { fbt } from "fbtee"; +fbt._( + /* __FBT__ start */ { + jsfbt: { + m: [], + t: { desc: "Lists", text: "Available Locations: {locations}" }, + }, + project: "", + } /* __FBT__ end */, + [fbt._list("locations", ["Tokyo", "London", "Vienna"], null, "or")] +); + +`; diff --git a/packages/babel-plugin-fbtee/src/__tests__/__snapshots__/fbtEnum-test.tsx.snap b/packages/babel-plugin-fbtee/src/__tests__/__snapshots__/fbtEnum-test.tsx.snap index 1ea9ba45..5f722d4e 100644 --- a/packages/babel-plugin-fbtee/src/__tests__/__snapshots__/fbtEnum-test.tsx.snap +++ b/packages/babel-plugin-fbtee/src/__tests__/__snapshots__/fbtEnum-test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Test Fbt Enum should handle functional enums (with references) (import default) 1`] = ` -import { fbt } from "fbt"; +import { fbt } from "fbtee"; import aEnum from "Test$FbtEnum"; var x = fbt._( /* __FBT__ start */ { @@ -21,7 +21,7 @@ var x = fbt._( `; exports[`Test Fbt Enum should handle functional enums (with references) (import star) 1`] = ` -import { fbt } from "fbt"; +import { fbt } from "fbtee"; import * as aEnum from "Test$FbtEnum"; var x = fbt._( /* __FBT__ start */ { diff --git a/packages/babel-plugin-fbtee/src/__tests__/fbt-list-test.tsx b/packages/babel-plugin-fbtee/src/__tests__/fbt-list-test.tsx new file mode 100644 index 00000000..bb482369 --- /dev/null +++ b/packages/babel-plugin-fbtee/src/__tests__/fbt-list-test.tsx @@ -0,0 +1,64 @@ +import { expect, test } from '@jest/globals'; +import { + jsCodeFbtCallSerializer, + snapshotTransform, + withFbtImportStatement, +} from './FbtTestUtil.tsx'; + +expect.addSnapshotSerializer(jsCodeFbtCallSerializer); + +const transform = (input: string) => + snapshotTransform(withFbtImportStatement(input)); + +test('fbt.list()', () => { + expect( + transform( + `fbt( + 'Available Locations: ' + fbt.list('locations', ['Tokyo', 'London', 'Vienna'], null, 'or'), + 'Lists', + )`, + ), + ).toMatchSnapshot(); +}); + +test('', () => { + expect( + transform( + `const x = ( + + Available Locations: . + + );`, + ), + ).toMatchSnapshot(); + + expect( + transform( + `const x = ( + + Available Locations: . + + );`, + ), + ).toMatchSnapshot(); + + expect( + transform( + `const x = ( + + Available Locations: . + + );`, + ), + ).toMatchSnapshot(); + + expect( + transform( + `const x = ( + + Available Locations: . + + );`, + ), + ).toMatchSnapshot(); +}); diff --git a/packages/babel-plugin-fbtee/src/__tests__/fbtEnum-test.tsx b/packages/babel-plugin-fbtee/src/__tests__/fbtEnum-test.tsx index 6744b4fc..e34ed6dd 100644 --- a/packages/babel-plugin-fbtee/src/__tests__/fbtEnum-test.tsx +++ b/packages/babel-plugin-fbtee/src/__tests__/fbtEnum-test.tsx @@ -1,4 +1,4 @@ -import { describe, expect, it, jest } from '@jest/globals'; +import { describe, expect, it } from '@jest/globals'; import TestFbtEnumManifest from '../__mocks__/TestFbtEnumManifest.tsx'; import { jsCodeFbtCallSerializer, @@ -8,21 +8,15 @@ import { expect.addSnapshotSerializer(jsCodeFbtCallSerializer); -function runTest(data: { input: string; throws?: string }) { - expect( - snapshotTransform(data.input, { fbtEnumManifest: TestFbtEnumManifest }), - ).toMatchSnapshot(); -} - -describe('Test Fbt Enum', () => { - beforeEach(() => { - // Ensure the Enum registrar config is reset. - jest.resetModules(); +const transform = (input: string) => + snapshotTransform(withFbtImportStatement(input), { + fbtEnumManifest: TestFbtEnumManifest, }); +describe('Test Fbt Enum', () => { it('should handle jsx enums (with references)', () => { - runTest({ - input: withFbtImportStatement( + expect( + transform( `import aEnum from 'Test$FbtEnum'; var x = ( @@ -31,12 +25,12 @@ describe('Test Fbt Enum', () => { );`, ), - }); + ).toMatchSnapshot(); }); it('should handle jsx string literals', () => { - runTest({ - input: withFbtImportStatement( + expect( + transform( `import aEnum from 'Test$FbtEnum'; var x = ( @@ -45,45 +39,43 @@ describe('Test Fbt Enum', () => { );`, ), - }); + ).toMatchSnapshot(); }); it('should handle functional enums (with references) (require)', () => { - runTest({ - input: withFbtImportStatement( + expect( + transform( `import aEnum from 'Test$FbtEnum'; var x = fbt('Click to see ' + fbt.enum(id, aEnum), 'enums!');`, ), - }); + ).toMatchSnapshot(); }); it('should handle functional enums (with references) (import default)', () => { - runTest({ - input: ` - import { fbt } from 'fbt'; + expect( + transform(` import aEnum from 'Test$FbtEnum'; var x = fbt('Click to see ' + fbt.enum(id, aEnum), 'enums!'); - `, - }); + `), + ).toMatchSnapshot(); }); it('should handle functional enums (with references) (import star)', () => { - runTest({ - input: ` - import { fbt } from 'fbt'; + expect( + transform(` import * as aEnum from 'Test$FbtEnum'; var x = fbt('Click to see ' + fbt.enum(id, aEnum), 'enums!'); - `, - }); + `), + ).toMatchSnapshot(); }); it('should handle functional enums (with references) in templates', () => { - runTest({ - input: withFbtImportStatement( + expect( + transform( `import aEnum from 'Test$FbtEnum'; var x = fbt(\`Click to see \${fbt.enum(id, aEnum)}\`, 'enums!');`, ), - }); + ).toMatchSnapshot(); }); it('should throw when enum values are not strings', () => { diff --git a/packages/babel-plugin-fbtee/src/__tests__/fbtFunctional-test.tsx b/packages/babel-plugin-fbtee/src/__tests__/fbtFunctional-test.tsx index e0eb5bb7..4bc17f48 100644 --- a/packages/babel-plugin-fbtee/src/__tests__/fbtFunctional-test.tsx +++ b/packages/babel-plugin-fbtee/src/__tests__/fbtFunctional-test.tsx @@ -2874,9 +2874,7 @@ with some other stuff.\` );`, ), - throws: - `Expected fbt constructs to not nest inside fbt constructs, ` + - `but found fbt.param nest inside fbt.name`, + throws: `'fbt' constructs should not be nested inside of other fbt constructs. Found 'fbt.param' nested inside 'fbt.name'.`, }, 'should throw when a fbt.param is nested inside another fbt.param': { @@ -2899,18 +2897,13 @@ with some other stuff.\` );`, ), - throws: - `Expected fbt constructs to not nest inside fbt constructs, ` + - `but found fbt.param nest inside fbt.param`, + throws: `'fbt' constructs should not be nested inside of other fbt constructs. Found 'fbt.param' nested inside 'fbt.param'.`, }, 'should throw when a fbt.param is used outside of fbt': { input: withFbtImportStatement(`var z = fbt.param('name', val);`), - throws: - `Fbt constructs can only be used within the scope of an fbt` + - ` string. I.e. It should be used directly inside an ` + - `‹fbt› / ‹fbs› callsite`, + throws: `fbt constructs must be used within the scope of other fbt constructs.`, }, 'should throw when concatenating an fbt construct to a string while using the array argument syntax': diff --git a/packages/babel-plugin-fbtee/src/__tests__/fbtInnerOuter-test.tsx b/packages/babel-plugin-fbtee/src/__tests__/fbtInnerOuter-test.tsx index b1e60f74..430c885c 100644 --- a/packages/babel-plugin-fbtee/src/__tests__/fbtInnerOuter-test.tsx +++ b/packages/babel-plugin-fbtee/src/__tests__/fbtInnerOuter-test.tsx @@ -2,17 +2,15 @@ import { describe, expect, it } from '@jest/globals'; import { getChildToParentRelationships } from '../index.tsx'; import { transform, withFbtImportStatement } from './FbtTestUtil.tsx'; -function testChildToParentRelationships([, testData]: readonly [ +const testChildToParentRelationships = ([, testData]: readonly [ name: string, - { input: string; output: Record }, -]) { - const body = testData.input.replace(/\/\*\*(?:\/|[^*]|\*+[^*/])*\*+\//, ''); - transform(body, { collectFbt: true }); - - expect(JSON.stringify(testData.output, null, ' ')).toEqual( - JSON.stringify(getChildToParentRelationships(), null, ' '), - ); -} + { input: string; output: Map }, +]) => { + transform(testData.input.replace(/\/\*\*(?:\/|[^*]|\*+[^*/])*\*+\//, ''), { + collectFbt: true, + }); + expect(testData.output).toEqual(getChildToParentRelationships()); +}; const testData = [ [ @@ -24,7 +22,7 @@ const testData = [ liked your video ;`, ), - output: { 1: 0 }, + output: new Map([[1, 0]]), }, ], [ @@ -39,7 +37,10 @@ const testData = [ your video ;`, ), - output: { 1: 0, 2: 1 }, + output: new Map([ + [1, 0], + [2, 1], + ]), }, ], [ @@ -55,7 +56,12 @@ const testData = [ ;`, ), - output: { 1: 0, 2: 1, 3: 0, 4: 3 }, + output: new Map([ + [1, 0], + [2, 1], + [3, 0], + [4, 3], + ]), }, ], [ @@ -79,7 +85,10 @@ const testData = [
another child!
;`, ), - output: { 1: 0, 4: 3 }, + output: new Map([ + [1, 0], + [4, 3], + ]), }, ], [ @@ -95,7 +104,10 @@ const testData = [ ;`, ), - output: { 1: 0, 3: 2 }, + output: new Map([ + [1, 0], + [3, 2], + ]), }, ], ] as const; diff --git a/packages/babel-plugin-fbtee/src/__tests__/jsx-test.tsx b/packages/babel-plugin-fbtee/src/__tests__/jsx-test.tsx index 003be82d..37d01815 100644 --- a/packages/babel-plugin-fbtee/src/__tests__/jsx-test.tsx +++ b/packages/babel-plugin-fbtee/src/__tests__/jsx-test.tsx @@ -374,7 +374,7 @@ const testData = { fbtCommon: { No: 'The description for the common string "No"' }, }, - throws: ` must not have "desc" attribute`, + throws: ` must not have "desc" attribute.`, }, 'should throw for strings with `common` attribute equal to false': { @@ -384,7 +384,7 @@ const testData = { fbtCommon: { Yes: 'The description for the common string "Yes"' }, }, - throws: `Unable to find attribute "desc".`, + throws: `This node requires a 'desc' attribute.`, }, 'should throw on invalid attributes in fbt:param': { diff --git a/packages/babel-plugin-fbtee/src/babel-processors/FbtFunctionCallProcessor.tsx b/packages/babel-plugin-fbtee/src/babel-processors/FbtFunctionCallProcessor.tsx index 61ec2199..e15bfc7d 100644 --- a/packages/babel-plugin-fbtee/src/babel-processors/FbtFunctionCallProcessor.tsx +++ b/packages/babel-plugin-fbtee/src/babel-processors/FbtFunctionCallProcessor.tsx @@ -31,7 +31,6 @@ import FbtElementNode from '../fbt-nodes/FbtElementNode.tsx'; import FbtImplicitParamNode from '../fbt-nodes/FbtImplicitParamNode.tsx'; import type { AnyFbtNode } from '../fbt-nodes/FbtNode.tsx'; import { isConcreteFbtNode } from '../fbt-nodes/FbtNodeType.tsx'; -import FbtParamNode from '../fbt-nodes/FbtParamNode.tsx'; import type { BindingName, FbtCallSiteOptions, @@ -463,7 +462,7 @@ export default class FbtFunctionCallProcessor { } if (fbtNode instanceof FbtElementNode) { - // gather list of svArgsMap for all args combination for later sanity checks + // gather list of svArgsMap for all args combination for later checks svArgsMapList.push(svArgsMap); } else if (this.pluginOptions.generateOuterTokenName === true) { leaf.outerTokenName = fbtNode.getTokenName(svArgsMap); @@ -565,23 +564,15 @@ export default class FbtFunctionCallProcessor { return; } - const parentFbtConstructName = - nodeChecker.getFbtNodeType(parentNode); - if ( - parentFbtConstructName && - isConcreteFbtNode(parentFbtConstructName) - ) { + const parentName = nodeChecker.getFbtNodeType(parentNode); + if (parentName && isConcreteFbtNode(parentName)) { throw errorAt( parentNode, - `Expected fbt constructs to not nest inside fbt constructs, ` + - `but found ` + - `${nodeChecker.moduleName}.${ - nullthrows(childFbtConstructName) as string - } ` + - `nest inside ` + - `${nodeChecker.moduleName}.${ - nullthrows(parentFbtConstructName) as string - }`, + `'fbt' constructs should not be nested inside of other fbt constructs. Found '${nodeChecker.moduleName}.${nullthrows( + childFbtConstructName, + )}' nested inside '${nodeChecker.moduleName}.${nullthrows( + parentName, + )}'.`, ); } parentPath = parentPath.parentPath; @@ -666,9 +657,10 @@ export default class FbtFunctionCallProcessor { const fbtRuntimeArgs = []; for (const child of fbtNode.children) { if ( - child instanceof FbtParamNode && - child.options.gender == null && - child.options.number == null + child.type === 'list' || + (child.type === 'param' && + child.options.gender == null && + child.options.number == null) ) { fbtRuntimeArgs.push(child.getFbtRuntimeArg()); } diff --git a/packages/babel-plugin-fbtee/src/babel-processors/JSXFbtProcessor.tsx b/packages/babel-plugin-fbtee/src/babel-processors/JSXFbtProcessor.tsx index 62591e63..6292813a 100644 --- a/packages/babel-plugin-fbtee/src/babel-processors/JSXFbtProcessor.tsx +++ b/packages/babel-plugin-fbtee/src/babel-processors/JSXFbtProcessor.tsx @@ -35,8 +35,8 @@ import { import type { BindingName, FbtOptionConfig } from '../FbtConstants.tsx'; import { CommonOption, - FbtCallMustHaveAtLeastOneOfTheseAttributes, FbtRequiredAttributes, + RequiredFbtAttributes, ValidFbtOptions, } from '../FbtConstants.tsx'; import FbtNodeChecker from '../FbtNodeChecker.tsx'; @@ -79,6 +79,16 @@ export default class JSXFbtProcessor { this.nodeChecker = nodeChecker; this.path = path; this.validFbtExtraOptions = validFbtExtraOptions; + + const { node } = this; + for (const attribute of node.openingElement.attributes) { + if (attribute.type === 'JSXSpreadAttribute') { + throw errorAt( + node, + `<${this.moduleName}> does not support spreading attributes.`, + ); + } + } } static create({ @@ -139,10 +149,10 @@ export default class JSXFbtProcessor { getUnknownCommonStringErrorMessage(moduleName, textValue), ); } - if (getAttributeByName(this._getOpeningElementAttributes(), 'desc')) { + if (getAttributeByName(this.node, 'desc')) { throw errorAt( node, - `<${moduleName} common={true}> must not have "desc" attribute`, + `<${moduleName} common> must not have "desc" attribute.`, ); } desc = stringLiteral(descValue); @@ -153,13 +163,12 @@ export default class JSXFbtProcessor { } _getOptions(): ObjectExpression | null { - // Optional attributes to be passed as options. - const attrs = this._getOpeningElementAttributes(); + const attributes = this.node.openingElement.attributes; this._assertHasMandatoryAttributes(); const options = - attrs.length > 0 + attributes.length > 0 ? getOptionsFromAttributes( - attrs, + attributes, { ...this.validFbtExtraOptions, ...ValidFbtOptions }, FbtRequiredAttributes, ) @@ -167,40 +176,20 @@ export default class JSXFbtProcessor { return (options?.properties.length ?? 0) > 0 ? options : null; } - _getOpeningElementAttributes(): ReadonlyArray { - if (this._openingElementAttributes != null) { - return this._openingElementAttributes; - } - - const { node } = this; - this._openingElementAttributes = node.openingElement.attributes.map( - (attribute) => { - if (attribute.type === 'JSXSpreadAttribute') { - throw errorAt( - node, - `<${this.moduleName}> does not support JSX spread attribute`, - ); - } - return attribute; - }, - ); - return this._openingElementAttributes; - } - _assertHasMandatoryAttributes() { if ( - !this._getOpeningElementAttributes().some( + !this.node.openingElement.attributes.some( (attribute) => + attribute.type === 'JSXAttribute' && attribute.name.type === 'JSXIdentifier' && - FbtCallMustHaveAtLeastOneOfTheseAttributes.has(attribute.name.name), + RequiredFbtAttributes.has(attribute.name.name), ) ) { throw errorAt( this.node, - `<${this.moduleName}> must have at least ` + - `one of these attributes: ${[ - ...FbtCallMustHaveAtLeastOneOfTheseAttributes, - ].join(', ')}`, + `<${this.moduleName}> must have at least one of these attributes: ${[ + ...RequiredFbtAttributes, + ].join(', ')}`, ); } } @@ -317,10 +306,7 @@ export default class JSXFbtProcessor { _getDescAttributeValue(): Expression { const { moduleName } = this; - const descAttr = getAttributeByNameOrThrow( - this._getOpeningElementAttributes(), - 'desc', - ); + const descAttr = getAttributeByNameOrThrow(this.node, 'desc'); const { node } = this; if (!descAttr || descAttr.value == null) { throw errorAt(node, `<${moduleName}> requires a "desc" attribute`); @@ -344,10 +330,7 @@ export default class JSXFbtProcessor { } _getCommonAttributeValue(): null | BooleanLiteral { - const commonAttr = getAttributeByName( - this._getOpeningElementAttributes(), - CommonOption, - ); + const commonAttr = getAttributeByName(this.node, CommonOption); if (commonAttr == null) { return null; } diff --git a/packages/babel-plugin-fbtee/src/bin.tsx b/packages/babel-plugin-fbtee/src/bin.tsx index fb50fec3..a5dc23b8 100644 --- a/packages/babel-plugin-fbtee/src/bin.tsx +++ b/packages/babel-plugin-fbtee/src/bin.tsx @@ -1,4 +1,4 @@ -#! /usr/bin/env node --experimental-strip-types --no-warnings +#!/usr/bin/env -S node --experimental-strip-types --no-warnings const command = process.argv[2]; process.argv.splice(2, 1); diff --git a/packages/babel-plugin-fbtee/src/bin/FbtCollector.tsx b/packages/babel-plugin-fbtee/src/bin/FbtCollector.tsx index c9f9d597..b770c428 100644 --- a/packages/babel-plugin-fbtee/src/bin/FbtCollector.tsx +++ b/packages/babel-plugin-fbtee/src/bin/FbtCollector.tsx @@ -29,9 +29,9 @@ export type CollectorConfig = { transform?: ExternalTransform | null; }; type ParentPhraseIndex = number; -export type ChildParentMappings = { - [childPhraseIndex: number]: ParentPhraseIndex; -}; +export type ChildParentMappings = Map; +export type RawChildParentMappings = Record; + export type HashToLeaf = Partial< Record< PatternHash, @@ -88,17 +88,13 @@ const transform = ( }; export default class FbtCollector implements IFbtCollector { - _phrases: Array; - _childParentMappings: ChildParentMappings; - _extraOptions: FbtOptionConfig; - _config: CollectorConfig; + _phrases: Array = []; + _childParentMappings: ChildParentMappings = new Map(); - constructor(config: CollectorConfig, extraOptions: FbtOptionConfig) { - this._phrases = []; - this._childParentMappings = {}; - this._extraOptions = extraOptions; - this._config = config; - } + constructor( + private readonly config: CollectorConfig, + private readonly extraOptions: FbtOptionConfig, + ) {} async collectFromOneFile( source: string, @@ -107,37 +103,35 @@ export default class FbtCollector implements IFbtCollector { ): Promise { const options = { collectFbt: true, - extraOptions: this._extraOptions, - fbtCommon: this._config.fbtCommon, + extraOptions: this.extraOptions, + fbtCommon: this.config.fbtCommon, fbtEnumManifest, filename, - generateOuterTokenName: this._config.generateOuterTokenName, + generateOuterTokenName: this.config.generateOuterTokenName, } as const; if (!textContainsFbtLikeModule(source)) { return; } - const externalTransform = this._config.transform; + const externalTransform = this.config.transform; if (externalTransform) { externalTransform(source, options, filename); } else { transform( source, options, - this._config.plugins || [], - this._config.presets || [], + this.config.plugins || [], + this.config.presets || [], ); } const newPhrases = getExtractedStrings(); const newChildParentMappings = getChildToParentRelationships(); const offset = this._phrases.length; - Object.entries(newChildParentMappings).forEach( - ([childIndex, parentIndex]) => { - this._childParentMappings[offset + +childIndex] = offset + parentIndex; - }, - ); + for (const [childIndex, parentIndex] of newChildParentMappings) { + this._childParentMappings.set(offset + childIndex, offset + parentIndex); + } // PackagerPhrase is an extended type of Phrase this._phrases.push(...(newPhrases as Array)); diff --git a/packages/babel-plugin-fbtee/src/bin/__mocks__/CustomFbtCollector.tsx b/packages/babel-plugin-fbtee/src/bin/__mocks__/CustomFbtCollector.tsx index 37d10859..b455d58d 100644 --- a/packages/babel-plugin-fbtee/src/bin/__mocks__/CustomFbtCollector.tsx +++ b/packages/babel-plugin-fbtee/src/bin/__mocks__/CustomFbtCollector.tsx @@ -60,10 +60,7 @@ export default class CustomFbtCollector implements IFbtCollector { } getChildParentMappings(): ChildParentMappings { - return { - // We need an object keyed by numbers only - [1]: 0, - }; + return new Map([[1, 0]]); } getFbtElementNodes(): Array { diff --git a/packages/babel-plugin-fbtee/src/bin/__tests__/translate-test.tsx b/packages/babel-plugin-fbtee/src/bin/__tests__/translate-test.tsx index 4d3f3f71..33f8b119 100644 --- a/packages/babel-plugin-fbtee/src/bin/__tests__/translate-test.tsx +++ b/packages/babel-plugin-fbtee/src/bin/__tests__/translate-test.tsx @@ -1,10 +1,18 @@ +import { afterEach, describe, it, jest } from '@jest/globals'; import { jsCodeNonASCIICharSerializer } from '../../__tests__/FbtTestUtil.tsx'; import { Options, processJSON } from '../translateUtils.tsx'; expect.addSnapshotSerializer(jsCodeNonASCIICharSerializer); +const consoleError = console.error; + +afterEach(() => { + console.error = consoleError; +}); + function testTranslateNewPhrases(options: Options) { it('should not throw on missing translations', async () => { + console.error = jest.fn(); const result = await processJSON( { phrases: [ @@ -43,6 +51,7 @@ function testTranslateNewPhrases(options: Options) { options, ); expect(result).toMatchSnapshot(); + expect(console.error).toHaveBeenCalled(); }); it('should translate string with no variation', async () => { diff --git a/packages/babel-plugin-fbtee/src/bin/collect.tsx b/packages/babel-plugin-fbtee/src/bin/collect.tsx index 7c3b7eb1..735d5ab1 100644 --- a/packages/babel-plugin-fbtee/src/bin/collect.tsx +++ b/packages/babel-plugin-fbtee/src/bin/collect.tsx @@ -1,4 +1,5 @@ import { existsSync, readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; import path, { resolve } from 'node:path'; import yargs from 'yargs'; import type { PlainFbtNode } from '../fbt-nodes/FbtNode.tsx'; @@ -12,9 +13,9 @@ import { getPackagers, } from './collectFbtUtils.tsx'; import type { - ChildParentMappings, IFbtCollector, PackagerPhrase, + RawChildParentMappings, } from './FbtCollector.tsx'; /** @@ -39,13 +40,12 @@ export type CollectFbtOutput = { * Index 1: phrase for "to the {=jungle}" * Index 2: phrase for "jungle" * - * Consequently, `childParentMappings` will be: + * Consequently, `childParentMappings` maps from childIndex to parentIndex: * * ``` * "childParentMappings": { - * // childIndex: parentIndex - * "1": 0, - * "2": 1 + * 1: 0, + * 2: 1, * } * ``` * @@ -53,7 +53,7 @@ export type CollectFbtOutput = { * The phrase at index 1 has a parent at index 0. * The phrase at index 2 has a parent at index 1; so it's a grand-child. */ - childParentMappings: ChildParentMappings; + childParentMappings: RawChildParentMappings; /** * List fbt element nodes (which in a sense represents the fbt DOM tree) for each fbt callsite * found in the source code. @@ -80,112 +80,105 @@ export type CollectFbtOutput = { export type CollectFbtOutputPhrase = CollectFbtOutput['phrases'][number]; -const args = { - COMMON_STRINGS: 'fbt-common-path', - CUSTOM_COLLECTOR: 'custom-collector', - GEN_FBT_NODES: 'gen-fbt-nodes', - GEN_OUTER_TOKEN_NAME: 'gen-outer-token-name', - HASH: 'hash-module', - HELP: 'h', - MANIFEST: 'manifest', - OPTIONS: 'options', - PACKAGER: 'packager', - PLUGINS: 'plugins', - PRESETS: 'presets', - PRETTY: 'pretty', - TRANSFORM: 'transform', -} as const; - const y = yargs(process.argv.slice(2)); const argv = y .usage('Collect fbt instances from source:\n$0 [options]') - .string(args.HASH) - .describe(args.HASH, 'Path to hashing module to use in text packager.') - .default(args.PACKAGER, 'text') + .string('hash-module') + .describe('hash-module', 'Path to hashing module to use in text packager.') + .default('packager', 'text') .describe( - args.PACKAGER, + 'packager', 'Packager to use. Choices are:\n' + " 'text' - hashing is done at the text (or leaf) level (more granular)\n" + "'phrase' - hashing is done at the phrase (entire fbt callsite) level\n" + " 'both' - Both phrase and text hashing are performed\n" + " 'none' - No hashing or alteration of phrase data\n", ) - .choices(args.PACKAGER, Object.values(packagerTypes)) - .describe(args.HELP, 'Display usage message') - .alias(args.HELP, 'help') - .boolean(args.MANIFEST) - .default(args.MANIFEST, false) + .choices('packager', Object.values(packagerTypes)) + .describe('h', 'Display usage message') + .alias('h', 'help') + .boolean('manifest') + .default('manifest', false) .describe( - args.MANIFEST, + 'manifest', 'Interpret stdin as JSON map of {: ' + '[, ...]}. Otherwise stdin itself will be parsed', ) - .string(args.COMMON_STRINGS) - .default(args.COMMON_STRINGS, '') + .string('fbt-common-path') + .default('fbt-common-path', '') .describe( - args.COMMON_STRINGS, + 'fbt-common-path', 'Optional path to the common strings module. ' + 'This is a map from {[text]: [description]}.', ) - .boolean(args.PRETTY) - .default(args.PRETTY, false) - .describe(args.PRETTY, 'Pretty-print the JSON output') - .boolean(args.GEN_OUTER_TOKEN_NAME) - .default(args.GEN_OUTER_TOKEN_NAME, false) + .boolean('pretty') + .default('pretty', false) + .describe('pretty', 'Pretty-print the JSON output') + .boolean('gen-outer-token-name') + .default('gen-outer-token-name', false) .describe( - args.GEN_OUTER_TOKEN_NAME, + 'gen-outer-token-name', 'Generate the outer token name of an inner string in the JSON output. ' + 'E.g. For the fbt string `Hello World`, ' + 'the outer string is "Hello {=World}", and the inner string is: "World". ' + 'So the outer token name of the inner string will be "=World"', ) - .boolean(args.GEN_FBT_NODES) - .default(args.GEN_FBT_NODES, false) + .boolean('gen-fbt-nodes') + .default('gen-fbt-nodes', false) .describe( - args.GEN_FBT_NODES, + 'gen-fbt-nodes', 'Generate the abstract representation of the fbt callsites as FbtNode trees.', ) - .string(args.TRANSFORM) - .default(args.TRANSFORM, null) + .string('transform') + .default('transform', null) .describe( - args.TRANSFORM, + 'transform', 'A custom transform to call into rather than the default provided. ' + 'Expects a signature of (source, options, filename) => mixed, and ' + 'for babel-pluginf-fbt to be run within the transform.', ) - .array(args.PLUGINS) - .default(args.PLUGINS, []) + .array('plugins') + .default('plugins', []) .describe( - args.PLUGINS, + 'plugins', 'List of auxiliary Babel plugins to enable for parsing source.\n' + 'E.g. --plugins @babel/plugin-syntax-dynamic-import @babel/plugin-syntax-numeric-separator', ) - .array(args.PRESETS) - .default(args.PRESETS, []) + .array('presets') + .default('presets', []) .describe( - args.PRESETS, + 'presets', 'List of auxiliary Babel presets to enable for parsing source.\n' + 'E.g. --presets @babel/preset-typescript', ) - .string(args.OPTIONS) + .string('options') .describe( - args.OPTIONS, + 'options', 'additional options that fbt(..., {can: "take"}). ' + - `i.e. --${args.OPTIONS} "locale,qux,id"`, + `i.e. --options "locale,qux,id"`, ) - .string(args.CUSTOM_COLLECTOR) + .string('custom-collector') .describe( - args.CUSTOM_COLLECTOR, + 'custom-collector', `In some complex scenarios, passing custom Babel presets or plugins to preprocess ` + `the input JS is not flexible enough. As an alternative, you can provide your own ` + `implementation of the FbtCollector module. ` + `It must at least expose the same public methods to expose the extract fbt phrases.\n` + - `i.e. --${args.CUSTOM_COLLECTOR} myFbtCollector.js`, + `i.e. --custom-collector myFbtCollector.js`, + ) + .boolean('include-default-strings') + .default('include-default-strings', true) + .describe( + 'include-default-strings', + `Include the default strings required by fbtee, such as for ''.`, ) .parseSync(); +const root = process.cwd(); +const require = createRequire(root); const extraOptions: FbtOptionConfig = {}; -const cliExtraOptions = argv[args.OPTIONS]; +const cliExtraOptions = argv['options']; + if (cliExtraOptions) { const opts = cliExtraOptions.split(','); for (let ii = 0; ii < opts.length; ++ii) { @@ -199,7 +192,7 @@ async function processJsonSource(collector: IFbtCollector, source: string) { let manifest: EnumManifest = {}; if (existsSync(manifestPath)) { manifest = ( - await import(path.resolve(process.cwd(), manifestPath), { + await import(path.resolve(root, manifestPath), { with: { type: 'json' }, }) ).default; @@ -214,12 +207,33 @@ async function processJsonSource(collector: IFbtCollector, source: string) { async function writeOutput(collector: IFbtCollector) { const packagers = await getPackagers( - argv[args.PACKAGER] || 'text', - argv[args.HASH] || null, + argv['packager'] || 'text', + argv['hash-module'] || null, ); const output = buildCollectFbtOutput(collector, packagers, { - genFbtNodes: argv[args.GEN_FBT_NODES], + genFbtNodes: argv['gen-fbt-nodes'], }); + + if (argv['include-default-strings']) { + try { + const json = ( + await import(require.resolve('fbtee/Strings.json'), { + with: { type: 'json' }, + }) + ).default as CollectFbtOutput; + + output.childParentMappings = { + ...output.childParentMappings, + ...json.childParentMappings, + }; + output.phrases.push(...json.phrases); + } catch (error) { + console.error( + `Attempted to include default strings from 'fbtee', but couldn't locate them.${error instanceof Error ? `\nError: ${error.message}` : ''}`, + ); + } + } + process.stdout.write( JSON.stringify(output, null, argv.pretty ? ' ' : undefined), ); @@ -227,7 +241,7 @@ async function writeOutput(collector: IFbtCollector) { } async function processSource(collector: IFbtCollector, source: string) { - await (argv[args.MANIFEST] + await (argv['manifest'] ? processJsonSource(collector, source) : collector.collectFromOneFile(source, 'file.js')); } @@ -235,13 +249,13 @@ async function processSource(collector: IFbtCollector, source: string) { if (argv.help) { y.showHelp(); } else { - const transformPath = argv[args.TRANSFORM]; + const transformPath = argv['transform']; const transform = transformPath ? (await import(transformPath)).default : null; - const commonFile = argv[args.COMMON_STRINGS]?.length - ? resolve(process.cwd(), argv[args.COMMON_STRINGS]) + const commonFile = argv['fbt-common-path']?.length + ? resolve(root, argv['fbt-common-path']) : null; const fbtCommon = commonFile?.length ? (commonFile.endsWith('.json') @@ -255,13 +269,13 @@ if (argv.help) { const collector = await getFbtCollector( { fbtCommon, - generateOuterTokenName: argv[args.GEN_OUTER_TOKEN_NAME], - plugins: argv[args.PLUGINS].map(require), - presets: argv[args.PRESETS].map(require), + generateOuterTokenName: argv['gen-outer-token-name'], + plugins: argv['plugins'].map(require), + presets: argv['presets'].map(require), transform, }, extraOptions, - argv[args.CUSTOM_COLLECTOR], + argv['custom-collector'], ); if (!argv._.length) { diff --git a/packages/babel-plugin-fbtee/src/bin/collectFbtUtils.tsx b/packages/babel-plugin-fbtee/src/bin/collectFbtUtils.tsx index 1a9701ca..c52d866a 100644 --- a/packages/babel-plugin-fbtee/src/bin/collectFbtUtils.tsx +++ b/packages/babel-plugin-fbtee/src/bin/collectFbtUtils.tsx @@ -28,7 +28,9 @@ export function buildCollectFbtOutput( }, ): CollectFbtOutput { return { - childParentMappings: fbtCollector.getChildParentMappings(), + childParentMappings: Object.fromEntries( + fbtCollector.getChildParentMappings(), + ), fbtElementNodes: options.genFbtNodes ? fbtCollector.getFbtElementNodes() : // using `undefined` so that the field is not outputted by JSON.stringify diff --git a/packages/babel-plugin-fbtee/src/bin/translate.tsx b/packages/babel-plugin-fbtee/src/bin/translate.tsx index 0d61263d..21bafc31 100644 --- a/packages/babel-plugin-fbtee/src/bin/translate.tsx +++ b/packages/babel-plugin-fbtee/src/bin/translate.tsx @@ -92,7 +92,7 @@ const argv = y .default('fbt-hash-module', false) .describe( 'fbt-hash-module', - `Similar to --${'jenkins'}, but pass the hash-module of your choice. The ` + + `Similar to --jenkins, but pass the hash-module of your choice. The ` + 'module should export a function with the same signature and operation ' + 'of fbt-hash-module', ) diff --git a/packages/babel-plugin-fbtee/src/bin/translateUtils.tsx b/packages/babel-plugin-fbtee/src/bin/translateUtils.tsx index 7747aeef..a85c231d 100644 --- a/packages/babel-plugin-fbtee/src/bin/translateUtils.tsx +++ b/packages/babel-plugin-fbtee/src/bin/translateUtils.tsx @@ -140,7 +140,7 @@ function checkAndFilterTranslations( if (options.strict) { throw new Error(message); } else { - process.stderr.write(`${message}\n`); + console.error(message); } } else { filteredTranslations[hash] = translations[hash]; diff --git a/packages/babel-plugin-fbtee/src/fbt-nodes/FbtElementNode.tsx b/packages/babel-plugin-fbtee/src/fbt-nodes/FbtElementNode.tsx index 01225255..b8ac2ba7 100644 --- a/packages/babel-plugin-fbtee/src/fbt-nodes/FbtElementNode.tsx +++ b/packages/babel-plugin-fbtee/src/fbt-nodes/FbtElementNode.tsx @@ -45,6 +45,7 @@ import { GenderStringVariationArg } from './FbtArguments.tsx'; import FbtEnumNode from './FbtEnumNode.tsx'; import FbtImplicitParamNode from './FbtImplicitParamNode.tsx'; import type FbtImplicitParamNodeType from './FbtImplicitParamNode.tsx'; +import FbtListNode from './FbtListNode.tsx'; import FbtNameNode from './FbtNameNode.tsx'; import type { AnyFbtNode, FbtChildNode } from './FbtNode.tsx'; import FbtNode from './FbtNode.tsx'; @@ -110,6 +111,7 @@ const childNodeClasses = new Map( FbtNameNode, FbtParamNode, FbtPluralNode, + FbtListNode, FbtPronounNode, FbtSameParamNode, ].map( @@ -235,10 +237,10 @@ export default class FbtElementNode } /** - * Run some sanity checks before producing text + * Run some checks before producing text * @throws if some fbt nodes in the tree have duplicate token names */ - static beforeGetTextSanityCheck( + static beforeGetTextCheck( instance: FbtElementNode | FbtImplicitParamNodeType, argsMap: StringVariationArgsMap, ) { @@ -252,16 +254,16 @@ export default class FbtElementNode } /** - * Run some sanity checks before producing text + * Run some checks before producing text * @throws if some fbt nodes in the tree have duplicate token names */ - _beforeGetTextSanityCheck(argsMap: StringVariationArgsMap) { - FbtElementNode.beforeGetTextSanityCheck(this, argsMap); + _beforeGetTextCheck(argsMap: StringVariationArgsMap) { + FbtElementNode.beforeGetTextCheck(this, argsMap); } override getText(argsMap: StringVariationArgsMap): string { try { - this._beforeGetTextSanityCheck(argsMap); + this._beforeGetTextCheck(argsMap); return getTextFromFbtNodeTree( this, argsMap, @@ -291,7 +293,7 @@ export default class FbtElementNode * @see IFbtElementNode#getDescription */ getDescription(_args: StringVariationArgsMap): string { - const [_, descriptionNode] = this.getCallNodeArguments() || []; + const [, descriptionNode] = this.getCallNodeArguments() || []; invariant( descriptionNode != null, 'fbt description argument cannot be found', diff --git a/packages/babel-plugin-fbtee/src/fbt-nodes/FbtEnumNode.tsx b/packages/babel-plugin-fbtee/src/fbt-nodes/FbtEnumNode.tsx index ec985dfb..992b575f 100644 --- a/packages/babel-plugin-fbtee/src/fbt-nodes/FbtEnumNode.tsx +++ b/packages/babel-plugin-fbtee/src/fbt-nodes/FbtEnumNode.tsx @@ -161,7 +161,7 @@ export default class FbtEnumNode extends FbtNode< } override getFbtRuntimeArg(): CallExpression { - const [_, rangeArg] = this.getCallNodeArguments() || []; + const [, rangeArg] = this.getCallNodeArguments() || []; let runtimeRange = null; if (isIdentifier(rangeArg)) { diff --git a/packages/babel-plugin-fbtee/src/fbt-nodes/FbtImplicitParamNode.tsx b/packages/babel-plugin-fbtee/src/fbt-nodes/FbtImplicitParamNode.tsx index e4810190..361f295a 100644 --- a/packages/babel-plugin-fbtee/src/fbt-nodes/FbtImplicitParamNode.tsx +++ b/packages/babel-plugin-fbtee/src/fbt-nodes/FbtImplicitParamNode.tsx @@ -93,7 +93,7 @@ export default class FbtImplicitParamNode override getText(argsMap: StringVariationArgsMap): string { try { - FbtElementNode.beforeGetTextSanityCheck(this, argsMap); + FbtElementNode.beforeGetTextCheck(this, argsMap); return getTextFromFbtNodeTree( this, argsMap, diff --git a/packages/babel-plugin-fbtee/src/fbt-nodes/FbtListNode.tsx b/packages/babel-plugin-fbtee/src/fbt-nodes/FbtListNode.tsx new file mode 100644 index 00000000..afc4be7a --- /dev/null +++ b/packages/babel-plugin-fbtee/src/fbt-nodes/FbtListNode.tsx @@ -0,0 +1,90 @@ +import { + CallExpression, + isCallExpression, + isNullLiteral, + isStringLiteral, + Node, + nullLiteral, +} from '@babel/types'; +import { BindingName } from '../FbtConstants.tsx'; +import FbtNodeChecker from '../FbtNodeChecker.tsx'; +import { createRuntimeCallExpression, errorAt } from '../FbtUtil.tsx'; +import nullthrows from '../nullthrows.tsx'; +import FbtNode from './FbtNode.tsx'; +import { tokenNameToTextPattern } from './FbtNodeUtil.tsx'; + +type Options = { + name: string | null; +}; + +export default class FbtListNode extends FbtNode< + never, + CallExpression, + null, + Options +> { + static readonly type = 'list'; + readonly type = 'list'; + + static fromNode(moduleName: BindingName, node: Node): FbtListNode | null { + if (!isCallExpression(node)) { + return null; + } + + const constructName = + FbtNodeChecker.forModule(moduleName).getFbtNodeType(node); + return constructName === 'list' + ? new FbtListNode({ + moduleName, + node, + }) + : null; + } + + override getArgsForStringVariationCalc(): ReadonlyArray { + return []; + } + + override getTokenName(): string { + return nullthrows(this.options.name); + } + + override getText(): string { + try { + return tokenNameToTextPattern(this.getTokenName()); + } catch (error) { + throw errorAt(this.node, error); + } + } + + override getOptions() { + const [name] = this.getCallNodeArguments() || []; + + return { + name: isStringLiteral(name) ? name.value : null, + }; + } + + override getFbtRuntimeArg(): CallExpression { + const [name, items, conjunction, delimiter] = + this.getCallNodeArguments() || []; + if (!items) { + throw errorAt(this.node, `'items' attribute for 'fbt:list' is missing.`); + } + + if (!name) { + throw errorAt(this.node, `'name' attribute for 'fbt:list' is missing.`); + } + + const args = [name, items]; + const hasDelimiter = delimiter && !isNullLiteral(delimiter); + if ((conjunction && !isNullLiteral(conjunction)) || hasDelimiter) { + args.push(conjunction || nullLiteral()); + } + if (hasDelimiter) { + args.push(delimiter); + } + + return createRuntimeCallExpression(this, args); + } +} diff --git a/packages/babel-plugin-fbtee/src/fbt-nodes/FbtNode.tsx b/packages/babel-plugin-fbtee/src/fbt-nodes/FbtNode.tsx index 6c14b1b3..30ed1c2c 100644 --- a/packages/babel-plugin-fbtee/src/fbt-nodes/FbtNode.tsx +++ b/packages/babel-plugin-fbtee/src/fbt-nodes/FbtNode.tsx @@ -17,6 +17,7 @@ import type { } from './FbtArguments.tsx'; import type FbtEnumNode from './FbtEnumNode.tsx'; import type FbtImplicitParamNode from './FbtImplicitParamNode.tsx'; +import FbtListNode from './FbtListNode.tsx'; import type FbtNameNode from './FbtNameNode.tsx'; import { FbtNodeType } from './FbtNodeType.tsx'; import type FbtParamNode from './FbtParamNode.tsx'; @@ -27,6 +28,7 @@ import type FbtTextNode from './FbtTextNode.tsx'; export type FbtChildNode = | FbtEnumNode + | FbtListNode | FbtImplicitParamNode | FbtNameNode | FbtParamNode @@ -255,15 +257,12 @@ export default abstract class FbtNode< getArgsForStringVariationCalc(): ReadonlyArray { throw errorAt( this.node, - 'This method must be implemented in a child class', + `'getArgsForStringVariationCalc' must be implemented in a child class.`, ); } getText(_argsMap: StringVariationArgsMap): string { - throw errorAt( - this.node, - 'This method must be implemented in a child class', - ); + throw errorAt(this.node, `'getText' must be implemented in a child class.`); } getTokenAliases(_argsMap: StringVariationArgsMap): TokenAliases | null { @@ -296,7 +295,7 @@ export default abstract class FbtNode< if ( error instanceof Error && error.message.includes( - 'This method must be implemented in a child class', + `'getArgsForStringVariationCalc': This method must be implemented in a child class.`, ) ) { stringVariationArgs = error; @@ -380,7 +379,7 @@ export default abstract class FbtNode< getFbtRuntimeArg(): CallExpression | null { throw errorAt( this.node, - 'This method must be implemented in a child class', + `'getFbtRuntimeArg' must be implemented in a child class.`, ); } diff --git a/packages/babel-plugin-fbtee/src/fbt-nodes/FbtNodeType.tsx b/packages/babel-plugin-fbtee/src/fbt-nodes/FbtNodeType.tsx index 61e7b191..66b2c853 100644 --- a/packages/babel-plugin-fbtee/src/fbt-nodes/FbtNodeType.tsx +++ b/packages/babel-plugin-fbtee/src/fbt-nodes/FbtNodeType.tsx @@ -1,5 +1,6 @@ export type ConcreteFbtNodeType = | 'enum' + | 'list' | 'name' | 'param' | 'plural' @@ -14,6 +15,7 @@ export type FbtNodeType = export const isConcreteFbtNode = (node: string): node is ConcreteFbtNodeType => node === 'enum' || + node === 'list' || node === 'name' || node === 'param' || node === 'plural' || diff --git a/packages/babel-plugin-fbtee/src/fbt-nodes/FbtNodeUtil.tsx b/packages/babel-plugin-fbtee/src/fbt-nodes/FbtNodeUtil.tsx index 23ed112c..c13787aa 100644 --- a/packages/babel-plugin-fbtee/src/fbt-nodes/FbtNodeUtil.tsx +++ b/packages/babel-plugin-fbtee/src/fbt-nodes/FbtNodeUtil.tsx @@ -211,12 +211,12 @@ export function buildFbtNodeMapForSameParam( } = {}; runOnNestedChildren(fbtNode, (child) => { if (child instanceof FbtSameParamNode) { - tokenNameToSameParamNode[child.getTokenName(argsMap)] = child; + tokenNameToSameParamNode[child.getTokenName()] = child; return; } else if ( // FbtImplicitParamNode token names appear redundant but // they'll be deduplicated via the token name mangling logic - child instanceof FbtImplicitParamNode + child.type === 'implicitParam' ) { return; } diff --git a/packages/babel-plugin-fbtee/src/fbt-nodes/FbtPluralNode.tsx b/packages/babel-plugin-fbtee/src/fbt-nodes/FbtPluralNode.tsx index 55a92c1e..a1ee8817 100644 --- a/packages/babel-plugin-fbtee/src/fbt-nodes/FbtPluralNode.tsx +++ b/packages/babel-plugin-fbtee/src/fbt-nodes/FbtPluralNode.tsx @@ -78,7 +78,7 @@ export default class FbtPluralNode extends FbtNode< ); try { - const [_, countArg] = this.getCallNodeArguments() || []; + const [, countArg] = this.getCallNodeArguments() || []; const count = enforceNodeCallExpressionArg( countArg, '`count`, the second function argument', diff --git a/packages/babel-plugin-fbtee/src/fbt-nodes/FbtSameParamNode.tsx b/packages/babel-plugin-fbtee/src/fbt-nodes/FbtSameParamNode.tsx index 438bec5b..88990ac4 100644 --- a/packages/babel-plugin-fbtee/src/fbt-nodes/FbtSameParamNode.tsx +++ b/packages/babel-plugin-fbtee/src/fbt-nodes/FbtSameParamNode.tsx @@ -8,7 +8,6 @@ import invariant from 'invariant'; import { BindingName } from '../FbtConstants.tsx'; import FbtNodeChecker from '../FbtNodeChecker.tsx'; import { errorAt } from '../FbtUtil.tsx'; -import type { StringVariationArgsMap } from './FbtArguments.tsx'; import FbtNode from './FbtNode.tsx'; import { tokenNameToTextPattern } from './FbtNodeUtil.tsx'; @@ -61,13 +60,13 @@ export default class FbtSameParamNode extends FbtNode< } } - override getTokenName(_argsMap: StringVariationArgsMap): string { + override getTokenName(): string { return this.options.name; } - override getText(_argsList: StringVariationArgsMap): string { + override getText(): string { try { - return tokenNameToTextPattern(this.getTokenName(_argsList)); + return tokenNameToTextPattern(this.getTokenName()); } catch (error) { throw errorAt(this.node, error); } diff --git a/packages/babel-plugin-fbtee/src/getNamespacedArgs.tsx b/packages/babel-plugin-fbtee/src/getNamespacedArgs.tsx index 9b1d9b78..2500ac70 100644 --- a/packages/babel-plugin-fbtee/src/getNamespacedArgs.tsx +++ b/packages/babel-plugin-fbtee/src/getNamespacedArgs.tsx @@ -1,7 +1,10 @@ import { + isJSXExpressionContainer, + isStringLiteral, JSXElement, jsxExpressionContainer, Node, + nullLiteral, stringLiteral, } from '@babel/types'; import { ConcreteFbtNodeType } from './fbt-nodes/FbtNodeType.tsx'; @@ -18,6 +21,7 @@ import { errorAt, expandStringConcat, filterEmptyNodes, + getAttributeByName, getAttributeByNameOrThrow, getOptionsFromAttributes, normalizeSpaces, @@ -25,57 +29,71 @@ import { export default function getNamespacedArgs( moduleName: string, -): Record Array> { +): Record Array> { return { - /** - * or - */ enum(node: JSXElement) { if (!node.openingElement.selfClosing) { - throw errorAt(node, `Expected ${moduleName}:enum to be selfClosing.`); + throw errorAt(node, `Expected ${moduleName}:enum to be self closing.`); } - const rangeAttr = getAttributeByNameOrThrow( - node.openingElement.attributes, - 'enum-range', - ); - - if (rangeAttr.value?.type !== 'JSXExpressionContainer') { + const range = getAttributeByNameOrThrow(node, 'enum-range'); + if (range.value?.type !== 'JSXExpressionContainer') { throw errorAt( node, 'Expected JSX Expression for enum-range attribute but got ' + - rangeAttr.value?.type, + range.value?.type, ); } - const valueAttr = getAttributeByNameOrThrow( - node.openingElement.attributes, - 'value', - ); - - if (valueAttr.value?.type === 'JSXExpressionContainer') { - return [valueAttr.value.expression, rangeAttr.value.expression]; - } else if (valueAttr.value?.type === 'StringLiteral') { - return [valueAttr.value, rangeAttr.value.expression]; + const value = getAttributeByNameOrThrow(node, 'value'); + if (value.value?.type === 'JSXExpressionContainer') { + return [value.value.expression, range.value.expression]; + } else if (value.value?.type === 'StringLiteral') { + return [value.value, range.value.expression]; } throw errorAt( node, - `Expected value attribute of <${moduleName}:enum> to be an expression ` + - `but got ${valueAttr.value?.type}`, + `Expected value attribute of <${moduleName}:enum> to be an expression but got ${value.value?.type}`, ); }, - /** - * or - */ + list(node: JSXElement) { + if (!node.openingElement.selfClosing) { + throw errorAt(node, `Expected ${moduleName}:list to be self closing.`); + } + + const name = getAttributeByNameOrThrow(node, 'name').value; + const items = getAttributeByNameOrThrow(node, 'items'); + + if (!isJSXExpressionContainer(items.value)) { + throw errorAt( + node, + `${moduleName}:param expects an array as "items" attribute.`, + ); + } + + const conjunction = getAttributeByName(node, 'conjunction'); + const delimiter = getAttributeByName(node, 'delimiter'); + return [ + name, + items.value.expression, + isStringLiteral(conjunction?.value) + ? conjunction.value + : isJSXExpressionContainer(conjunction?.value) + ? conjunction.value + : nullLiteral(), + isStringLiteral(delimiter?.value) + ? delimiter.value + : isJSXExpressionContainer(delimiter?.value) + ? delimiter.value.expression + : nullLiteral(), + ]; + }, + name(node: JSXElement) { - const attributes = node.openingElement.attributes; - const nameAttribute = getAttributeByNameOrThrow(attributes, 'name').value; - const genderAttribute = getAttributeByNameOrThrow( - attributes, - 'gender', - ).value; + const name = getAttributeByNameOrThrow(node, 'name').value; + const genderAttribute = getAttributeByNameOrThrow(node, 'gender').value; const children = filterEmptyNodes(node.children).filter( (child) => @@ -97,20 +115,17 @@ export default function getNamespacedArgs( } return [ - nameAttribute, + name, singularArg, genderAttribute?.type === 'JSXExpressionContainer' ? genderAttribute.expression - : null, + : nullLiteral(), ]; }, - /** - * or - */ param(node: JSXElement) { const attributes = node.openingElement.attributes; - const nameAttr = getAttributeByNameOrThrow(attributes, 'name'); + const name = getAttributeByNameOrThrow(node, 'name').value; const options = getOptionsFromAttributes( attributes, ValidParamOptions, @@ -125,9 +140,7 @@ export default function getNamespacedArgs( ); }); - // - // should be the equivalent of - // {' '} + // should be the equivalent of {' '} if ( children.length === 0 && node.children.length === 1 && @@ -146,16 +159,15 @@ export default function getNamespacedArgs( ); } - const nameAttrValue = nameAttr.value; if ( - nameAttrValue?.type === 'StringLiteral' && - nameAttrValue.loc && - nameAttrValue.loc.end.line > nameAttrValue.loc.start.line + name?.type === 'StringLiteral' && + name.loc && + name.loc.end.line > name.loc.start.line ) { - nameAttrValue.value = normalizeSpaces(nameAttrValue.value); + name.value = normalizeSpaces(name.value); } const paramArgs = [ - nameAttrValue, + name, (children[0].type === 'JSXExpressionContainer' && children[0].expression) || children[0], @@ -168,9 +180,6 @@ export default function getNamespacedArgs( return paramArgs; }, - /** - * or - */ plural(node: JSXElement) { const attributes = node.openingElement.attributes; const options = getOptionsFromAttributes( @@ -178,7 +187,7 @@ export default function getNamespacedArgs( PluralOptions, PluralRequiredAttributes, ); - const countAttr = getAttributeByNameOrThrow(attributes, 'count').value; + const count = getAttributeByNameOrThrow(node, 'count').value; const children = filterEmptyNodes(node.children).filter( (child) => child.type === 'JSXText' || child.type === 'JSXExpressionContainer', @@ -197,20 +206,17 @@ export default function getNamespacedArgs( singularNode, ); const singularArg = stringLiteral( - normalizeSpaces(singularText.value).trimRight(), + normalizeSpaces(singularText.value).trimEnd(), ); return [ singularArg, - countAttr?.type === 'JSXExpressionContainer' - ? countAttr.expression - : null, + count?.type === 'JSXExpressionContainer' + ? count.expression + : nullLiteral(), options, ]; }, - /** - * or - */ pronoun(node: JSXElement) { if (!node.openingElement.selfClosing) { throw errorAt( @@ -220,8 +226,8 @@ export default function getNamespacedArgs( } const attributes = node.openingElement.attributes; - const typeAttr = getAttributeByNameOrThrow(attributes, 'type').value; - if (typeAttr?.type !== 'StringLiteral') { + const typeAttribute = getAttributeByNameOrThrow(node, 'type').value; + if (typeAttribute?.type !== 'StringLiteral') { throw errorAt( node, `${moduleName}:pronoun attribute "type" must have StringLiteral content`, @@ -230,7 +236,7 @@ export default function getNamespacedArgs( if ( !Object.prototype.hasOwnProperty.call( ValidPronounUsages, - typeAttr.value, + typeAttribute.value, ) ) { throw errorAt( @@ -240,9 +246,9 @@ export default function getNamespacedArgs( ']', ); } - const result: Array = [stringLiteral(typeAttr.value)]; - const genderExpr = getAttributeByNameOrThrow(attributes, 'gender').value; + const result: Array = [stringLiteral(typeAttribute.value)]; + const genderExpr = getAttributeByNameOrThrow(node, 'gender').value; if (genderExpr?.type === 'JSXExpressionContainer') { result.push(genderExpr.expression); } @@ -259,9 +265,6 @@ export default function getNamespacedArgs( return result; }, - /** - * or - */ sameParam(node: JSXElement) { if (!node.openingElement.selfClosing) { throw errorAt( @@ -270,12 +273,7 @@ export default function getNamespacedArgs( ); } - const nameAttr = getAttributeByNameOrThrow( - node.openingElement.attributes, - 'name', - ); - - return [nameAttr.value]; + return [getAttributeByNameOrThrow(node, 'name').value]; }, }; } diff --git a/packages/babel-plugin-fbtee/src/index.tsx b/packages/babel-plugin-fbtee/src/index.tsx index 93e73d1e..3999d786 100644 --- a/packages/babel-plugin-fbtee/src/index.tsx +++ b/packages/babel-plugin-fbtee/src/index.tsx @@ -149,9 +149,8 @@ export type Phrase = FbtCallSiteOptions & { line_end: number; project: string; } & ObjectWithJSFBT; -type ChildToParentMap = { - [childIndex: number]: number; -}; + +type ChildToParentMap = Map; /** * Default options passed from a docblock. @@ -200,7 +199,7 @@ export default function transform() { validFbtExtraOptions = pluginOptions.extraOptions || {}; initDefaultOptions(visitor); allMetaPhrases = []; - childToParent = {}; + childToParent = new Map(); }, visitor: { /** @@ -267,7 +266,7 @@ export default function transform() { addMetaPhrase(metaPhrase, pluginOptions); if (metaPhrase.parentIndex != null) { - addEnclosingString( + childToParent.set( index + initialPhraseCount, metaPhrase.parentIndex + initialPhraseCount, ); @@ -307,9 +306,7 @@ export default function transform() { ) { throw errorAt( path.node, - `Fbt constructs can only be used within the scope of an fbt` + - ` string. I.e. It should be used directly inside an ` + - `‹fbt› / ‹fbs› callsite`, + `fbt constructs must be used within the scope of other fbt constructs.`, ); } }, @@ -352,10 +349,6 @@ function addMetaPhrase(metaPhrase: MetaPhrase, pluginOptions: PluginOptions) { }); } -function addEnclosingString(childIdx: number, parentIdx: number) { - childToParent[childIdx] = parentIdx; -} - function getEnumManifest(opts: PluginOptions): EnumManifest | null { const { fbtEnumManifest, fbtEnumPath, fbtEnumToPath } = opts; if (fbtEnumManifest != null) { @@ -377,7 +370,7 @@ export function getExtractedStrings(): Array { } export function getChildToParentRelationships(): ChildToParentMap { - return childToParent || {}; + return childToParent; } export function getFbtElementNodes(): Array { diff --git a/packages/babel-preset-fbtee/src/bin.tsx b/packages/babel-preset-fbtee/src/bin.tsx index 8cd95dfb..17b9cc31 100644 --- a/packages/babel-preset-fbtee/src/bin.tsx +++ b/packages/babel-preset-fbtee/src/bin.tsx @@ -1,3 +1,3 @@ -#! /usr/bin/env node --experimental-strip-types --no-warnings +#!/usr/bin/env -S node --experimental-strip-types --no-warnings import('@nkzw/babel-plugin-fbtee/lib/bin.js'); diff --git a/packages/fbtee/.npmignore b/packages/fbtee/.npmignore index 85de9cf9..97e84e3c 100644 --- a/packages/fbtee/.npmignore +++ b/packages/fbtee/.npmignore @@ -1 +1,6 @@ +.enum_manifest.json +.src_manifest.json +babel-build.config.js +lib-tmp +scripts src diff --git a/packages/fbtee/ReactTypes.d.ts b/packages/fbtee/ReactTypes.d.ts index 184511d1..2afd68b3 100644 --- a/packages/fbtee/ReactTypes.d.ts +++ b/packages/fbtee/ReactTypes.d.ts @@ -1,3 +1,5 @@ +import { Conjunction } from './src/list.tsx'; + enum IntlVariations { BITMASK_NUMBER = 28, NUMBER_ZERO = 16, @@ -110,36 +112,51 @@ type FbsOutput = { type FbtEnumProps = { 'enum-range': Array | { [enumKey: string]: string }; + key?: string | null; value: string; }; type FbtParamProps = ParamOptions & { + key?: string | null; name: string; }; type FbtPluralProps = PluralOptions & { count: number; + key?: string | null; }; type FbtPronounProps = PronounOptions & { gender: GenderConst; + key?: string | null; type: PronounType; }; type FbtNameProps = { gender: IntlVariations; + key?: string | null; name: string; }; type FbtSameParamProps = { + key?: string | null; name: string; }; -type FbtProps = +type FbtProps = { key?: string | null } & ( | (FbtOptions & { desc: string; }) - | { common: true }; + | { common: true } +); + +type FbtListProps = { + conjunction?: Conjunction; + delimiter?: Delimiter; + items: Array; + key?: string | null; + name: string; +}; declare module 'react' { namespace JSX { @@ -158,6 +175,7 @@ declare module 'react' { 'fbs:same-param': FbtSameParamProps; fbt: PropsWithChildren; 'fbt:enum': FbtEnumProps; + 'fbt:list': FbtListProps; 'fbt:name': PropsWithChildren; 'fbt:param': PropsWithChildren; 'fbt:plural': PropsWithChildren; diff --git a/packages/fbtee/babel-build.config.js b/packages/fbtee/babel-build.config.js new file mode 100644 index 00000000..5f821768 --- /dev/null +++ b/packages/fbtee/babel-build.config.js @@ -0,0 +1,11 @@ +import babelSyntaxTypescript from '@babel/plugin-syntax-typescript'; +import babelFbteeRuntime from '../babel-plugin-fbtee-runtime/lib/index.js'; +import babelFbtee from '../babel-plugin-fbtee/lib/index.js'; + +export default { + plugins: [ + babelFbtee, + babelFbteeRuntime, + [babelSyntaxTypescript, { isTSX: true }], + ], +}; diff --git a/packages/fbtee/package.json b/packages/fbtee/package.json index 180e4acb..02265795 100644 --- a/packages/fbtee/package.json +++ b/packages/fbtee/package.json @@ -21,12 +21,14 @@ "type": "module", "main": "lib/index.js", "scripts": { - "build": "tsup src/index.tsx -d lib --target=node22 --format=esm --clean --no-splitting --dts" + "build": "pnpm build:babel && pnpm build:prepend && tsup lib-tmp/index.tsx -d lib --target=node22 --format=esm --clean --no-splitting --dts", + "build:babel": "babel --delete-dir-on-start --copy-files --config-file ./babel-build.config.js --out-dir=lib-tmp --extensions=.tsx --keep-file-extension --ignore='src/**/__tests__/*.tsx' src", + "build:fbtee-strings": "pnpm fbtee manifest && pnpm fbtee collect --pretty --include-default-strings=false --manifest < .src_manifest.json > Strings.json $(find src -type f \\! -path '*/__tests__/*' \\! -path '*/__mocks__/*') && ./scripts/rewrite-filepaths.ts", + "build:prepend": "node -e \"const file = './lib-tmp/list.tsx'; fs.writeFileSync(file, '// @ts-nocheck\\n' + fs.readFileSync(file, 'utf8'));\"" }, "dependencies": { "invariant": "^2.2.4" }, - "devDependencies": {}, "peerDependencies": { "@nkzw/babel-plugin-fbtee": "workspace:^", "@nkzw/babel-plugin-fbtee-runtime": "workspace:^", diff --git a/packages/fbtee/scripts/rewrite-filepaths.ts b/packages/fbtee/scripts/rewrite-filepaths.ts new file mode 100755 index 00000000..03ae6a52 --- /dev/null +++ b/packages/fbtee/scripts/rewrite-filepaths.ts @@ -0,0 +1,12 @@ +#!/usr/bin/env -S node --experimental-strip-types --no-warnings +import { readFileSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; + +const fileName = join(import.meta.dirname, '../Strings.json'); +const strings = JSON.parse(readFileSync(fileName, 'utf8')); + +for (const key in strings.phrases) { + strings.phrases[key].filepath = 'node_modules/fbtee/lib/index.js'; +} + +writeFileSync(fileName, JSON.stringify(strings, null, 2), 'utf8'); diff --git a/packages/fbtee/src/Hooks.tsx b/packages/fbtee/src/Hooks.tsx index 82dc137c..311d9f5a 100644 --- a/packages/fbtee/src/Hooks.tsx +++ b/packages/fbtee/src/Hooks.tsx @@ -5,7 +5,6 @@ import type { } from '@nkzw/babel-plugin-fbtee'; import FbtResult from './FbtResult.tsx'; import type { FbtTableArg } from './FbtTableAccessor.tsx'; -import IntlViewerContext from './IntlViewerContext.tsx'; import type { BaseResult, FbtErrorContext, @@ -13,6 +12,7 @@ import type { NestedFbtContentItems, PureStringResult, } from './Types.d.ts'; +import IntlViewerContext from './ViewerContext.tsx'; export type ResolverFn = ( contents: NestedFbtContentItems, diff --git a/packages/fbtee/src/Types.d.ts b/packages/fbtee/src/Types.d.ts index 5c0881b4..16f9e864 100644 --- a/packages/fbtee/src/Types.d.ts +++ b/packages/fbtee/src/Types.d.ts @@ -1,6 +1,7 @@ import { ReactElement, ReactNode, ReactPortal } from 'react'; import GenderConst from './GenderConst.tsx'; import IntlVariations from './IntlVariations.tsx'; +import { Conjunction, Delimiter } from './list.tsx'; /** * Translated string from an `fbt()` call. @@ -137,11 +138,13 @@ type FbtAPIT = { value: string, range: ReadonlyArray | Readonly<{ [key: string]: string }>, ) => ParamOutput; - name: ( - tokenName: string, - value: string, - gender: IntlVariations, + list: ( + name: string, + items: ReadonlyArray, + conjunction?: Conjunction, + delimiter?: Delimiter, ) => ParamOutput; + name: (name: string, value: string, gender: IntlVariations) => ParamOutput; param: ( name: string, value: ParamInput, diff --git a/packages/fbtee/src/IntlViewerContext.tsx b/packages/fbtee/src/ViewerContext.tsx similarity index 100% rename from packages/fbtee/src/IntlViewerContext.tsx rename to packages/fbtee/src/ViewerContext.tsx diff --git a/packages/fbtee/src/__mocks__/getFbtResult.tsx b/packages/fbtee/src/__mocks__/getFbtResult.tsx index 79f55925..6643e9ba 100644 --- a/packages/fbtee/src/__mocks__/getFbtResult.tsx +++ b/packages/fbtee/src/__mocks__/getFbtResult.tsx @@ -1,4 +1,4 @@ -import FbtResult from '../FbtResult.tsx'; +import type FbtResult from '../FbtResult.tsx'; import type { NestedFbtContentItems } from '../Types.d.ts'; export default function getFbtResult( diff --git a/packages/fbtee/src/__tests__/__snapshots__/list-test.tsx.snap b/packages/fbtee/src/__tests__/__snapshots__/list-test.tsx.snap new file mode 100644 index 00000000..84ff7579 --- /dev/null +++ b/packages/fbtee/src/__tests__/__snapshots__/list-test.tsx.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` 1`] = `"Available Locations: Tokyo, London and Vienna."`; + +exports[`fbt.list() 1`] = `"Available Locations: Tokyo, London and Vienna"`; diff --git a/packages/fbtee/src/__tests__/fbs-test.tsx b/packages/fbtee/src/__tests__/fbs-test.tsx index 5c52d5d5..ddedd7be 100644 --- a/packages/fbtee/src/__tests__/fbs-test.tsx +++ b/packages/fbtee/src/__tests__/fbs-test.tsx @@ -3,8 +3,8 @@ import { describe, expect, it } from '@jest/globals'; import React from 'react'; import { fbs } from '../index.tsx'; -import IntlViewerContext from '../IntlViewerContext.tsx'; import setupFbtee from '../setupFbtee.tsx'; +import IntlViewerContext from '../ViewerContext.tsx'; setupFbtee({ hooks: { diff --git a/packages/fbtee/src/__tests__/fbt-runtime-test.tsx b/packages/fbtee/src/__tests__/fbt-runtime-test.tsx index 14038290..4b67896f 100644 --- a/packages/fbtee/src/__tests__/fbt-runtime-test.tsx +++ b/packages/fbtee/src/__tests__/fbt-runtime-test.tsx @@ -8,7 +8,7 @@ import intlNumUtils from '../intlNumUtils.tsx'; // in jest tests. We might need to move these modules inside beforeEach(). // These ones can stay here for now since they have a consistent behavior across this test suite. import IntlVariations from '../IntlVariations.tsx'; -import IntlViewerContext from '../IntlViewerContext.tsx'; +import IntlViewerContext from '../ViewerContext.tsx'; const ONE = String(IntlVariations.NUMBER_ONE); const FEW = String(IntlVariations.NUMBER_FEW); diff --git a/packages/fbtee/src/__tests__/intlList-test.tsx b/packages/fbtee/src/__tests__/intlList-test.tsx deleted file mode 100644 index 37d02e05..00000000 --- a/packages/fbtee/src/__tests__/intlList-test.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import getFbtResult from '../__mocks__/getFbtResult.tsx'; -import intlList, { Conjunctions, Delimiters } from '../intlList.tsx'; -import IntlViewerContext from '../IntlViewerContext.tsx'; -import setupFbtee from '../setupFbtee.tsx'; - -setupFbtee({ - hooks: { - getFbtResult, - getViewerContext: () => IntlViewerContext, - }, - translations: { en_US: {} }, -}); - -describe('intlList', () => { - it('should handle an empty list', () => { - expect(intlList([])).toBe(''); - }); - it('should handle a list full of null/undefined items', () => { - expect(intlList([null, undefined])).toBe(''); - }); - it('should handle a single item', () => { - expect(intlList(['first'])).toBe('first'); - }); - it('should handle two items', () => { - expect(intlList(['first', 'second'])).toBe('first and second'); - }); - it('should handle three items', () => { - expect(intlList(['first', 'second', 'third'])).toBe( - 'first, second and third', - ); - }); - it('should handle a bunch of items', () => { - const items = ['1', '2', '3', '4', '5', '6', '7', '8']; - const result = intlList(items); - expect(result).toBe('1, 2, 3, 4, 5, 6, 7 and 8'); - }); - it('should handle a bunch of items, some of which are null/undefined', () => { - const items = ['1', '2', '3', '4', null, '5', undefined, '6', '7', '8']; - const result = intlList(items); - expect(result).toBe('1, 2, 3, 4, 5, 6, 7 and 8'); - }); - it('should handle no conjunction', () => { - expect(intlList(['first', 'second', 'third'], Conjunctions.NONE)).toBe( - 'first, second, third', - ); - }); - it('should handle optional delimiter', () => { - expect( - intlList( - ['first', 'second', 'third'], - Conjunctions.NONE, - Delimiters.SEMICOLON, - ), - ).toBe('first; second; third'); - }); - it('should handle bullet delimiters', () => { - expect( - intlList( - ['first', 'second', 'third'], - Conjunctions.NONE, - Delimiters.BULLET, - ), - ).toBe('first \u2022 second \u2022 third'); - }); -}); diff --git a/packages/fbtee/src/__tests__/intlNumUtils-test.tsx b/packages/fbtee/src/__tests__/intlNumUtils-test.tsx index 27e9c5fc..d3633d58 100644 --- a/packages/fbtee/src/__tests__/intlNumUtils-test.tsx +++ b/packages/fbtee/src/__tests__/intlNumUtils-test.tsx @@ -208,9 +208,6 @@ describe('intlNumUtils:', () => { expect(intlNumUtils.formatNumber(12_345.1, 1)).toBe('12345#1'); // Above the thousand separator threshold. expect(intlNumUtils.formatNumber(123_456.1, 1)).toBe('123456#1'); - - // Clean up. - jest.resetModules(); }); }); @@ -275,7 +272,6 @@ describe('intlNumUtils:', () => { expect(intlNumUtils.formatNumberWithThousandDelimiters(1_234_567.1)).toBe( '12,34,567.1', ); - jest.resetModules(); }); it('Should render native digits when available', () => { @@ -293,7 +289,6 @@ describe('intlNumUtils:', () => { expect(intlNumUtils.formatNumberWithThousandDelimiters(1_234_567.1)).toBe( '\u0967\u0968,\u0969\u096A,\u096B\u096C\u096D.\u0967', ); - jest.resetModules(); }); it('Should respect user locale for number formatting', () => { @@ -314,9 +309,6 @@ describe('intlNumUtils:', () => { expect( intlNumUtils.formatNumberWithThousandDelimiters(123_456.1, 1), ).toBe('123/456#1'); - - // Clean up. - jest.resetModules(); }); }); @@ -512,9 +504,6 @@ describe('intlNumUtils:', () => { '\u0661\u0662\u0663\u066c\u0664\u0665\u0666\u066b\u0667\u0668\u0669', ), ).toBe(123_456.789); // decimal - - // Clean up. - jest.resetModules(); }); it('Should parse numbers with Persian keyboard input characters', () => { @@ -549,9 +538,6 @@ describe('intlNumUtils:', () => { '-\u06f1\u06f2\u06f3\u066C\u06f4\u06f5\u06f6\u066C\u06f7\u06f8\u06f9', ), ).toBe(-123_456_789); - - // Clean up. - jest.resetModules(); }); }); diff --git a/packages/fbtee/src/__tests__/list-test.tsx b/packages/fbtee/src/__tests__/list-test.tsx new file mode 100644 index 00000000..f791d855 --- /dev/null +++ b/packages/fbtee/src/__tests__/list-test.tsx @@ -0,0 +1,80 @@ +import { describe, expect, it } from '@jest/globals'; +import getFbtResult from '../__mocks__/getFbtResult.tsx'; +import fbt from '../fbt.tsx'; +import list from '../list.tsx'; +import setupFbtee from '../setupFbtee.tsx'; +import IntlViewerContext from '../ViewerContext.tsx'; + +setupFbtee({ + hooks: { + getFbtResult, + getViewerContext: () => IntlViewerContext, + }, + translations: { en_US: {} }, +}); + +describe('list', () => { + it('should handle an empty list', () => { + expect(list([])).toBe(''); + }); + it('should handle a list full of null/undefined items', () => { + expect(list([null, undefined])).toBe(''); + }); + it('should handle a single item', () => { + expect(list(['first'])).toBe('first'); + }); + it('should handle two items', () => { + expect(list(['first', 'second'])).toBe('first and second'); + }); + it('should handle three items', () => { + expect(list(['first', 'second', 'third'])).toBe('first, second and third'); + }); + it('should handle a bunch of items', () => { + expect(list(['1', '2', '3', '4', '5', '6', '7', '8'])).toBe( + '1, 2, 3, 4, 5, 6, 7 and 8', + ); + }); + it('should handle a bunch of items, some of which are null/undefined', () => { + expect( + list(['1', '2', '3', '4', null, '5', undefined, '6', '7', '8']), + ).toBe('1, 2, 3, 4, 5, 6, 7 and 8'); + }); + it('should handle no conjunction', () => { + expect(list(['first', 'second', 'third'], 'none')).toBe( + 'first, second, third', + ); + }); + it('should handle optional delimiter', () => { + expect(list(['first', 'second', 'third'], 'none', 'semicolon')).toBe( + 'first; second; third', + ); + }); + it('should handle bullet delimiters', () => { + expect(list(['first', 'second', 'third'], 'none', 'bullet')).toBe( + 'first \u2022 second \u2022 third', + ); + }); +}); + +test('fbt.list()', () => { + expect( + fbt( + 'Available Locations: ' + + // @ts-expect-error + fbt.list('locations', ['Tokyo', 'London', 'Vienna']), + 'Lists', + ), + ).toMatchSnapshot(); +}); + +test('', () => { + // eslint-disable-next-line no-unused-expressions, @typescript-eslint/no-unused-expressions + fbt; + + expect( + + Available Locations:{' '} + . + , + ).toMatchSnapshot(); +}); diff --git a/packages/fbtee/src/__tests__/mock-fbt-test.tsx b/packages/fbtee/src/__tests__/mock-fbt-test.tsx index 2ac13008..656a0103 100644 --- a/packages/fbtee/src/__tests__/mock-fbt-test.tsx +++ b/packages/fbtee/src/__tests__/mock-fbt-test.tsx @@ -1,8 +1,8 @@ import { describe, expect, it } from '@jest/globals'; import getFbtResult from '../__mocks__/getFbtResult.tsx'; import { fbt } from '../index.tsx'; -import IntlViewerContext from '../IntlViewerContext.tsx'; import setupFbtee from '../setupFbtee.tsx'; +import IntlViewerContext from '../ViewerContext.tsx'; setupFbtee({ hooks: { diff --git a/packages/fbtee/src/fbt.tsx b/packages/fbtee/src/fbt.tsx index 36b24216..7a2b23c7 100644 --- a/packages/fbtee/src/fbt.tsx +++ b/packages/fbtee/src/fbt.tsx @@ -2,6 +2,7 @@ import type { FbtTableKey, PatternString } from '@nkzw/babel-plugin-fbtee'; import invariant from 'invariant'; +import { ReactElement } from 'react'; import FbtResult from './FbtResult.tsx'; import type { ParamVariationType, @@ -22,6 +23,7 @@ import { getGenderVariations, getNumberVariations, } from './IntlVariationResolver.tsx'; +import list, { Conjunction, Delimiter } from './list.tsx'; import substituteTokens, { Substitutions } from './substituteTokens.tsx'; import type { BaseResult, NestedFbtContentItems } from './Types.d.ts'; @@ -125,17 +127,14 @@ export function createRuntime({ } } - substitutions = getAllSubstitutions(args); invariant(table !== null, 'Table access failed'); + substitutions = getAllSubstitutions(args); } const patternString = Array.isArray(table) ? table[0] : table; if (typeof patternString !== 'string') { throw new Error( - 'Table access did not result in string: ' + - (table === undefined ? 'undefined' : JSON.stringify(table)) + - ', Type: ' + - typeof table, + `Table access did not result in string: ${table === undefined ? 'undefined' : JSON.stringify(table)}, Type: ${typeof table}`, ); } @@ -171,9 +170,19 @@ export function createRuntime({ } return FbtTableAccessor.getEnumResult(value); }, - _implicitParam: (label: string, value: P, variations?: Variations) => param(label, value, variations), + _list: ( + label: string, + items: ReadonlyArray, + conjunction?: Conjunction, + delimiter?: Delimiter, + ) => [ + null, + { + [label]: list(items, conjunction, delimiter), + }, + ], _name: (label: string, value: P, gender: GenderConst) => FbtTableAccessor.getGenderResult(getGenderVariations(gender), { diff --git a/packages/fbtee/src/formatNumber.tsx b/packages/fbtee/src/formatNumber.tsx index 28dc9685..95a19cfd 100644 --- a/packages/fbtee/src/formatNumber.tsx +++ b/packages/fbtee/src/formatNumber.tsx @@ -1,6 +1,11 @@ import { ReactElement } from 'react'; +import fbs from './fbs.tsx'; import intlNumUtils from './intlNumUtils.tsx'; +// Ensure the local version of `fbs` is used instead of auto-importing `fbtee`. +// eslint-disable-next-line no-unused-expressions, @typescript-eslint/no-unused-expressions +fbs; + function formatNumber(value: number, decimals?: number | null): string { return intlNumUtils.formatNumber(value, decimals); } diff --git a/packages/fbtee/src/index.tsx b/packages/fbtee/src/index.tsx index 599c1d24..9130a8dc 100644 --- a/packages/fbtee/src/index.tsx +++ b/packages/fbtee/src/index.tsx @@ -7,7 +7,7 @@ export { default as setupFbtee } from './setupFbtee.tsx'; export { default as GenderConst } from './GenderConst.tsx'; export { default as FbtTranslations } from './FbtTranslations.tsx'; export { default as FbtResult } from './FbtResult.tsx'; -export { default as intlList } from './intlList.tsx'; +export { default as list, List } from './list.tsx'; export const fbt = fbtInternal as unknown as FbtAPI; export const fbs = fbsInternal as unknown as FbsAPI; diff --git a/packages/fbtee/src/intlList.tsx b/packages/fbtee/src/intlList.tsx deleted file mode 100644 index 05884a3d..00000000 --- a/packages/fbtee/src/intlList.tsx +++ /dev/null @@ -1,185 +0,0 @@ -/** - * @fbt {"project": "intl-core"} - */ - -/// - -import invariant from 'invariant'; -import { isValidElement } from 'react'; -import fbt from './fbt.tsx'; - -// Ensure the runtime is included. -// eslint-disable-next-line no-unused-expressions, @typescript-eslint/no-unused-expressions -fbt; - -export const Conjunctions = { - AND: 'AND', - NONE: 'NONE', - OR: 'OR', -} as const; - -export const Delimiters = { - BULLET: 'BULLET', - COMMA: 'COMMA', - SEMICOLON: 'SEMICOLON', -} as const; - -type Conjunction = keyof typeof Conjunctions; -type Delimiter = keyof typeof Delimiters; - -export default function intlList( - items: ReadonlyArray, - conjunction: Conjunction = Conjunctions.AND, - delimiter: Delimiter = Delimiters.COMMA, -): React.ReactNode { - items = items.filter(Boolean); - - if (process.env.NODE_ENV === 'development') { - for (const item of items) { - invariant( - typeof item === 'string' || isValidElement(item), - 'Must provide a string or ReactComponent to intlList.', - ); - } - } - - const count = items.length; - if (count === 0) { - return ''; - } else if (count === 1) { - return items[0]; - } - - const lastItem = items[count - 1]; - let output: React.ReactNode = items[0]; - - for (let i = 1; i < count - 1; ++i) { - switch (delimiter) { - case Delimiters.SEMICOLON: - output = ( - - {output} - {'; '} - {items[i]} - - ); - break; - case Delimiters.BULLET: - output = ( - - {output} •{' '} - {items[i]} - - ); - break; - default: - output = ( - - {output} - {', '} - {items[i]} - - ); - } - } - - switch (conjunction) { - case Conjunctions.AND: - return ( - - {output} - and - {lastItem} - - ); - - case Conjunctions.OR: - return ( - - {output} - or - {lastItem} - - ); - - case Conjunctions.NONE: - switch (delimiter) { - case Delimiters.SEMICOLON: - return ( - - {output} - {'; '} - {lastItem} - - ); - case Delimiters.BULLET: - return ( - - {output} •{' '} - {lastItem} - - ); - default: - return ( - - {output} - {', '} - {lastItem} - - ); - } - default: - invariant( - false, - 'Invalid conjunction %s provided to intlList', - conjunction, - ); - } -} diff --git a/packages/fbtee/src/list.tsx b/packages/fbtee/src/list.tsx new file mode 100644 index 00000000..00bb0d64 --- /dev/null +++ b/packages/fbtee/src/list.tsx @@ -0,0 +1,134 @@ +/// + +import invariant from 'invariant'; +import { isValidElement, ReactElement, ReactNode } from 'react'; +import fbt from './fbt.tsx'; + +export type Conjunction = 'and' | 'none' | 'or'; +export type Delimiter = 'bullet' | 'comma' | 'semicolon'; + +export default function list( + items: ReadonlyArray, + conjunction: Conjunction = 'and', + delimiter: Delimiter = 'comma', +): ReactNode { + // Ensure the local version of `fbt` is used instead of auto-importing `fbtee`. + // eslint-disable-next-line no-unused-expressions, @typescript-eslint/no-unused-expressions + fbt; + + items = items.filter(Boolean); + + if (process.env.NODE_ENV === 'development') { + for (const item of items) { + invariant( + typeof item === 'string' || isValidElement(item), + `Must provide a string or ReactComponent to ''.`, + ); + } + } + + const count = items.length; + if (count === 0) { + return ''; + } else if (count === 1) { + return items[0]; + } + + const lastItem = items.at(-1); + let output: ReactNode = items[0]; + + for (let index = 1; index < count - 1; index++) { + switch (delimiter) { + case 'semicolon': + output = ( + + {output} + {'; '} + {items[index]} + + ); + break; + case 'bullet': + output = ( + + {output} •{' '} + {items[index]} + + ); + break; + default: + output = ( + + {output} + {', '} + {items[index]} + + ); + } + } + + switch (conjunction) { + case 'and': + return ( + + {output} + and + {lastItem} + + ); + + case 'or': + return ( + + {output} + or + {lastItem} + + ); + + case 'none': + switch (delimiter) { + case 'semicolon': + return ( + + {output} + {'; '} + {lastItem} + + ); + case 'bullet': + return ( + + {output} •{' '} + {lastItem} + + ); + default: + return ( + + {output} + {', '} + {lastItem} + + ); + } + default: { + conjunction satisfies never; + throw new Error( + `Invalid conjunction ${conjunction} provided to ''.`, + ); + } + } +} + +export function List({ + conjunction, + delimiter, + items, +}: { + conjunction?: Conjunction; + delimiter?: Delimiter; + items: Array; +}) { + return list(items, conjunction, delimiter); +} diff --git a/packages/fbtee/src/setupFbtee.tsx b/packages/fbtee/src/setupFbtee.tsx index 6225ff90..1d549cc3 100644 --- a/packages/fbtee/src/setupFbtee.tsx +++ b/packages/fbtee/src/setupFbtee.tsx @@ -3,8 +3,8 @@ import FbtResult from './FbtResult.tsx'; import FbtTranslations, { TranslationDictionary } from './FbtTranslations.tsx'; import getFbsResult from './getFbsResult.tsx'; import Hook, { Hooks } from './Hooks.tsx'; -import IntlViewerContext from './IntlViewerContext.tsx'; import type { IFbtErrorListener, NestedFbtContentItems } from './Types.js'; +import IntlViewerContext from './ViewerContext.tsx'; const getFbtResult = ( contents: NestedFbtContentItems, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 56e325d5..67b31d9b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: .: devDependencies: + '@babel/cli': + specifier: ^7.26.4 + version: 7.26.4(@babel/core@7.26.0) '@babel/core': specifier: ^7.26.0 version: 7.26.0 @@ -23,6 +26,9 @@ importers: '@babel/plugin-syntax-import-attributes': specifier: ^7.26.0 version: 7.26.0(@babel/core@7.26.0) + '@babel/plugin-syntax-typescript': + specifier: ^7.25.9 + version: 7.25.9(@babel/core@7.26.0) '@babel/preset-react': specifier: ^7.26.3 version: 7.26.3(@babel/core@7.26.0) @@ -286,6 +292,13 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@babel/cli@7.26.4': + resolution: {integrity: sha512-+mORf3ezU3p3qr+82WvJSnQNE1GAYeoCfEv4fik6B5/2cvKZ75AX8oawWQdXtM9MmndooQj15Jr9kelRFWsuRw==} + engines: {node: '>=6.9.0'} + hasBin: true + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -873,6 +886,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3': + resolution: {integrity: sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==} + '@nkzw/eslint-config@1.18.1': resolution: {integrity: sha512-dpVQ6OPHD2/tU8K9ThMBXCmSIUfgessxH8/iCnzQrjyTmFnMNJhIZMsE0DqnIfFFdhf7Fu6dZkvKmHllzi2/bg==} peerDependencies: @@ -1467,6 +1483,10 @@ packages: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + chokidar@4.0.1: resolution: {integrity: sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==} engines: {node: '>= 14.16.0'} @@ -1518,6 +1538,10 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + commander@6.2.1: + resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} + engines: {node: '>= 6'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -2010,6 +2034,9 @@ packages: resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} engines: {node: '>= 6'} + fs-readdir-recursive@1.1.0: + resolution: {integrity: sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==} + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -2711,6 +2738,10 @@ packages: magic-string@0.30.15: resolution: {integrity: sha512-zXeaYRgZ6ldS1RJJUrMrYgNJ4fdwnyI6tVqoiIhyCyv5IVTK9BU8Ic2l253GGETQHxI4HNUwhJ3fjDhKqEoaAw==} + make-dir@2.1.0: + resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} + engines: {node: '>=6'} + make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} @@ -2945,6 +2976,10 @@ packages: engines: {node: '>=0.10'} hasBin: true + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + pioppo@1.2.0: resolution: {integrity: sha512-Mze+UGMj6fGMq7KQQTtiTDQN1RrfXeQwUssaKRgzp3HLx8SS5B9cTFIqQLkr2t6cIWpefGTpwiOLN8Y+3IODBg==} @@ -3082,6 +3117,10 @@ packages: resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} engines: {node: '>=8'} + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + readdirp@4.0.2: resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==} engines: {node: '>= 14.16.0'} @@ -3222,6 +3261,10 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + slash@2.0.0: + resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==} + engines: {node: '>=6'} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -3770,6 +3813,20 @@ snapshots: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 + '@babel/cli@7.26.4(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@jridgewell/trace-mapping': 0.3.25 + commander: 6.2.1 + convert-source-map: 2.0.0 + fs-readdir-recursive: 1.1.0 + glob: 7.2.3 + make-dir: 2.1.0 + slash: 2.0.0 + optionalDependencies: + '@nicolo-ribaudo/chokidar-2': 2.1.8-no-fsevents.3 + chokidar: 3.6.0 + '@babel/code-frame@7.26.2': dependencies: '@babel/helper-validator-identifier': 7.25.9 @@ -4516,6 +4573,9 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3': + optional: true + '@nkzw/eslint-config@1.18.1(eslint@8.57.1)(typescript@5.7.2)': dependencies: '@nkzw/eslint-plugin': 1.8.0(eslint@8.57.1) @@ -5248,6 +5308,19 @@ snapshots: char-regex@1.0.2: {} + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + optional: true + chokidar@4.0.1: dependencies: readdirp: 4.0.2 @@ -5289,6 +5362,8 @@ snapshots: commander@4.1.1: {} + commander@6.2.1: {} + concat-map@0.0.1: {} confbox@0.1.8: {} @@ -5923,6 +5998,8 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 + fs-readdir-recursive@1.1.0: {} + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -6869,6 +6946,11 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.0 optional: true + make-dir@2.1.0: + dependencies: + pify: 4.0.1 + semver: 5.7.2 + make-dir@4.0.0: dependencies: semver: 7.6.3 @@ -7091,6 +7173,8 @@ snapshots: pidtree@0.6.0: {} + pify@4.0.1: {} + pioppo@1.2.0: dependencies: dettle: 1.0.4 @@ -7215,6 +7299,11 @@ snapshots: parse-json: 5.2.0 type-fest: 0.6.0 + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + optional: true + readdirp@4.0.2: {} reflect.getprototypeof@1.0.8: @@ -7369,6 +7458,8 @@ snapshots: signal-exit@4.1.0: {} + slash@2.0.0: {} + slash@3.0.0: {} sort-object-keys@1.1.3: {} diff --git a/tsconfig.json b/tsconfig.json index 9be4c39c..2e1ef4cf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,6 +26,6 @@ "strict": true, "target": "es2022" }, - "exclude": ["packages/*/lib/", "node_modules"], + "exclude": ["packages/*/lib/", "packages/*/lib-tmp/", "node_modules"], "include": ["**/*.ts", "**/*.tsx"] }