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

Support App Router & Pages Router #22

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from 6 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
15 changes: 6 additions & 9 deletions __tests__/__snapshots__/templateAppDir.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ function Component() {
export default function __Next_Translate_new__88d9831a00__(props) {
const forceUpdate = __react.useReducer(() => [])[1];
const isClient = typeof window !== \\"undefined\\";
const el = isClient && document.getElementById(\\"__NEXT_TRANSLATE_DATA__\\");

if (isClient && !window.__NEXT_TRANSLATE__) {
if (isClient && !window.__NEXT_TRANSLATE__ && el) {
window.__NEXT_TRANSLATE__ = {
lang: __i18nConfig.defaultLocale,
namespaces: {},
Expand All @@ -29,8 +30,6 @@ export default function __Next_Translate_new__88d9831a00__(props) {
__react.useEffect(update);

function update(rerender = true) {
const el = document.getElementById(\\"__NEXT_TRANSLATE_DATA__\\");

if (!el) return;

const { lang, ns, pathname } = el.dataset;
Expand Down Expand Up @@ -59,8 +58,9 @@ function Component() {
export default function __Next_Translate_new__88d9831a00__(props) {
const forceUpdate = __react.useReducer(() => [])[1];
const isClient = typeof window !== \\"undefined\\";
const el = isClient && document.getElementById(\\"__NEXT_TRANSLATE_DATA__\\");

if (isClient && !window.__NEXT_TRANSLATE__) {
if (isClient && !window.__NEXT_TRANSLATE__ && el) {
window.__NEXT_TRANSLATE__ = {
lang: __i18nConfig.defaultLocale,
namespaces: {},
Expand All @@ -75,8 +75,6 @@ export default function __Next_Translate_new__88d9831a00__(props) {
__react.useEffect(update);

function update(rerender = true) {
const el = document.getElementById(\\"__NEXT_TRANSLATE_DATA__\\");

if (!el) return;

const { lang, ns, pathname } = el.dataset;
Expand Down Expand Up @@ -105,8 +103,9 @@ function Component() {
export default function __Next_Translate_new__88d9831a00__(props) {
const forceUpdate = __react.useReducer(() => [])[1];
const isClient = typeof window !== \\"undefined\\";
const el = isClient && document.getElementById(\\"__NEXT_TRANSLATE_DATA__\\");

if (isClient && !window.__NEXT_TRANSLATE__) {
if (isClient && !window.__NEXT_TRANSLATE__ && el) {
window.__NEXT_TRANSLATE__ = {
lang: __i18nConfig.defaultLocale,
namespaces: {},
Expand All @@ -121,8 +120,6 @@ export default function __Next_Translate_new__88d9831a00__(props) {
__react.useEffect(update);

function update(rerender = true) {
const el = document.getElementById(\\"__NEXT_TRANSLATE_DATA__\\");

if (!el) return;

const { lang, ns, pathname } = el.dataset;
Expand Down
77 changes: 48 additions & 29 deletions __tests__/index.test.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,65 @@
import { possiblePagesDirs } from '../src/utils'
import nextTranslate from '../src/index'
import path from 'path'
import fs from 'fs'

jest.spyOn(fs, 'existsSync')
jest.spyOn(fs, 'readdirSync')

jest.mock(
'../i18n',
() => ({
locales: ['en'],
defaultLocale: 'en',
const pagesInDirTests = [
{
name: 'pagesInDir should become array in loader pagesPaths config',
pagesInDir: 'src/app',
pages: {
'*': ['common'],
},
}),
{ virtual: true }
)
pagesPaths: expect.arrayContaining([expect.stringMatching(/src\/app\/$/)]),
},
{
name: 'pagesPaths should be defaulted to possible pages paths if pagesInDir is undefined',
pagesInDir: undefined,
pagesPaths: possiblePagesDirs.map((possiblePagesDir) => path.resolve(possiblePagesDir) + '/'),
},
]

describe('nextTranslate', () => {
describe('nextTranslate -> pagesInDir', () => {
test('uses app dir loader if pagesInDir points to app dir', () => {
fs.readdirSync.mockImplementationOnce(() => [])
fs.existsSync.mockImplementationOnce(() => true)
beforeEach(() => {
jest.resetModules()
})

pagesInDirTests.forEach(({ name, pagesInDir, pagesPaths }) => {
test(name, () => {
fs.readdirSync.mockImplementation(() => [])
fs.existsSync.mockImplementation(() => true)

jest.doMock(
'../i18n',
() => ({
locales: ['en'],
defaultLocale: 'en',
pagesInDir,
pages: {
'*': ['common'],
},
}),
{ virtual: true }
)

const config = nextTranslate({})
const config = nextTranslate({})

expect(config.webpack({})).toEqual(
expect.objectContaining({
module: {
rules: expect.arrayContaining([
expect.objectContaining({
use: expect.objectContaining({
loader: 'next-translate-plugin/loader',
options: expect.objectContaining({
isAppDirNext13: true,
expect(config.webpack({})).toEqual(
expect.objectContaining({
module: {
rules: expect.arrayContaining([
expect.objectContaining({
use: expect.objectContaining({
loader: 'next-translate-plugin/loader',
options: expect.objectContaining({ pagesPaths }),
}),
}),
}),
]),
},
})
)
]),
},
})
)
})
})
})
})
10 changes: 10 additions & 0 deletions __tests__/loader.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import webpack from 'webpack'

describe('nextTranslate', () => {
Copy link
Owner

Choose a reason for hiding this comment

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

❤️

describe('nextTranslate -> loader', () => {
test.todo('loader should use server page helpers')
test.todo('loader should use client page helpers')
test.todo('loader should use client component helpers')
test.todo('loader should use legacy page helpers')
})
})
57 changes: 25 additions & 32 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,20 @@ import path from 'path'
import type webpack from 'webpack'
import type { NextConfig } from 'next'

import { getDefaultExport, hasHOC, hasStaticName, parseFile } from './utils'
import {
getDefaultExport,
hasHOC,
hasStaticName,
parseFile,
possiblePagesDirs,
} from './utils'
import { LoaderOptions } from './types'
import type { I18nConfig, NextI18nConfig } from 'next-translate'

const test = /\.(tsx|ts|js|mjs|jsx)$/
const appDirNext13 = [
'app',
'src/app',
'app/app',
'integrations/app'
] as const

// https://github.com/blitz-js/blitz/blob/canary/nextjs/packages/next/build/utils.ts#L54-L59
const possiblePageDirs = [
'pages',
'src/pages',
'app/pages',
'integrations/pages',
] as const

function nextTranslate(nextConfig: NextConfig = {}): NextConfig {
const basePath = pkgDir()
const isAppDirNext13 = nextConfig.experimental?.appDir;
const dirs = isAppDirNext13 ? appDirNext13 : possiblePageDirs;

// NEXT_TRANSLATE_PATH env is supported both relative and absolute path
const dir = path.resolve(
Expand Down Expand Up @@ -56,26 +46,30 @@ function nextTranslate(nextConfig: NextConfig = {}): NextConfig {

let hasGetInitialPropsOnAppJs = false

if (!pagesInDir) {
for (const possiblePageDir of dirs) {
if (fs.existsSync(path.join(dir, possiblePageDir))) {
pagesInDir = possiblePageDir
break
}
}
}
const pagesInDirs = pagesInDir
? [pagesInDir]
: possiblePagesDirs.filter((possiblePagesDir) =>
fs.existsSync(path.join(dir, possiblePagesDir))
)

if (!pagesInDir || !fs.existsSync(path.join(dir, pagesInDir))) {
if (!pagesInDirs.length) {
// Pages folder not found, so we're not using the loader
return nextConfigWithI18n
}


const pagesPath = path.join(dir, pagesInDir)
const app = fs.readdirSync(pagesPath).find((page) => page.startsWith('_app.'))
const pagesPaths = pagesInDirs.map((pagesInDir) => path.join(dir, pagesInDir))
const app = pagesPaths
.filter((pagePath) => pagePath.endsWith('/pages'))
.map((pagePath) => {
const appPagePath = fs
.readdirSync(pagePath)
.find((page) => page.startsWith('_app.'))
return appPagePath && path.join(pagePath, appPagePath)
})
.find(Boolean)

if (app) {
const appPkg = parseFile(dir, path.join(pagesPath, app))
const appPkg = parseFile(dir, app)
const defaultExport = getDefaultExport(appPkg)

if (defaultExport) {
Expand Down Expand Up @@ -117,13 +111,12 @@ function nextTranslate(nextConfig: NextConfig = {}): NextConfig {
loader: 'next-translate-plugin/loader',
options: {
basePath,
pagesPath: path.join(pagesPath, '/'),
pagesPaths: pagesPaths.map((pagePath) => path.join(pagePath, '/')),
hasAppJs: Boolean(app),
hasGetInitialPropsOnAppJs,
hasLoadLocaleFrom: typeof restI18n.loadLocaleFrom === 'function',
extensionsRgx: restI18n.extensionsRgx || test,
revalidate: restI18n.revalidate || 0,
isAppDirNext13,
} as LoaderOptions,
},
})
Expand Down
35 changes: 25 additions & 10 deletions src/loader.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type webpack from 'webpack'

import templateWithHoc from './templateWithHoc'
import templateWithLoader from './templateWithLoader'
import templateWithHoc from './templateWithHoc'
import templateAppDir from './templateAppDir'
import {
parseFile,
getDefaultAppJs,
Expand All @@ -11,27 +12,31 @@ import {
isPageToIgnore,
hasHOC,
} from './utils'
import { removeCommentsFromCode } from './utils'
import { LoaderOptions } from './types'
import templateAppDir from './templateAppDir'

export default function loader(
this: webpack.LoaderContext<LoaderOptions>,
rawCode: string
) {
const {
basePath,
pagesPath,
pagesPaths,
hasAppJs,
hasGetInitialPropsOnAppJs,
hasLoadLocaleFrom,
extensionsRgx,
revalidate,
isAppDirNext13,
} = this.getOptions()

// Normalize slashes in a file path to be posix/unix-like forward slashes
const normalizedPagesPath = pagesPath.replace(/\\/g, '/')
const normalizedPagesPaths = pagesPaths.map((pagesPath) =>
pagesPath.replace(/\\/g, '/')
)
const normalizedResourcePath = this.resourcePath.replace(/\\/g, '/')
const normalizedPagesPath = normalizedPagesPaths.find((pagesPath) =>
normalizedResourcePath.startsWith(pagesPath)
)

// In case that there aren't /_app.js we want to overwrite the default _app
// to provide the I18Provider on top.
Expand All @@ -43,10 +48,8 @@ export default function loader(
return getDefaultAppJs(hasLoadLocaleFrom)
}

// Skip rest of files that are not inside /pages
if (!isAppDirNext13 && !normalizedResourcePath.startsWith(normalizedPagesPath)) return rawCode

const page = normalizedResourcePath.replace(normalizedPagesPath, '/')
const page = normalizedResourcePath.replace(normalizedPagesPath || '', '/')
const pageNoExt = page.replace(extensionsRgx, '')
const pagePkg = parseFile(basePath, normalizedResourcePath)
const defaultExport = getDefaultExport(pagePkg)
Expand All @@ -55,10 +58,22 @@ export default function loader(
// "export default" on the page
if (!defaultExport) return rawCode

if (isAppDirNext13) {
return templateAppDir(pagePkg, { hasLoadLocaleFrom, pageNoExt, normalizedResourcePath, normalizedPagesPath })
// In Next 13 "use client" components must be transpiled to include helpers
// even though it is tipically not inside a pages directory
const isClientComponent = /^['"]use client['"]/.test(removeCommentsFromCode(pagePkg.getCode()))

if (normalizedPagesPath?.endsWith('app/') || isClientComponent) {
return templateAppDir(pagePkg, {
hasLoadLocaleFrom,
pageNoExt,
normalizedResourcePath,
normalizedPagesPath,
})
}

// Skip files that are not inside a valid page dir
if (!normalizedPagesPath) return rawCode

// Skip any transformation if the page is not in raw code
// Fixes issue with Nx: https://github.com/vinissimus/next-translate/issues/677
if (hasExportName(pagePkg, '__N_SSP') || hasExportName(pagePkg, '__N_SSG')) {
Expand Down
5 changes: 2 additions & 3 deletions src/templateAppDir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,9 @@ function templateAppDirClientComponent({ code, hash, pageVariableName }: ClientT
export default function __Next_Translate_new__${hash}__(props) {
const forceUpdate = __react.useReducer(() => [])[1]
const isClient = typeof window !== 'undefined'
const el = isClient && document.getElementById('__NEXT_TRANSLATE_DATA__')

if (isClient && !window.__NEXT_TRANSLATE__) {
if (isClient && !window.__NEXT_TRANSLATE__ && el) {
window.__NEXT_TRANSLATE__ = { lang: __i18nConfig.defaultLocale, namespaces: {} }
update(false)
}
Expand All @@ -105,8 +106,6 @@ function templateAppDirClientComponent({ code, hash, pageVariableName }: ClientT
__react.useEffect(update)

function update(rerender = true) {
const el = document.getElementById('__NEXT_TRANSLATE_DATA__')

if (!el) return

const { lang, ns, pathname } = el.dataset
Expand Down
3 changes: 1 addition & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ import type ts from 'typescript'

export interface LoaderOptions {
basePath: string
pagesPath: string
pagesPaths: string[]
hasAppJs: boolean
hasGetInitialPropsOnAppJs: boolean
hasLoadLocaleFrom: boolean
extensionsRgx: RegExp
revalidate: number
isAppDirNext13: boolean
}

export type Transformer = (
Expand Down
14 changes: 14 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -576,3 +576,17 @@ export function interceptExport(
export function removeCommentsFromCode(code: string) {
return code.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '')
}

export const possiblePagesDirs = [
// Next 13 app dir
'app',
'src/app',
'app/app',
'integrations/app',

// https://github.com/blitz-js/blitz/blob/canary/nextjs/packages/next/build/utils.ts#L54-L59
'pages',
'src/pages',
'app/pages',
'integrations/pages',
] as const