Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(macro): support JSX macro inside conditional expressions #1436

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -302,20 +318,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",
},
},
},
],
Comment on lines +321 to +336
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was a single element - become an array

},
})
})
Expand All @@ -332,7 +350,7 @@ describe("jsx macro", () => {
/>`
)
const tokens = macro.tokenizeNode(exp)
expect(tokens).toMatchObject({
expect(tokens[0]).toMatchObject({
format: "select",
name: "gender",
options: {
Expand Down
Loading