Skip to content

Commit

Permalink
feat(macro): support JSX macro inside conditional expressions (#1436)
Browse files Browse the repository at this point in the history
* refactor(macro): pass  down and manipulate with `path` instead of `node`

* feature(macro): support JSX macro inside conditional expressions

* refactor(macro): remove alreadyVisitedCache in favor of dedupe references from babel-macro-plugin
  • Loading branch information
timofei-iatsenko authored Feb 15, 2023
1 parent 320b41f commit 44f8360
Show file tree
Hide file tree
Showing 5 changed files with 258 additions and 164 deletions.
40 changes: 21 additions & 19 deletions packages/macro/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,21 +52,24 @@ const jsxMacroTags = new Set(["Trans", "Plural", "Select", "SelectOrdinal"])
function macro({ references, state, babel, config }: MacroParams) {
const opts: LinguiMacroOpts = config as LinguiMacroOpts

const jsxNodes: NodePath[] = []
const jsNodes: NodePath[] = []
const jsxNodes = new Set<NodePath>()
const jsNodes = new Set<NodePath>()
let needsI18nImport = false

Object.keys(references).forEach((tagName) => {
const nodes = references[tagName]

if (jsMacroTags.has(tagName)) {
nodes.forEach((node) => {
jsNodes.push(node.parentPath)
jsNodes.add(node.parentPath)
})
} else if (jsxMacroTags.has(tagName)) {
// babel-plugin-macros return JSXIdentifier nodes.
// Which is for every JSX element would be presented twice (opening / close)
// Here we're taking JSXElement and dedupe it.
nodes.forEach((node) => {
// identifier.openingElement.jsxElement
jsxNodes.push(node.parentPath.parentPath)
jsxNodes.add(node.parentPath.parentPath)
})
} else {
throw nodes[0].buildCodeFrameError(`Unknown macro ${tagName}`)
Expand All @@ -76,14 +79,16 @@ function macro({ references, state, babel, config }: MacroParams) {
const stripNonEssentialProps =
process.env.NODE_ENV == "production" && !opts.extract

jsNodes.filter(isRootPath(jsNodes)).forEach((path) => {
if (alreadyVisited(path)) return
const jsNodesArray = Array.from(jsNodes)

jsNodesArray.filter(isRootPath(jsNodesArray)).forEach((path) => {
const macro = new MacroJS(babel, { i18nImportName, stripNonEssentialProps })
if (macro.replacePath(path)) needsI18nImport = true
})

jsxNodes.filter(isRootPath(jsxNodes)).forEach((path) => {
if (alreadyVisited(path)) return
const jsxNodesArray = Array.from(jsxNodes)

jsxNodesArray.filter(isRootPath(jsxNodesArray)).forEach((path) => {
const macro = new MacroJSX(babel, { stripNonEssentialProps })
macro.replacePath(path)
})
Expand All @@ -92,7 +97,7 @@ function macro({ references, state, babel, config }: MacroParams) {
addImport(babel, state, i18nImportModule, i18nImportName)
}

if (jsxNodes.length) {
if (jsxNodes.size) {
addImport(babel, state, TransImportModule, TransImportName)
}
}
Expand Down Expand Up @@ -135,6 +140,13 @@ function addImport(
}
}

/**
* Filtering nested macro calls
*
* <Macro>
* <Macro /> <-- this would be filtered out
* </Macro>
*/
function isRootPath(allPath: NodePath[]) {
return (node: NodePath) =>
(function traverse(path): boolean {
Expand All @@ -146,16 +158,6 @@ function isRootPath(allPath: NodePath[]) {
})(node)
}

const alreadyVisitedCache = new WeakSet()
const alreadyVisited = (path: NodePath) => {
if (alreadyVisitedCache.has(path)) {
return true
} else {
alreadyVisitedCache.add(path)
return false
}
}

;[...jsMacroTags, ...jsxMacroTags].forEach((name) => {
Object.defineProperty(module.exports, name, {
get() {
Expand Down
56 changes: 37 additions & 19 deletions packages/macro/src/macroJsx.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
import { parseExpression as _parseExpression } from "@babel/parser"
import * as types from "@babel/types"
import MacroJSX, { normalizeWhitespace } from "./macroJsx"
import { JSXElement } from "@babel/types"
import { transformSync } from "@babel/core"
import type { NodePath } from "@babel/traverse"
import type { JSXElement } from "@babel/types"

const parseExpression = (expression: string) =>
_parseExpression(expression, {
plugins: ["jsx"],
}) as JSXElement
const parseExpression = (expression: string) => {
let path: NodePath<JSXElement>

transformSync(expression, {
filename: "unit-test.js",
plugins: [
{
visitor: {
JSXElement: (d) => {
path = d
d.stop()
},
},
},
],
})

return path
}

function createMacro() {
return new MacroJSX({ types }, { stripNonEssentialProps: false })
Expand Down Expand Up @@ -321,20 +337,22 @@ describe("jsx macro", () => {
}),
format: "plural",
options: {
one: {
type: "arg",
name: "gender",
value: expect.objectContaining({
one: [
{
type: "arg",
name: "gender",
type: "Identifier",
}),
format: "select",
options: {
male: "he",
female: "she",
other: "they",
value: expect.objectContaining({
name: "gender",
type: "Identifier",
}),
format: "select",
options: {
male: "he",
female: "she",
other: "they",
},
},
},
],
},
})
})
Expand All @@ -351,7 +369,7 @@ describe("jsx macro", () => {
/>`
)
const tokens = macro.tokenizeNode(exp)
expect(tokens).toMatchObject({
expect(tokens[0]).toMatchObject({
format: "select",
name: "gender",
options: {
Expand Down
Loading

0 comments on commit 44f8360

Please sign in to comment.