Skip to content

Commit 5ddcffe

Browse files
committed
feat: improve performance when building frontend routing yml
1 parent 6993dce commit 5ddcffe

File tree

10 files changed

+262
-156
lines changed

10 files changed

+262
-156
lines changed
File renamed without changes.

build.config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,13 @@ export default defineBuildConfig({
1313
'webpack-sources',
1414
'webpack-virtual-modules',
1515
'@jridgewell/sourcemap-codec',
16+
'graphql',
17+
'nuxt-graphql-middleware/utils',
18+
/#vuepal-build/,
19+
/#nuxt-graphql-middleware/,
20+
/#graphql-operations/,
1621
],
22+
replace: {
23+
'process.env.PLAYGROUND_MODULE_BUILD': 'undefined',
24+
},
1725
})

playground/nuxt.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export default defineNuxtConfig({
2525
},
2626

2727
experimental: {
28-
scanPageMeta: true,
28+
scanPageMeta: 'after-resolve',
2929
},
3030

3131
languageNegotiation: {

playground/pages/contact/index.vue

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<div>
3-
<h1>This is the contact page, a static node.</h1>
3+
<h1>This is the contact page, a static node asdf.</h1>
44
</div>
55
</template>
66

@@ -9,12 +9,14 @@ import { definePageMeta } from '#imports'
99
1010
definePageMeta({
1111
name: 'contact-page',
12-
drupalFrontendRoute: true,
12+
meta: {
13+
drupalFrontendRoute: true,
14+
},
1315
languageMapping: {
1416
de: '/de/kontakt',
1517
fr: '/fr/contactez-nous',
1618
en: '/en/contact-us',
17-
it: '/it/contactioasdfasdf',
19+
it: '/it/conatactio',
1820
},
1921
})
2022
</script>

src/build/classes/FileCache.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export class FileCache<T> {
2+
private cache: Map<string, T> = new Map()
3+
4+
public get(filePath: string): T | undefined {
5+
return this.cache.get(filePath)
6+
}
7+
8+
public set(filePath: string, value: T): void {
9+
this.cache.set(filePath, value)
10+
}
11+
12+
public clear(filePath: string): void {
13+
this.cache.delete(filePath)
14+
}
15+
}

src/build/classes/ModuleHelper.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { relative } from 'pathe'
1313
import type { Nuxt, ResolvedNuxtTemplate } from 'nuxt/schema'
1414
import { fileExists, logger } from '../helpers'
1515
import { useGraphqlModuleContext } from 'nuxt-graphql-middleware/utils'
16+
import { FileCache } from './FileCache'
1617

1718
type GraphqlModuleContext = NonNullable<
1819
ReturnType<typeof useGraphqlModuleContext>
@@ -72,6 +73,8 @@ export class ModuleHelper {
7273

7374
public readonly graphql: GraphqlModuleContext
7475

76+
public readonly caches: FileCache<unknown>[] = []
77+
7578
constructor(
7679
public nuxt: Nuxt,
7780
moduleUrl: string,
@@ -355,4 +358,14 @@ export class ModuleHelper {
355358
)
356359
this.graphql.addImportFile(resolved)
357360
}
361+
362+
public createFileCache<T>(): FileCache<T> {
363+
const cache = new FileCache<T>()
364+
this.caches.push(cache)
365+
return cache
366+
}
367+
368+
public clearFilePathCaches(filePath: string) {
369+
this.caches.forEach((cache) => cache.clear(filePath))
370+
}
358371
}
Lines changed: 124 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import fs from 'node:fs'
22
import { addTemplate } from '@nuxt/kit'
3-
import { stringify } from 'yaml'
43
import type { NuxtPage } from '@nuxt/schema'
5-
import { nonNullable } from './../../../runtime/helpers/type'
64
import { defineVuepalFeature } from '../defineFeature'
5+
import type { FileCache } from '../../classes/FileCache'
6+
import type { ModuleHelper } from '../../classes/ModuleHelper'
7+
import { logger } from '../../helpers'
8+
9+
function nonNullable<T>(value: T): value is NonNullable<T> {
10+
return value !== null && value !== undefined
11+
}
712

813
/**
914
* Extracts the language mapping.
@@ -20,97 +25,111 @@ const extractLanguageMapping = (
2025
return
2126
}
2227

23-
try {
24-
const jsonString = `{${match.trim().replace(/'/g, '"')}}`
28+
const jsonString = `{${match.trim().replace(/'/g, '"')}}`
2529

26-
const mapping = eval(`(${jsonString})`)
27-
if (typeof mapping !== 'object') {
30+
const fn = new Function(`return ${jsonString}`)
31+
const mapping = fn()
32+
if (typeof mapping !== 'object') {
33+
return
34+
}
35+
36+
for (const key in mapping) {
37+
if (typeof key !== 'string') {
2838
return
2939
}
3040

31-
for (const key in mapping) {
32-
if (typeof key !== 'string') {
33-
return
34-
}
35-
36-
const value = mapping[key]
41+
const value = mapping[key]
3742

38-
if (typeof value !== 'string') {
39-
return
40-
}
43+
if (typeof value !== 'string') {
44+
return
4145
}
42-
43-
return mapping
44-
} catch (e) {
45-
console.log('Error in Vuepal:')
46-
console.log(e)
4746
}
48-
}
4947

50-
type ExtractedDrupalFrontendRoute = {
51-
aliases: Record<string, string>
52-
path: string
53-
name: string
48+
return mapping
5449
}
5550

56-
type DrupalFrontendRouteEntry = {
57-
aliases: Record<string, string>
51+
type ExtractedPage = {
52+
filePath: string
53+
isDrupalFrontendRoute: boolean
54+
yml?: string
5855
}
5956

60-
const extractFrontendRouteData = async (
61-
page: NuxtPage,
62-
isSingleLanguage: boolean,
63-
): Promise<ExtractedDrupalFrontendRoute | undefined> => {
64-
if (!page.file || !page.name) {
65-
return
66-
}
67-
const code = await fs.promises.readFile(page.file).then((v) => v.toString())
57+
class PageCollector {
58+
private cache: FileCache<ExtractedPage>
59+
private templateContents = ''
6860

69-
if (!code.includes('drupalFrontendRoute')) {
70-
return
61+
constructor(
62+
helper: ModuleHelper,
63+
private langcodes: string[],
64+
) {
65+
this.cache = helper.createFileCache()
7166
}
7267

73-
const aliases = extractLanguageMapping(code)
74-
if (!aliases && !isSingleLanguage) {
75-
return
76-
}
77-
return {
78-
path: page.path,
79-
name: page.name,
80-
aliases: aliases || {},
81-
}
82-
}
68+
private async handlePage(page: NuxtPage): Promise<ExtractedPage | undefined> {
69+
if (!page.file) {
70+
return
71+
}
72+
73+
if (!page.name) {
74+
return
75+
}
76+
77+
const name = page.name
78+
79+
const cached = this.cache.get(page.file)
80+
if (cached) {
81+
return cached
82+
}
8383

84-
const generateFrontendRoutesYaml = (
85-
pages: NuxtPage[],
86-
langcodes: string[],
87-
): Promise<string> => {
88-
const isSingleLanguage = langcodes.length === 1
89-
return Promise.all(
90-
pages.map((v) => extractFrontendRouteData(v, isSingleLanguage)),
91-
).then((routes) => {
92-
const sortedRoutes = routes
93-
.filter(nonNullable)
94-
.sort((a, b) => a.name.localeCompare(b.name))
95-
const keys = sortedRoutes.reduce<Record<string, DrupalFrontendRouteEntry>>(
96-
(acc, v) => {
97-
const allLangcodes: Record<string, string> = langcodes.reduce<
98-
Record<string, string>
99-
>((acc, langcode) => {
100-
acc[langcode] = v.aliases[langcode] || v.path
101-
return acc
102-
}, {})
103-
acc[v.name] = {
104-
aliases: allLangcodes,
84+
const contents = await fs.promises
85+
.readFile(page.file)
86+
.then((v) => v.toString())
87+
88+
const extracted: ExtractedPage = {
89+
filePath: page.file,
90+
isDrupalFrontendRoute: false,
91+
}
92+
93+
if (contents.includes('drupalFrontendRoute')) {
94+
extracted.isDrupalFrontendRoute = true
95+
try {
96+
const languageMapping = extractLanguageMapping(contents)
97+
if (languageMapping) {
98+
const mapping = Object.entries(languageMapping)
99+
.map(([langcode, path]) => {
100+
return ` ${langcode}: '${path}'`
101+
})
102+
.join('\n')
103+
extracted.yml = ` ${name}:\n aliases:\n${mapping}`
105104
}
105+
} catch (e) {
106+
logger.warn(
107+
`Failed to extract language mapping in page "${page.file}"`,
108+
e,
109+
)
110+
}
111+
}
112+
113+
this.cache.set(page.file, extracted)
114+
return extracted
115+
}
106116

107-
return acc
108-
},
109-
{},
117+
public async handlePages(pages: NuxtPage[]) {
118+
const mapped = await Promise.all(
119+
pages.map((page) => this.handlePage(page)),
120+
).then((result) =>
121+
result
122+
.map((v) => v?.yml)
123+
.filter(nonNullable)
124+
.sort(),
110125
)
111126

112-
return stringify({ keys }, { sortMapEntries: true })
113-
})
127+
this.templateContents = `keys:\n${mapped.join('\n')}`
128+
}
129+
130+
getTemplateContents(): string {
131+
return this.templateContents
132+
}
114133
}
115134

116135
export default defineVuepalFeature<{
@@ -129,24 +148,6 @@ export default defineVuepalFeature<{
129148
name: 'frontendRouting',
130149
description: '',
131150
setup(helper, options) {
132-
if (!helper.isModuleBuild && options?.outputPath) {
133-
let templateContents = ''
134-
const templatePath = helper.resolvers.root.resolve(options.outputPath)
135-
136-
addTemplate({
137-
filename: templatePath,
138-
write: true,
139-
getContents: (ctx) => {
140-
return ''
141-
},
142-
})
143-
}
144-
145-
// function buildTemplateContents() {
146-
// const pages: NuxtPage[] = ctx.app.pages || []
147-
// return generateFrontendRoutesYaml(pages, options.langcodes)
148-
// }
149-
150151
helper.addTemplate('page-meta', null, () => {
151152
return `
152153
declare module "#app" {
@@ -155,7 +156,7 @@ declare module "#app" {
155156
* If set to true, this route is considered a "Drupal Frontend Route".
156157
* It will generate an entry in the frontend_routing.settings.yml file.
157158
*
158-
* This will make sure that the node connected to this route will always
159+
* This will make sure that the node connected to this Nuxt page will always
159160
* have the paths defined in this component. It will not be possible to
160161
* override the path in Drupal.
161162
*/
@@ -166,5 +167,36 @@ declare module "#app" {
166167
export {}
167168
`
168169
})
170+
171+
if (helper.isModuleBuild) {
172+
return
173+
}
174+
175+
const outputPath = options?.outputPath
176+
const langcodes = options?.langcodes
177+
if (!outputPath) {
178+
throw new Error(`Missing required option "frontendRouting.outputPath".`)
179+
}
180+
181+
if (!langcodes?.length) {
182+
throw new Error(`Missing required option "frontendRouting.langcodes".`)
183+
}
184+
185+
const collector = new PageCollector(helper, langcodes)
186+
187+
addTemplate({
188+
filename: helper.resolvers.root.resolve(options.outputPath),
189+
write: true,
190+
getContents: () => collector.getTemplateContents(),
191+
})
192+
193+
// This hook is called by Nuxt when any page changes.
194+
// During development, the hook is called *after* the builder:watch
195+
// event.
196+
helper.nuxt.hooks.hook('pages:resolved', async (pages) => {
197+
helper.logDebug('frontendRouting: pages:resolved start')
198+
await collector.handlePages(pages)
199+
helper.logDebug('frontendRouting: pages:resolved done')
200+
})
169201
},
170202
})

0 commit comments

Comments
 (0)