Skip to content

Commit

Permalink
feat(extractor): expose extractFromFileWithBabel function for better …
Browse files Browse the repository at this point in the history
…flexibility (#1719)
  • Loading branch information
timofei-iatsenko authored Jun 29, 2023
1 parent 8206965 commit 43486dc
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 75 deletions.
208 changes: 134 additions & 74 deletions packages/cli/src/api/extractors/babel.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { DEFAULT_EXTENSIONS, transformAsync } from "@babel/core"
import { DEFAULT_EXTENSIONS, transformAsync, ParserOptions } from "@babel/core"

import type { ExtractPluginOpts } from "@lingui/babel-plugin-extract-messages"
import type {
ExtractPluginOpts,
Origin,
} from "@lingui/babel-plugin-extract-messages"
import linguiExtractMessages from "@lingui/babel-plugin-extract-messages"

import type { ExtractorType } from "@lingui/conf"
import { ParserPlugin } from "@babel/parser"
import { SourceMapConsumer } from "source-map"

import { LinguiMacroOpts } from "@lingui/macro/node"
import { ExtractedMessage, ExtractorCtx } from "@lingui/conf"

const babelRe = new RegExp(
"\\.(" +
Expand All @@ -21,6 +25,131 @@ const inlineSourceMapsRE = new RegExp(
/\/[\/\*][#@]\s+sourceMappingURL=data:application\/json;(?:charset:utf-8;)?base64,/i
)

/**
* Create a source mapper which could read original positions
* from either inline sourcemaps or from external passed as `sourceMaps` argument.
*
* Warning! You have to call destroy method after you finish working with a mapper.
*
* @param code source code
* @param sourceMaps Raw Sourcemaps object to mapping from. Check the https://github.com/mozilla/source-map#new-sourcemapconsumerrawsourcemap
*/
async function createSourceMapper(code: string, sourceMaps?: any) {
let sourceMapsConsumer: import("source-map").SourceMapConsumer

if (sourceMaps) {
const { SourceMapConsumer } = await import("source-map")
sourceMapsConsumer = await new SourceMapConsumer(sourceMaps)
} else if (code.search(inlineSourceMapsRE) != -1) {
const { SourceMapConsumer } = await import("source-map")
const { fromSource } = await import("convert-source-map")
sourceMapsConsumer = await new SourceMapConsumer(
fromSource(code).toObject()
)
}

return {
destroy: () => {
if (sourceMapsConsumer) {
sourceMapsConsumer.destroy()
}
},

originalPositionFor: (origin: Origin): Origin => {
if (!sourceMapsConsumer) {
return origin
}

const [_, line, column] = origin

const mappedPosition = sourceMapsConsumer.originalPositionFor({
line,
column,
})

return [mappedPosition.source, mappedPosition.line, mappedPosition.column]
},
}
}

/**
* @public
*
* Low level function used in default Lingui extractor.
* This function setup source maps and lingui plugins needed for
* extraction process but leaving `parserOptions` up to userland implementation.
*
*
* @example
* ```ts
* const extractor: ExtractorType = {
* ...
* async extract(filename, code, onMessageExtracted, ctx) {
* return extractFromFileWithBabel(filename, code, onMessageExtracted, ctx, {
* // https://babeljs.io/docs/babel-parser#plugins
* plugins: [
* "decorators-legacy",
* "typescript",
* "jsx",
* ],
* })
* },
* }
* ```
*/
export async function extractFromFileWithBabel(
filename: string,
code: string,
onMessageExtracted: (msg: ExtractedMessage) => void,
ctx: ExtractorCtx,
parserOpts: ParserOptions
) {
const mapper = await createSourceMapper(code, ctx?.sourceMaps)

await transformAsync(code, {
// don't generate code
code: false,

babelrc: false,
configFile: false,

filename: filename,

inputSourceMap: ctx?.sourceMaps,
parserOpts,

plugins: [
[
"macros",
{
// macro plugin uses package `resolve` to find a path of macro file
// this will not follow jest pathMapping and will resolve path from ./build
// instead of ./src which makes testing & developing hard.
// here we override resolve and provide correct path for testing
resolvePath: (source: string) => require.resolve(source),
lingui: {
extract: true,
linguiConfig: ctx.linguiConfig,
} satisfies LinguiMacroOpts,
},
],
[
linguiExtractMessages,
{
onMessageExtracted: (msg) => {
return onMessageExtracted({
...msg,
origin: mapper.originalPositionFor(msg.origin),
})
},
} satisfies ExtractPluginOpts,
],
],
})

mapper.destroy()
}

const extractor: ExtractorType = {
match(filename) {
return babelRe.test(filename)
Expand Down Expand Up @@ -53,78 +182,9 @@ const extractor: ExtractorType = {
parserPlugins.push("jsx")
}

let sourceMapsConsumer: SourceMapConsumer

if (ctx?.sourceMaps) {
sourceMapsConsumer = await new SourceMapConsumer(ctx?.sourceMaps)
} else if (code.search(inlineSourceMapsRE) != -1) {
const { fromSource } = await import("convert-source-map")
sourceMapsConsumer = await new SourceMapConsumer(
fromSource(code).toObject()
)
}

await transformAsync(code, {
// don't generate code
code: false,

babelrc: false,
configFile: false,

filename: filename,

inputSourceMap: ctx?.sourceMaps,
parserOpts: {
plugins: parserPlugins,
},

plugins: [
[
"macros",
{
// macro plugin uses package `resolve` to find a path of macro file
// this will not follow jest pathMapping and will resolve path from ./build
// instead of ./src which makes testing & developing hard.
// here we override resolve and provide correct path for testing
resolvePath: (source: string) => require.resolve(source),
lingui: {
extract: true,
linguiConfig: ctx.linguiConfig,
} satisfies LinguiMacroOpts,
},
],
[
linguiExtractMessages,
{
onMessageExtracted: (msg) => {
if (!sourceMapsConsumer) {
return onMessageExtracted(msg)
}

const [_, line, column] = msg.origin

const mappedPosition = sourceMapsConsumer.originalPositionFor({
line,
column,
})

return onMessageExtracted({
...msg,
origin: [
mappedPosition.source,
mappedPosition.line,
mappedPosition.column,
],
})
},
} satisfies ExtractPluginOpts,
],
],
return extractFromFileWithBabel(filename, code, onMessageExtracted, ctx, {
plugins: parserPlugins,
})

if (sourceMapsConsumer) {
sourceMapsConsumer.destroy()
}
},
}

Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ export { getCatalogForFile, getCatalogs } from "./catalog/getCatalogs"

export { createCompiledCatalog } from "./compile"

export { default as extractor } from "./extractors/babel"
export {
default as extractor,
extractFromFileWithBabel,
} from "./extractors/babel"
export { getCatalogDependentFiles } from "./catalog/getCatalogDependentFiles"
export * from "./types"

1 comment on commit 43486dc

@vercel
Copy link

@vercel vercel bot commented on 43486dc Jun 29, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.