diff --git a/packages/amagaki-plugin-partial-library/example/amagaki.ts b/packages/amagaki-plugin-partial-library/example/amagaki.ts index 66363635..7fe8e8ad 100644 --- a/packages/amagaki-plugin-partial-library/example/amagaki.ts +++ b/packages/amagaki-plugin-partial-library/example/amagaki.ts @@ -13,12 +13,18 @@ export default async (pod: Pod) => { }, staticRoutes: [ { - path: `/static/`, + path: '/static/', staticDir: '/dist/', }, ], }); - - PartialLibraryPlugin.register(pod, {}) + PartialLibraryPlugin.register(pod, { + partial: { + tracked: ['spacer'], + }, + serving: { + template: '/views/library.njk', + }, + }); }; diff --git a/packages/amagaki-plugin-partial-library/example/views/base.njk b/packages/amagaki-plugin-partial-library/example/views/base.njk index 3c25221a..ba9f2711 100644 --- a/packages/amagaki-plugin-partial-library/example/views/base.njk +++ b/packages/amagaki-plugin-partial-library/example/views/base.njk @@ -9,15 +9,17 @@
- {% set partial = pod.doc('/content/partials/header.yaml').fields %} - {% include "/views/partials/header.njk" %} - {% if doc.fields.partials %} - {% asyncEach partial in doc.fields.partials %} - {% include "/views/partials/" ~ partial.partial ~ ".njk" %} - {% endeach %} - {% else %} -
- {{doc.body|markdown|safe}} -
- {% endif %} + {% block main %} + {% set partial = pod.doc('/content/partials/header.yaml').fields %} + {% include "/views/partials/header.njk" %} + {% if doc.fields.partials %} + {% asyncEach partial in doc.fields.partials %} + {% include "/views/partials/" ~ partial.partial ~ ".njk" %} + {% endeach %} + {% else %} +
+ {{doc.body|markdown|safe}} +
+ {% endif %} + {% endblock %}
diff --git a/packages/amagaki-plugin-partial-library/example/views/library.njk b/packages/amagaki-plugin-partial-library/example/views/library.njk new file mode 100644 index 00000000..430e63fa --- /dev/null +++ b/packages/amagaki-plugin-partial-library/example/views/library.njk @@ -0,0 +1,42 @@ +{% extends "/views/base.njk" %} + +{% block main %} + + {% if doc.fields.partial %} + {% set trackedPartial = doc.fields.partials[doc.fields.partial] %} + +

{{ doc.fields.partial }}

+ +
+
Instance count ({{trackedPartial.length}}) split by locale
+ {% for locale, localeLen in trackedPartial.lengthByLocale %} +
{{locale}}: {{localeLen}}
+ {% endfor %} +
+ + {% for partialInstance in trackedPartial.instances %} + {# Only show if we have the config. #} + {% if partialInstance.config %} + {% if loop.index > 1 %} +
+ {% endif %} + +
+ {% if partialInstance.urlPath %} + {{ partialInstance.urlPath }} + {% endif %} + {% if partialInstance.locale %} + ({{partialInstance.locale.id}}) + {% endif %} +
+ + {% set partial = partialInstance.config %} + {% include "/views/partials/" ~ doc.fields.partial ~ ".njk" %} + {% endif %} + {% endfor %} + {% endif %} +{% endblock %} diff --git a/packages/amagaki-plugin-partial-library/src/partial-library-templates.ts b/packages/amagaki-plugin-partial-library/src/partial-library-templates.ts deleted file mode 100644 index e0b61169..00000000 --- a/packages/amagaki-plugin-partial-library/src/partial-library-templates.ts +++ /dev/null @@ -1,58 +0,0 @@ -export const libraryIndexTemplate = ` - - - - - - Partial library - - - -

Partial Library

- - - -`; - -export const libraryPartialTemplate = ` - - - - - - Partial: {{partial.key}} - - - -

Partial Library

- - - -

Partial: {{partial.key}}

- -
-
Instance count
-
{{partial.length}}
-
- -`; diff --git a/packages/amagaki-plugin-partial-library/src/partial-library.ts b/packages/amagaki-plugin-partial-library/src/partial-library.ts index ca2c14d7..408c8168 100644 --- a/packages/amagaki-plugin-partial-library/src/partial-library.ts +++ b/packages/amagaki-plugin-partial-library/src/partial-library.ts @@ -1,41 +1,44 @@ -import * as fsPath from 'path'; - import { - Artifact, Builder, - CreatedPath, + DocumentRoute, + Locale, + NunjucksTemplateEngine, PluginComponent, Pod, - TemplateEngineRenderResult, + Route, + RouteProvider, + Router, } from '@amagaki/amagaki'; -import { - libraryIndexTemplate, - libraryPartialTemplate, -} from './partial-library-templates'; -export interface PartialLibraryPluginOptions { +export interface PartialLibraryPluginConfig { /** - * Options for how the plugin acts during the build process. + * Options for how the document is parsed for gathering partials. */ - build?: { + document?: { /** - * Loading bar label for build process. + * Document field key to use for finding the partials. * - * @default 'Partials Library' + * @default 'partials' */ - loadingLabel?: string; + key?: string; }; /** - * Options for how the document is parsed for gathering partials. + * Options for how the partials are parsed. */ - document?: { + parsing?: { /** - * Document field key to use for finding the partials. + * Directory containing the partials definitions. * - * @default 'partials' + * @default '/views/partials/' */ - key?: string; + partialDirectory?: string; + /** + * Partial directory contains sub directories of partials. + * + * @default false + */ + partialsInSubDirectories?: boolean; }; /** @@ -62,21 +65,6 @@ export interface PartialLibraryPluginOptions { tracked?: string[]; }; - /** - * Options for how the plugin renders the library. - */ - rendering?: { - /** - * View to use for rendering the library. - * - * Used to determine the template engine to use. - * - * If no view is provided, renders the library using nunjucks without - * styling or other project specific styling. - */ - view?: string; - }; - /** * Options for how the partials are served during build. */ @@ -90,29 +78,26 @@ export interface PartialLibraryPluginOptions { /** * Template to use when building the library. * - * @default '/views/base.njk' + * If not provided a default, styleless template will be used. */ template?: string; + /** + * Title for the partial library. + * + * @default 'Partial Library' + */ + title?: string; }; } -/** - * Context used for rendering the partial library. - */ -export interface PartialLibraryContext { - pod: Pod; - partials: Record; - partial?: PartialLibraryPartial; - pathPrefix: string; -} - /** * Track instances information for a partial. */ export class PartialLibraryInstance { constructor( public readonly config?: Record, - public readonly fileName?: string + public readonly urlPath?: string, + public readonly locale?: Locale ) {} } @@ -124,13 +109,29 @@ export class PartialLibraryPartial { constructor(public readonly key: string) {} - addInstance(config?: Record, fileName?: string) { - this.instances.push(new PartialLibraryInstance(config, fileName)); + addInstance(config?: Record, urlPath?: string, locale?: Locale) { + this.instances.push(new PartialLibraryInstance(config, urlPath, locale)); } get length() { return this.instances.length; } + + get lengthByLocale(): Record { + const result: Record = {}; + + for (const instance of this.instances) { + const localeKey = instance.locale?.id ?? 'default'; + + if (!result[localeKey]) { + result[localeKey] = 0; + } + + result[localeKey]++; + } + + return result; + } } /** @@ -146,20 +147,42 @@ export class PartialLibraryPlugin implements PluginComponent { */ static register( pod: Pod, - options?: PartialLibraryPluginOptions - ): PartialLibraryPluginOptions | undefined { - pod.plugins.register(PartialLibraryPlugin, options); + config: PartialLibraryPluginConfig + ): PartialLibraryPluginConfig { + // Ignore the library on prod. + if (pod.env.name === 'prod') { + return undefined; + } + + pod.plugins.register(PartialLibraryPlugin, config ?? {}); + return config; + } - return options; + constructor(public pod: Pod, public config: PartialLibraryPluginConfig) { + // Add the route provider for the partial library. + const provider = new PartialLibraryRouteProvider( + this.pod.router, + this.config + ); + pod.router.addProvider(provider); } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async beforeBuildHook(builder: Builder) { + // Stub to be recognized as a plugin component. + } +} + +class PartialLibraryPartialTracker { partials: Record = {}; - constructor(public pod: Pod, public config: PartialLibraryPluginOptions) {} + constructor(public config: PartialLibraryPluginConfig) {} addPartialInstance( key: string, partialConfig: Record, - fileName?: string + urlPath?: string, + locale?: Locale ) { if (!(key in this.partials)) { this.partials[key] = new PartialLibraryPartial(key); @@ -172,185 +195,172 @@ export class PartialLibraryPlugin implements PluginComponent { // Track only (no config) for the tracked partials. if (this.config.partial?.tracked?.includes(key)) { - this.partials[key].addInstance(undefined, fileName); + this.partials[key].addInstance(undefined, urlPath, locale); return; } - this.partials[key].addInstance(partialConfig, fileName); + this.partials[key].addInstance(partialConfig, urlPath, locale); } +} - /** - * Hook for finding all of the partial usage when rendering. - */ - async afterRenderHook(result: TemplateEngineRenderResult): Promise { - const docPartials = - result.context.doc?.fields[this.config.document?.key ?? 'partials'] ?? []; - - for (const partialConfig of docPartials) { - const partialKey = this.config.partial?.key ?? 'partial'; - const partial = partialConfig[partialKey]; - this.addPartialInstance(partial, partialConfig, result.path); +/** + * Custom route provider for the partial library. + */ +class PartialLibraryRouteProvider extends RouteProvider { + constructor(router: Router, public config: PartialLibraryPluginConfig) { + super(router); + this.type = 'partialLibrary'; + } + + async routes() { + const routes = []; + const partialDirectory = + this.config.parsing?.partialDirectory ?? '/views/partials'; + const podPaths = this.pod.walk(partialDirectory); + const partials: string[] = []; + for (const podPath of podPaths) { + const useSubDirectories = + this.config.parsing?.partialsInSubDirectories ?? false; + + // Some pods use the directory as the name of the partial. + // Ex: /src/partials//... + if (useSubDirectories) { + const dirName = podPath.replace(partialDirectory, '').split('/')[0]; + partials.push(dirName); + } else { + partials.push(podPath.split('/').pop().split('.')[0]); + } } + routes.push(new PartialLibraryRoute(this, this.config, {})); + partials.forEach(partial => { + routes.push( + new PartialLibraryReviewRoute(this, this.config, { + partial: partial, + }) + ); + }); + return routes; } +} - /** - * Hook for generating the library files before the manifest is generated. - * - * This allows for all of the document partials to be parsed and tracked - * before generating the library output and still be included in the - * build manifest for deployment. - * - * @param builder Builder instance - */ - async beforeBuildManifestHook( - builder: Builder, - createdPaths: Array, - artifacts: Array - ): Promise { - const partialKeys = Object.keys(this.partials).sort(); - const bar = Builder.createProgressBar( - this.config?.build?.loadingLabel ?? 'Partial library' - ); - const startTime = new Date().getTime(); +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface PartialLibraryRouteOptions {} - // Pages for all partials plus main library index. - bar.start(partialKeys.length + 1, 0, { - customDuration: Builder.formatProgressBarTime(0), - }); +interface PartialLibraryReviewRouteOptions extends PartialLibraryRouteOptions { + partial: string; +} - const pathPrefix = this.config.serving?.pathPrefix ?? '/library/'; +class PartialLibraryRoute extends Route { + constructor( + public provider: RouteProvider, + public config: PartialLibraryPluginConfig, + public options: PartialLibraryRouteOptions + ) { + super(provider); + } - // Add the main library page. - const normalPath = Builder.normalizePath(pathPrefix); - const tempPath = fsPath.join( - builder.tempDirRoot, - builder.outputDirectoryPodPath, - normalPath - ); - const realPath = this.pod.getAbsoluteFilePath( - fsPath.join(builder.outputDirectoryPodPath, normalPath) - ); + get path() { + return this.urlPath; + } - const createdPath = { - tempPath, - normalPath, - realPath, - }; + get urlPath() { + return this.urlPathBase; + } - // TODO: Use a nunjucks template. - const timer = this.pod.profiler.timer( - 'library.render', - 'Partial Library render' - ); + get urlPathBase() { + return this.config.serving?.pathPrefix ?? '/library/'; + } - try { - const context = { - pod: this.pod, - partials: this.partials, - pathPrefix, - }; - if (this.config.rendering?.view) { - const templateEngine = this.pod.engines.getEngineByFilename( - this.config.rendering.view - ); - const content = await templateEngine.render( - this.config.rendering.view, - context - ); - await builder.writeFileAsync(createdPath.tempPath, content); - } else { - const templateEngine = - this.pod.engines.getEngineByFilename('library.njk'); - const content = await templateEngine.renderFromString( - libraryIndexTemplate, - context + // Search through the routes to find all documents and pull out the partials. + async trackPartials(): Promise { + const tracker = new PartialLibraryPartialTracker(this.config); + const routes = await this.pod.router.routes(); + + // Routes are not sorted by default, but we want to sort the partial usage + // by path. + const pathToRoute: Record = {}; + + for (const route of routes) { + if (route.provider.type === 'collection' && route.urlPath) { + pathToRoute[route.urlPath] = route as DocumentRoute; + } + } + + // Sort the routes by path and find all partials. + for (const urlPath of Object.keys(pathToRoute).sort()) { + const docRoute = pathToRoute[urlPath]; + const docPartials = + docRoute.doc.fields[this.config.document?.key ?? 'partials'] ?? []; + + for (const partialConfig of docPartials) { + const partialKey = this.config.partial?.key ?? 'partial'; + const partial = partialConfig[partialKey]; + tracker.addPartialInstance( + partial, + partialConfig, + docRoute.urlPath, + docRoute.locale ); - await builder.writeFileAsync(createdPath.tempPath, content); } - } finally { - timer.stop(); } - createdPaths.push(createdPath); - artifacts.push({ - tempPath: createdPath.tempPath, - realPath: createdPath.realPath, - }); + return tracker; + } - bar.increment({ - customDuration: Builder.formatProgressBarTime( - new Date().getTime() - startTime - ), + async build() { + const tracker = await this.trackPartials(); + return await this.buildFake({ + partials: tracker.partials, }); + } - // Add each partial library page. - for (const partialKey of partialKeys) { - const partial = this.partials[partialKey]; - - const partialPath = `${pathPrefix}${partialKey}/`; - const normalPath = Builder.normalizePath(partialPath); - const tempPath = fsPath.join( - builder.tempDirRoot, - builder.outputDirectoryPodPath, - normalPath - ); - const realPath = this.pod.getAbsoluteFilePath( - fsPath.join(builder.outputDirectoryPodPath, normalPath) - ); - const createdPath = { - tempPath: tempPath, - normalPath: normalPath, - realPath: realPath, - }; - - // TODO: Use a nunjucks template. - const timer = this.pod.profiler.timer( - 'library.render', - 'Partial Library render' - ); + async buildFake(fields: Record) { + const fakeDoc = { + fields: { + ...fields, + title: this.config.serving?.title ?? 'Partial Library', + library: { + url: { + path: this.urlPathBase, + }, + }, + }, + locale: this.pod.locale('en'), + url: { + path: this.urlPath, + }, + }; + const template = this.config.serving?.template ?? '/views/base.njk'; + const engine = this.provider.pod.engines.getEngineByFilename( + template + ) as NunjucksTemplateEngine; + return await engine.render(template, { + doc: fakeDoc, + env: this.provider.pod.env, + pod: this.provider.pod, + process: process, + }); + } +} - try { - const context = { - pod: this.pod, - partial: partial, - partials: this.partials, - pathPrefix, - }; - if (this.config.rendering?.view) { - const templateEngine = this.pod.engines.getEngineByFilename( - this.config.rendering.view - ); - const content = await templateEngine.render( - this.config.rendering.view, - context - ); - await builder.writeFileAsync(createdPath.tempPath, content); - } else { - const templateEngine = - this.pod.engines.getEngineByFilename('library.njk'); - const content = await templateEngine.renderFromString( - libraryPartialTemplate, - context - ); - await builder.writeFileAsync(createdPath.tempPath, content); - } - } finally { - timer.stop(); - } +class PartialLibraryReviewRoute extends PartialLibraryRoute { + constructor( + public provider: RouteProvider, + public config: PartialLibraryPluginConfig, + public options: PartialLibraryReviewRouteOptions + ) { + super(provider, config, options); + } - createdPaths.push(createdPath); - artifacts.push({ - tempPath: createdPath.tempPath, - realPath: createdPath.realPath, - }); - - bar.increment({ - customDuration: Builder.formatProgressBarTime( - new Date().getTime() - startTime - ), - }); - } + get urlPath() { + return `${this.urlPathBase}${this.options.partial}/`; + } - bar.stop(); + async build() { + const tracker = await this.trackPartials(); + return await this.buildFake({ + partials: tracker.partials, + partial: this.options.partial, + }); } } diff --git a/packages/amagaki/src/builder.ts b/packages/amagaki/src/builder.ts index 9bd29769..edeefd7e 100644 --- a/packages/amagaki/src/builder.ts +++ b/packages/amagaki/src/builder.ts @@ -7,7 +7,7 @@ import * as stream from 'stream'; import * as util from 'util'; import * as utils from './utils'; -import { GitCommit, getGitData } from './gitData'; +import {GitCommit, getGitData} from './gitData'; import {Route, StaticRoute} from './router'; import {Pod} from './pod'; @@ -124,7 +124,7 @@ export class Builder { this.outputDirectoryPodPath, '.tmp', `amagaki-build-${(Math.random() + 1).toString(36).substring(6)}` - ) + ); } static normalizePath(path: string) { @@ -210,7 +210,10 @@ export class Builder { deleteOutputFiles(paths: Array, outputRootDir: string) { paths.forEach(outputPath => { // Delete the file. - const absOutputPath = fsPath.join(outputRootDir, outputPath.replace(/^\//, '')); + const absOutputPath = fsPath.join( + outputRootDir, + outputPath.replace(/^\//, '') + ); try { fs.unlinkSync(absOutputPath); } catch (err: any) { @@ -305,17 +308,21 @@ export class Builder { } async export(options: ExportOptions): Promise { - const buildDir = options.buildDir ?? this.pod.getAbsoluteFilePath(this.outputDirectoryPodPath); - const buildManifestPath = fsPath.join(buildDir, '.amagaki', 'manifest.json'); + const buildDir = + options.buildDir ?? + this.pod.getAbsoluteFilePath(this.outputDirectoryPodPath); + const buildManifestPath = fsPath.join( + buildDir, + '.amagaki', + 'manifest.json' + ); const exportManifestPath = options.exportControlDir ? fsPath.join(options.exportControlDir, 'manifest.json') : fsPath.join(options.exportDir, '.amagaki', 'manifest.json'); const buildManifest = this.getManifest(buildManifestPath); const exportManifest = this.getManifest(exportManifestPath); if (!buildManifest) { - throw new Error( - `Could not find build manifest at ${buildManifestPath}.` - ); + throw new Error(`Could not find build manifest at ${buildManifestPath}.`); } const filesToExport = buildManifest.files; @@ -327,7 +334,7 @@ export class Builder { edits: [], noChanges: [], deletes: [], - } + }; if (!existingFiles) { result.adds = buildManifest.files.map(pathSha => pathSha.path); } else { @@ -358,13 +365,24 @@ export class Builder { } if (exportManifest?.commit) { - console.log(chalk.yellow(`Previous export:`), `${exportManifest.built} by ${exportManifest.commit.author.email} (${exportManifest.commit.sha.slice(0, 6)})`); + console.log( + chalk.yellow('Previous export:'), + `${exportManifest.built} by ${ + exportManifest.commit.author.email + } (${exportManifest.commit.sha.slice(0, 6)})` + ); } if (buildManifest?.commit) { - console.log(chalk.yellow(` Current build:`), `${buildManifest.built} by ${buildManifest.commit.author.email} (${buildManifest.commit.sha.slice(0, 6)})`); + console.log( + chalk.yellow(' Current build:'), + `${buildManifest.built} by ${ + buildManifest.commit.author.email + } (${buildManifest.commit.sha.slice(0, 6)})` + ); } - const numOperations = result.adds.length + result.edits.length + result.deletes.length; + const numOperations = + result.adds.length + result.edits.length + result.deletes.length; if (numOperations === 0) { console.log( chalk.blue('No changes since last export: ') + @@ -376,7 +394,6 @@ export class Builder { const moveBar = Builder.createProgressBar('Exporting'); const showMoveProgressBar = numOperations >= Builder.ShowMoveProgressBarThreshold; - const moveStartTime = new Date().getTime(); if (showMoveProgressBar) { moveBar.start(numOperations, 0, { customDuration: Builder.formatProgressBarTime(0), @@ -385,16 +402,18 @@ export class Builder { // Copy adds and edits. const moveFiles = [...result.adds, ...result.edits]; - await async.mapLimit(moveFiles, Builder.NumConcurrentCopies, async (filePath: string) => { - const relativePath = filePath.replace(/^\//, ''); - const source = fsPath.join(buildDir, relativePath); - const destination = fsPath.join(options.exportDir, relativePath);; - Builder.ensureDirectoryExists(destination); - moveBar.increment(); - return fs.promises.copyFile( - source, destination - ); - }); + await async.mapLimit( + moveFiles, + Builder.NumConcurrentCopies, + async (filePath: string) => { + const relativePath = filePath.replace(/^\//, ''); + const source = fsPath.join(buildDir, relativePath); + const destination = fsPath.join(options.exportDir, relativePath); + Builder.ensureDirectoryExists(destination); + moveBar.increment(); + return fs.promises.copyFile(source, destination); + } + ); // Delete deleted files. this.deleteOutputFiles(result.deletes, options.exportDir); @@ -562,14 +581,6 @@ export class Builder { ); bar.stop(); - // Trigger can create extra paths that are not part of the normal routes. - // These files are not built as part of the normal routes, but are still - // included in the build manifest - const extraCreatedPaths: Array = []; - const extraArtifacts: Array = []; - await this.pod.plugins.trigger( - 'beforeBuildManifest', this, extraCreatedPaths, extraArtifacts); - // Moving files is pretty fast, but when the number of files is sufficiently // large, we want to communicate progress to the user with the progress bar. // If less than X files need to be moved, don't show the progress bar, @@ -585,7 +596,7 @@ export class Builder { } await async.mapLimit( - createdPaths.concat(extraCreatedPaths), + createdPaths, Builder.NumConcurrentCopies, async (createdPath: CreatedPath) => { // Start by building the manifest (and getting file shas). diff --git a/packages/amagaki/src/plugins.ts b/packages/amagaki/src/plugins.ts index 3e952e77..51cd179c 100644 --- a/packages/amagaki/src/plugins.ts +++ b/packages/amagaki/src/plugins.ts @@ -1,7 +1,10 @@ import express = require('express'); -import {BuildResult, Builder, CreatedPath, Artifact} from './builder'; -import {TemplateEngineComponent, TemplateEngineRenderResult} from './templateEngine'; +import {BuildResult, Builder} from './builder'; +import { + TemplateEngineComponent, + TemplateEngineRenderResult, +} from './templateEngine'; import {Pod} from './pod'; import {YamlTypeManager} from './plugins/yaml'; @@ -25,18 +28,6 @@ export interface PluginComponent { * Hook for working with the builder before the build is executed. */ beforeBuildHook?: (builder: Builder) => Promise; - /** - * Hook for working with the builder before the build manifest is generated. - * - * This occurs after normal routes are built, but before the manifest is - * generated. - * - * New created paths can be appended to the createdPaths array to be included - * in the manifest. - * - * New artifacts can be appended to the artifacts array. - */ - beforeBuildManifestHook?: (builder: Builder, createdPaths: Array, artifacts: Array) => Promise; /** * Hook for interfacing with the Express server. */ @@ -61,7 +52,8 @@ export interface PluginComponent { updatePathFormatContextHook?: (context: Record) => void; } -export interface PluginConfig {}; +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PluginConfig {} export interface PluginConstructor { new (pod: Pod, config: PluginConfig): PluginComponent; diff --git a/packages/amagaki/src/router.ts b/packages/amagaki/src/router.ts index 0b3a82ed..bda45bec 100644 --- a/packages/amagaki/src/router.ts +++ b/packages/amagaki/src/router.ts @@ -30,7 +30,6 @@ export interface RouteOptions { fields?: Record; } - export type RouteBuilder = (provider: RouteProvider) => Promise; export class Router { @@ -182,7 +181,9 @@ export class RouteProvider { return this._routes; } // Build all routes, then clear the route builders so they are not built again. - await Promise.all(this._routeBuilders.map(async (builder) => await builder(this))); + await Promise.all( + this._routeBuilders.map(async builder => await builder(this)) + ); return this._routes; }