Skip to content
Open
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
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@
"@miyaneee/rollup-plugin-json5": "^1.2.0",
"@nuxt/kit": "^4.1.2",
"@rollup/plugin-yaml": "^4.1.2",
"@vue/compiler-sfc": "^3.5.22",
"defu": "^6.1.4",
"devalue": "^5.1.1",
"h3": "^1.15.4",
Expand Down
3 changes: 0 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 32 additions & 34 deletions src/pages.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { addTemplate, updateTemplates } from '@nuxt/kit'
import { readFileSync } from 'node:fs'
import { isString } from '@intlify/shared'
import { parse as parseSFC } from '@vue/compiler-sfc'
import { parseAndWalk } from 'oxc-walker'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { getRoutePath, parseSegment } from './utils/route-parsing'
Expand All @@ -19,6 +18,7 @@ import type { I18nNuxtContext } from './context'
import type { ComputedRouteOptions, RouteOptionsResolver } from './kit/gen'
import type { I18nRoute } from './runtime/composables'
import { parseSync, type CallExpression, type ExpressionStatement, type ObjectExpression } from 'oxc-parser'
import { extractScriptContent } from './utils/extract-script'

export class NuxtPageAnalyzeContext {
config: NuxtI18nOptions['pages']
Expand Down Expand Up @@ -389,47 +389,45 @@ function getI18nRouteConfig(absolutePath: string, vfs: Record<string, string> =
const content = absolutePath in vfs ? vfs[absolutePath]! : readFileSync(absolutePath, 'utf-8')
if (!content.includes(DEFINE_I18N_ROUTE_FN)) return undefined

const { descriptor } = parseSFC(content)
const blocks = extractScriptContent(content)

const script = descriptor.scriptSetup || descriptor.script
if (!script) return undefined
for (const script of blocks) {
if (extract != null) break
parseAndWalk(script.code, absolutePath.replace(/\.\w+$/, '.' + script.loader), (node) => {
if (extract != null) return
let code = script.code

const lang = typeof script.attrs.lang === 'string' && /j|tsx/.test(script.attrs.lang) ? 'tsx' : 'ts'
let code = script.content

parseAndWalk(script.content, absolutePath.replace(/\.\w+$/, '.' + lang), (node) => {
if (extract != null) return

if (
node.type !== 'CallExpression'
|| node.callee.type !== 'Identifier'
|| node.callee.name !== DEFINE_I18N_ROUTE_FN
)
return
if (
node.type !== 'CallExpression'
|| node.callee.type !== 'Identifier'
|| node.callee.name !== DEFINE_I18N_ROUTE_FN
)
return

let routeArgument = node.arguments[0]
if (routeArgument == null) return
let routeArgument = node.arguments[0]
if (routeArgument == null) return

if (typeof script.attrs.lang === 'string' && /tsx?/.test(script.attrs.lang)) {
const transformed = transform('', script.content.slice(node.start, node.end).trim(), { lang })
code = transformed.code
if (typeof script.loader === 'string' && /tsx?/.test(script.loader)) {
const transformed = transform('', script.code.slice(node.start, node.end).trim(), { lang: script.loader })
code = transformed.code

if (transformed.errors.length) {
for (const error of transformed.errors) {
console.warn(`Error while transforming \`${DEFINE_I18N_ROUTE_FN}()\`` + error.codeframe)
if (transformed.errors.length) {
for (const error of transformed.errors) {
console.warn(`Error while transforming \`${DEFINE_I18N_ROUTE_FN}()\`` + error.codeframe)
}
return
}
return
}

// we already know that the first statement is a call expression
routeArgument = (
(parseSync('', transformed.code, { lang: 'js' }).program.body[0]! as ExpressionStatement)
.expression as CallExpression
).arguments[0]! as ObjectExpression
}
// we already know that the first statement is a call expression
routeArgument = (
(parseSync('', transformed.code, { lang: 'js' }).program.body[0]! as ExpressionStatement)
.expression as CallExpression
).arguments[0]! as ObjectExpression
}

extract = evalAndValidateValue(code.slice(routeArgument.start, routeArgument.end).trim())
})
extract = evalAndValidateValue(code.slice(routeArgument.start, routeArgument.end).trim())
})
}
}
catch (e: unknown) {
console.warn(`[nuxt-i18n] Couldn't read component data at ${absolutePath}: (${(e as Error).message})`)
Expand Down
45 changes: 22 additions & 23 deletions src/transform/macros.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,19 @@

import MagicString from 'magic-string'
import { createUnplugin } from 'unplugin'
import { parse as parseSFC } from '@vue/compiler-sfc'
import { VIRTUAL_PREFIX_HEX, isVue } from './utils'
import { DEFINE_I18N_ROUTE_FN } from '../constants'

import type { BundlerPluginOptions } from './utils'
import { parseAndWalk, ScopeTracker, walk } from 'oxc-walker'

const I18N_MACRO_FN_RE = new RegExp(`\\b${DEFINE_I18N_ROUTE_FN}\\s*\\(\\s*`)

/**
* TODO:
* `paths`, `locales` completions like `unplugin-vue-router`
* ref: https://github.com/posva/unplugin-vue-router
*/
export const TransformMacroPlugin = (options: BundlerPluginOptions) =>
createUnplugin(() => {
return {
name: 'nuxtjs:i18n-macros-transform',
enforce: 'pre',
enforce: 'post',

transformInclude(id) {
if (!id || id.startsWith(VIRTUAL_PREFIX_HEX)) {
Expand All @@ -39,25 +34,29 @@ export const TransformMacroPlugin = (options: BundlerPluginOptions) =>
filter: {
code: { include: I18N_MACRO_FN_RE },
},
handler(code) {
const parsed = parseSFC(code, { sourceMap: false })
// only transform <script>
const script = parsed.descriptor.scriptSetup ?? parsed.descriptor.script
if (!script) {
return
}

handler(code, id) {
const s = new MagicString(code)

// match content inside <script>
const match = script.content.match(I18N_MACRO_FN_RE)
if (match?.[0]) {
// tree-shake out any runtime references to the macro.
const scriptString = new MagicString(script.content)
scriptString.overwrite(match.index!, match.index! + match[0].length, `false && /*#__PURE__*/ ${match[0]}`)
try {
// Parse and collect scope information
const scopeTracker = new ScopeTracker({ preserveExitedScopes: true })
const parseResult = parseAndWalk(code, id, { scopeTracker })
scopeTracker.freeze()

walk(parseResult.program, {
scopeTracker,
enter(node) {
if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier') return

// using the locations from the parsed result we only replace the <script> contents
s.overwrite(script.loc.start.offset, script.loc.end.offset, scriptString.toString())
const name = node.callee.name
if (name !== DEFINE_I18N_ROUTE_FN) return
s.overwrite(node.start, node.end, ` false && /*@__PURE__*/ ${DEFINE_I18N_ROUTE_FN}${code.slice(node.callee.end, node.end)}`)
this.skip()
},
})
}
catch (e) {
console.error(e)
}

if (s.hasChanged()) {
Expand Down
14 changes: 14 additions & 0 deletions src/utils/extract-script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const SFC_SCRIPT_RE = /<script(?<attrs>[^>]*)>(?<content>[\s\S]*?)<\/script[^>]*>/gi
export function extractScriptContent(sfc: string) {
const contents: Array<{ loader: 'ts' | 'tsx', code: string }> = []
for (const match of sfc.matchAll(SFC_SCRIPT_RE)) {
if (match?.groups?.content) {
contents.push({
loader: match.groups.attrs && /[tj]sx/.test(match.groups.attrs) ? 'tsx' : 'ts',
code: match.groups.content.trim(),
})
}
}

return contents
}
Loading