Skip to content

Commit

Permalink
fix(webpack-loader): fix invalid dependencies have been reported by p… (
Browse files Browse the repository at this point in the history
  • Loading branch information
timofei-iatsenko authored Aug 3, 2023
1 parent 8c1c70c commit 1521ae7
Show file tree
Hide file tree
Showing 20 changed files with 214 additions and 48 deletions.
59 changes: 59 additions & 0 deletions packages/cli/src/api/catalog/getCatalogDependentFiles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { makeConfig } from "@lingui/conf"
import { Catalog } from "../catalog"
import { FormatterWrapper } from "../formats"
import mockFs from "mock-fs"
import * as process from "process"
import os from "os"
const skipOnWindows = os.platform() === "win32" ? it.skip : it

describe("getCatalogDependentFiles", () => {
let format: FormatterWrapper
Expand Down Expand Up @@ -151,4 +154,60 @@ describe("getCatalogDependentFiles", () => {
]
`)
})

// https://github.com/lingui/js-lingui/issues/1705
skipOnWindows(
"Should return absolute path when relative catalog path is specified",
async () => {
const oldCwd = process.cwd()

process.chdir("/")

mockFs({
"/src/locales": {
// "messages.pot": "bla",
"en.po": "bla",
"pl.po": "bla",
"es.po": "bla",
"pt-PT.po": "bla",
"pt-BR.po": "bla",
},
})

const config = makeConfig(
{
locales: ["en", "pl", "es", "pt-PT", "pt-BR"],
sourceLocale: "en",
fallbackLocales: {
"pt-PT": "pt-BR",
default: "en",
},
},
{ skipValidation: true }
)

const catalog = new Catalog(
{
name: null,
path: "./src/locales/{locale}",
include: ["src/"],
exclude: [],
format,
},
config
)

const actual = await getCatalogDependentFiles(catalog, "pt-PT")
mockFs.restore()

expect(actual).toMatchInlineSnapshot(`
[
/src/locales/pt-BR.po,
/src/locales/en.po,
]
`)

process.chdir(oldCwd)
}
)
})
7 changes: 6 additions & 1 deletion packages/cli/src/api/catalog/getCatalogDependentFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { Catalog } from "../catalog"
import { getFallbackListForLocale } from "./getFallbackListForLocale"
import fs from "node:fs/promises"

import path from "node:path"
import * as process from "process"

const fileExists = async (path: string) =>
!!(await fs.stat(path).catch(() => false))

Expand All @@ -25,7 +28,9 @@ export async function getCatalogDependentFiles(

const out: string[] = []

for (const file of files) {
for (let file of files) {
file = path.isAbsolute(file) ? file : path.join(process.cwd(), file)

if (await fileExists(file)) {
out.push(file)
}
Expand Down
19 changes: 18 additions & 1 deletion packages/loader/src/webpackLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,28 @@ const loader: LoaderDefinitionFunction<LinguiLoaderOptions> = async function (

const catalogRelativePath = path.relative(config.rootDir, this.resourcePath)

const { locale, catalog } = getCatalogForFile(
const fileCatalog = getCatalogForFile(
catalogRelativePath,
await getCatalogs(config)
)

if (!fileCatalog) {
throw new Error(
`Requested resource ${catalogRelativePath} is not matched to any of your catalogs paths specified in "lingui.config".
Resource: ${this.resourcePath}
Your catalogs:
${config.catalogs.map((c) => c.path).join("\n")}
Working dir is:
${process.cwd()}
Please check that \`catalogs.path\` is filled properly.\n`
)
}

const { locale, catalog } = fileCatalog
const dependency = await getCatalogDependentFiles(catalog, locale)
dependency.forEach((file) => this.addDependency(file))

Expand Down
13 changes: 13 additions & 0 deletions packages/loader/test/__snapshots__/loader.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,16 @@ exports[`lingui-loader should compile catalog in po format 1`] = `
mY42CM: Hello World,
}
`;

exports[`lingui-loader should compile catalog with relative path with no warnings 1`] = `
{
ED2Xk0: String from template,
mVmaLu: [
My name is ,
[
name,
],
],
mY42CM: Hello World,
}
`;
21 changes: 16 additions & 5 deletions packages/loader/test/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,28 @@ export type BuildResult = {
}

export async function build(entryPoint: string): Promise<BuildResult> {
// set cwd() to working path
const oldCwd = process.cwd()

process.chdir(path.dirname(entryPoint))

const compiler = getCompiler(entryPoint)

return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
if (err) reject(err)
if (stats.hasErrors()) reject(stats.toJson().errors)
process.chdir(oldCwd)

if (err) {
return reject(err)
}

const jsonStats = stats.toJson()
resolve({
loadBundle: () => import(path.join(jsonStats.outputPath, "bundle.js")),
stats: jsonStats,
compiler.close(() => {
resolve({
loadBundle: () =>
import(path.join(jsonStats.outputPath, "bundle.js")),
stats: jsonStats,
})
})
})
})
Expand Down
73 changes: 45 additions & 28 deletions packages/loader/test/loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,63 @@ import { build, watch } from "./compiler"
import { mkdtempSync } from "fs"
import os from "os"

const skipOnWindows = os.platform() === "win32" ? it.skip : it
const skipOnWindows = os.platform() === "win32" ? describe.skip : describe

describe("lingui-loader", () => {
skipOnWindows("lingui-loader", () => {
it("should compile catalog in po format", async () => {
expect.assertions(2)

const built = await build(path.join(__dirname, "po-format/entrypoint.js"))

const data = await built.loadBundle()
expect(built.stats.errors).toEqual([])
expect(built.stats.warnings).toEqual([])

expect((await data.load()).messages).toMatchSnapshot()
})

it("should compile catalog in json format", async () => {
expect.assertions(2)

const built = await build(
path.join(__dirname, "./json-format/entrypoint.js")
)

expect(built.stats.errors).toEqual([])
expect(built.stats.warnings).toEqual([])

const data = await built.loadBundle()
expect((await data.load()).messages).toMatchSnapshot()
})

skipOnWindows(
"should trigger webpack recompile on catalog dependency change",
async () => {
const fixtureTempPath = await copyFixture(
path.join(__dirname, "po-format")
)
it("should compile catalog with relative path with no warnings", async () => {
const built = await build(
path.join(__dirname, "./relative-catalog-path/entrypoint.js")
)

const watching = watch(path.join(fixtureTempPath, "/entrypoint.js"))
expect(built.stats.errors).toEqual([])
expect(built.stats.warnings).toEqual([])

const res = await watching.build()
const data = await built.loadBundle()
expect((await data.load()).messages).toMatchSnapshot()
})

expect((await res.loadBundle().then((m) => m.load())).messages)
.toMatchInlineSnapshot(`
it("should throw an error when requested catalog don't belong to lingui config", async () => {
const built = await build(
path.join(__dirname, "./not-known-catalog/entrypoint.js")
)

expect(built.stats.errors[0].message).toContain(
"is not matched to any of your catalogs paths"
)
expect(built.stats.warnings).toEqual([])
})

it("should trigger webpack recompile on catalog dependency change", async () => {
const fixtureTempPath = await copyFixture(path.join(__dirname, "po-format"))

const watching = watch(path.join(fixtureTempPath, "/entrypoint.js"))

const res = await watching.build()

expect((await res.loadBundle().then((m) => m.load())).messages)
.toMatchInlineSnapshot(`
{
ED2Xk0: String from template,
mVmaLu: [
Expand All @@ -55,10 +73,10 @@ describe("lingui-loader", () => {
}
`)

// change the dependency
await fs.writeFile(
path.join(fixtureTempPath, "/locale/messages.pot"),
`msgid "Hello World"
// change the dependency
await fs.writeFile(
path.join(fixtureTempPath, "/locale/messages.pot"),
`msgid "Hello World"
msgstr ""
msgid "My name is {name}"
Expand All @@ -67,13 +85,13 @@ msgstr ""
msgid "String from template changes!"
msgstr ""
`
)
)

const stats2 = await watching.build()
jest.resetModules()
const stats2 = await watching.build()
jest.resetModules()

expect((await stats2.loadBundle().then((m) => m.load())).messages)
.toMatchInlineSnapshot(`
expect((await stats2.loadBundle().then((m) => m.load())).messages)
.toMatchInlineSnapshot(`
{
mVmaLu: [
My name is ,
Expand All @@ -86,9 +104,8 @@ msgstr ""
}
`)

await watching.stop()
}
)
await watching.stop()
})
})

async function copyFixture(srcPath: string) {
Expand Down
10 changes: 10 additions & 0 deletions packages/loader/test/not-known-catalog/.linguirc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"locales": ["en"],
"catalogs": [{
"path": "./other-path/{locale}"
}],
"fallbackLocales": {
"default": "en"
},
"format": "po"
}
3 changes: 3 additions & 0 deletions packages/loader/test/not-known-catalog/entrypoint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export async function load() {
return import("@lingui/loader!./locale/en.po")
}
5 changes: 5 additions & 0 deletions packages/loader/test/not-known-catalog/locale/en.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
msgid "Hello World"
msgstr "Hello World"

msgid "My name is {name}"
msgstr "My name is {name}"
10 changes: 10 additions & 0 deletions packages/loader/test/relative-catalog-path/.linguirc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"locales": ["en"],
"catalogs": [{
"path": "./locale/{locale}"
}],
"fallbackLocales": {
"default": "en"
},
"format": "po"
}
3 changes: 3 additions & 0 deletions packages/loader/test/relative-catalog-path/entrypoint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export async function load() {
return import("@lingui/loader!./locale/en.po")
}
5 changes: 5 additions & 0 deletions packages/loader/test/relative-catalog-path/locale/en.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
msgid "Hello World"
msgstr "Hello World"

msgid "My name is {name}"
msgstr "My name is {name}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
msgid "Hello World"
msgstr ""

msgid "My name is {name}"
msgstr ""

msgid "String from template"
msgstr ""
2 changes: 1 addition & 1 deletion website/docs/misc/i18next.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ module.exports = {
locales: ["en", "cs", "fr"],
catalogs: [
{
path: "src/locales/{locale}/messages",
path: "<rootDir>/src/locales/{locale}/messages",
include: ["src"],
},
],
Expand Down
6 changes: 3 additions & 3 deletions website/docs/ref/conf.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,16 @@ Defines location of message catalogs and what files are included when [`extract`

Patterns in `include` and `exclude` are passed to [minimatch](https://github.com/isaacs/minimatch).

`path`, `include` and `exclude` patterns might include `<rootDir>` token, which is replaced by value of [`rootDir`](#rootdir).
`path`, `include`, and `exclude` are interpreted from the current process CWD. If you want to make these paths relative to the configuration file, you can prefix them with a [`rootDir`](#rootdir) token. By default, [`rootDir`](#rootdir) represents the configuration file's location.

`{name}` token in `path` is replaced with a catalog name. Source path must include `{name}` pattern as well and it works as a `*` glob pattern:

```json
{
"catalogs": [
{
"path": "./components/{name}/locale/{locale}",
"include": ["./components/{name}/"]
"path": "<rootDir>/components/{name}/locale/{locale}",
"include": ["<rootDir>/components/{name}/"]
}
]
}
Expand Down
10 changes: 5 additions & 5 deletions website/docs/ref/react.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,15 @@ const App = () => {

This hook allows access to the Lingui context. It returns an object with the following content:

| Key | Type | Description |
| ------------------ | --------------------- | ---------------------------------------------------------------------- |
| `i18n` | `I18n` | the `I18` object instance that you passed to `I18nProvider` |
| Key | Type | Description |
| ------------------ | --------------------- | ----------------------------------------------------------------------- |
| `i18n` | `I18n` | the `I18` object instance that you passed to `I18nProvider` |
| `_` | `I18n[_]` | reference to the [`i18n._`](/ref/core#i18n._) function, explained below |
| `defaultComponent` | `React.ComponentType` | the same `defaultComponent` you passed to `I18nProvider`, if provided |
| `defaultComponent` | `React.ComponentType` | the same `defaultComponent` you passed to `I18nProvider`, if provided |

Components that use `useLingui` hook will re-render when locale and / or catalogs change. However, the reference to the `i18n` object is stable and doesn't change between re-renders. This can lead to unexpected behavior with memoization (see [memoization pitfall](/tutorials/react-patterns#memoization-pitfall)).

To alleviate the issue, `useLingui` provides the `_` function, which is the same as [`i18n._`](/ref/core#i18n._) but *its reference changes* with each update of the Lingui context. Thanks to that, you can safely use this `_` function as a hook dependency.
To alleviate the issue, `useLingui` provides the `_` function, which is the same as [`i18n._`](/ref/core#i18n._) but _its reference changes_ with each update of the Lingui context. Thanks to that, you can safely use this `_` function as a hook dependency.

```jsx
import React from "react";
Expand Down
Loading

1 comment on commit 1521ae7

@vercel
Copy link

@vercel vercel bot commented on 1521ae7 Aug 3, 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.