Skip to content

Commit 0548b3f

Browse files
committed
feat: add makeEntityIndex to code_transformer
The method could be used for creating index files with list of dynamic imports for controllers, bouncer policies, event listeners, queue jobs and so on
1 parent 9807862 commit 0548b3f

File tree

2 files changed

+209
-6
lines changed

2 files changed

+209
-6
lines changed

src/code_transformer/main.ts

Lines changed: 98 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,14 @@
77
* file that was distributed with this source code.
88
*/
99

10-
import { join } from 'node:path'
1110
import { fileURLToPath } from 'node:url'
11+
import { basename, dirname, join, relative } from 'node:path'
12+
import string from '@poppinss/utils/string'
13+
import { isScriptFile } from '@poppinss/utils'
14+
import { fsReadAll } from '@poppinss/utils/fs'
15+
import { mkdir, writeFile } from 'node:fs/promises'
16+
import { type OneOrMore } from '@poppinss/utils/types'
17+
import StringBuilder from '@poppinss/utils/string_builder'
1218
import { installPackage, detectPackageManager } from '@antfu/install-pkg'
1319
import {
1420
Node,
@@ -26,6 +32,7 @@ import type {
2632
EnvValidationNode,
2733
BouncerPolicyNode,
2834
} from '../types/code_transformer.ts'
35+
import debug from '../debug.ts'
2936

3037
/**
3138
* This class is responsible for updating
@@ -42,6 +49,7 @@ export class CodeTransformer {
4249
* Directory of the adonisjs project
4350
*/
4451
#cwd: URL
52+
#cwdPath: string
4553

4654
/**
4755
* The TsMorph project
@@ -63,6 +71,7 @@ export class CodeTransformer {
6371

6472
constructor(cwd: URL) {
6573
this.#cwd = cwd
74+
this.#cwdPath = fileURLToPath(this.#cwd)
6675
this.project = new Project({
6776
tsConfigFilePath: join(fileURLToPath(this.#cwd), 'tsconfig.json'),
6877
manipulationSettings: { quoteKind: QuoteKind.Single },
@@ -235,7 +244,7 @@ export class CodeTransformer {
235244
/**
236245
* Get the `start/env.ts` source file
237246
*/
238-
const kernelUrl = fileURLToPath(new URL('./start/env.ts', this.#cwd))
247+
const kernelUrl = join(this.#cwdPath, './start/env.ts')
239248
const file = this.project.getSourceFileOrThrow(kernelUrl)
240249

241250
/**
@@ -308,7 +317,7 @@ export class CodeTransformer {
308317
/**
309318
* Get the `start/kernel.ts` source file
310319
*/
311-
const kernelUrl = fileURLToPath(new URL('./start/kernel.ts', this.#cwd))
320+
const kernelUrl = join(this.#cwdPath, './start/kernel.ts')
312321
const file = this.project.getSourceFileOrThrow(kernelUrl)
313322

314323
/**
@@ -345,7 +354,7 @@ export class CodeTransformer {
345354
/**
346355
* Get the `tests/bootstrap.ts` source file
347356
*/
348-
const testBootstrapUrl = fileURLToPath(new URL('./tests/bootstrap.ts', this.#cwd))
357+
const testBootstrapUrl = join(this.#cwdPath, './tests/bootstrap.ts')
349358
const file = this.project.getSourceFileOrThrow(testBootstrapUrl)
350359

351360
/**
@@ -383,7 +392,7 @@ export class CodeTransformer {
383392
/**
384393
* Get the `vite.config.ts` source file
385394
*/
386-
const viteConfigTsUrl = fileURLToPath(new URL('./vite.config.ts', this.#cwd))
395+
const viteConfigTsUrl = join(this.#cwdPath, './vite.config.ts')
387396

388397
const file = this.project.getSourceFile(viteConfigTsUrl)
389398
if (!file) {
@@ -438,7 +447,7 @@ export class CodeTransformer {
438447
/**
439448
* Get the `app/policies/main.ts` source file
440449
*/
441-
const kernelUrl = fileURLToPath(new URL('./app/policies/main.ts', this.#cwd))
450+
const kernelUrl = join(this.#cwdPath, './app/policies/main.ts')
442451
const file = this.project.getSourceFileOrThrow(kernelUrl)
443452

444453
/**
@@ -451,4 +460,87 @@ export class CodeTransformer {
451460
file.formatText(this.#editorSettings)
452461
await file.save()
453462
}
463+
464+
/**
465+
* Creates an index file that exports an object in which the key is the PascalCase
466+
* name of the entity and the value is a dynamic import.
467+
*
468+
* For example, in case of controllers, the index file will be the list of controller
469+
* names pointing a dynamically imported controller file.
470+
*
471+
* ```ts
472+
* export const controllers = {
473+
* Login: () => import('#controllers/login_controller'),
474+
* Login: () => import('#controllers/login_controller'),
475+
* }
476+
* ```
477+
*
478+
* @param source
479+
* @param outputPath
480+
* @param importAlias
481+
*/
482+
async makeEntityIndex(
483+
input: OneOrMore<{ source: string; importAlias?: string }>,
484+
output: {
485+
destination: string
486+
exportName?: string
487+
transformName?: (name: string) => string
488+
transformImport?: (modulePath: string) => string
489+
}
490+
) {
491+
const inputs = Array.isArray(input) ? input : [input]
492+
const outputPath = join(this.#cwdPath, output.destination)
493+
const outputDir = dirname(outputPath)
494+
const exportName =
495+
output.exportName ??
496+
new StringBuilder(basename(output.destination)).removeExtension().camelCase()
497+
498+
debug(
499+
'creating index for "%s" at destination "%s" using sources %O',
500+
exportName,
501+
outputPath,
502+
inputs
503+
)
504+
505+
const entries = await Promise.all(
506+
inputs.map(async ({ source, importAlias }) => {
507+
const sourcePath = join(this.#cwdPath, source)
508+
const filesList = await fsReadAll(sourcePath, {
509+
filter: isScriptFile,
510+
pathType: 'absolute',
511+
})
512+
513+
return filesList.map((filePath) => {
514+
const name = new StringBuilder(relative(sourcePath, filePath))
515+
.removeExtension()
516+
.pascalCase()
517+
.toString()
518+
519+
const importPath = importAlias
520+
? `${importAlias}/${new StringBuilder(relative(sourcePath, filePath)).removeExtension().toString()}`
521+
: relative(outputDir, filePath)
522+
523+
return {
524+
name: output.transformName?.(name) ?? name,
525+
importPath: output.transformImport?.(importPath) ?? importPath,
526+
}
527+
})
528+
})
529+
)
530+
531+
const outputContents = entries
532+
.flat(2)
533+
.reduce<string[]>(
534+
(result, entry) => {
535+
debug('adding "%O" to the index', entry)
536+
result.push(` ${entry.name}: () => import('${entry.importPath}'),`)
537+
return result
538+
},
539+
[`export const ${exportName} = {`]
540+
)
541+
.concat('}')
542+
543+
await mkdir(outputDir, { recursive: true })
544+
await writeFile(outputPath, outputContents.join('\n'))
545+
}
454546
}

tests/code_transformer.spec.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import dedent from 'dedent'
1111
import { test } from '@japa/runner'
1212
import { readFile } from 'node:fs/promises'
1313
import type { FileSystem } from '@japa/file-system'
14+
import StringBuilder from '@poppinss/utils/string_builder'
1415
import { CodeTransformer } from '../src/code_transformer/main.ts'
1516

1617
async function setupFakeAdonisproject(fs: FileSystem) {
@@ -923,3 +924,113 @@ test.group('Code Transformer | addAssemblerHook', (group) => {
923924
assert.equal(occurrences, 1)
924925
})
925926
})
927+
928+
test.group('Code Transformer | create entity index file', (group) => {
929+
group.each.setup(async ({ context }) => setupFakeAdonisproject(context.fs))
930+
931+
test('create index from flat and nested files', async ({ assert, fs }) => {
932+
const transformer = new CodeTransformer(fs.baseUrl)
933+
await fs.create('app/controllers/posts_controller.ts', '')
934+
await fs.create('app/controllers/user/posts_controller.ts', '')
935+
await fs.create('app/controllers/auth/signup_controller.ts', '')
936+
await fs.create('app/controllers/public/home_page.ts', '')
937+
938+
const outputPath = './.adonisjs/backend/controllers.ts'
939+
await transformer.makeEntityIndex(
940+
{ source: './app/controllers', importAlias: '#controllers' },
941+
{
942+
destination: outputPath,
943+
}
944+
)
945+
946+
assert.snapshot(await fs.contents(outputPath)).matchInline(`
947+
"export const controllers = {
948+
AuthSignupController: () => import('#controllers/auth/signup_controller'),
949+
PostsController: () => import('#controllers/posts_controller'),
950+
PublicHomePage: () => import('#controllers/public/home_page'),
951+
UserPostsController: () => import('#controllers/user/posts_controller'),
952+
}"
953+
`)
954+
})
955+
956+
test('create index without the import alias', async ({ assert, fs }) => {
957+
const transformer = new CodeTransformer(fs.baseUrl)
958+
await fs.create('app/controllers/posts_controller.ts', '')
959+
await fs.create('app/controllers/user/posts_controller.ts', '')
960+
await fs.create('app/controllers/auth/signup_controller.ts', '')
961+
await fs.create('app/controllers/public/home_page.ts', '')
962+
963+
const outputPath = './.adonisjs/backend/controllers.ts'
964+
await transformer.makeEntityIndex(
965+
{ source: './app/controllers' },
966+
{
967+
destination: outputPath,
968+
}
969+
)
970+
971+
assert.snapshot(await fs.contents(outputPath)).matchInline(`
972+
"export const controllers = {
973+
AuthSignupController: () => import('../../app/controllers/auth/signup_controller.ts'),
974+
PostsController: () => import('../../app/controllers/posts_controller.ts'),
975+
PublicHomePage: () => import('../../app/controllers/public/home_page.ts'),
976+
UserPostsController: () => import('../../app/controllers/user/posts_controller.ts'),
977+
}"
978+
`)
979+
})
980+
981+
test('apply name transformer', async ({ assert, fs }) => {
982+
const transformer = new CodeTransformer(fs.baseUrl)
983+
await fs.create('app/controllers/posts_controller.ts', '')
984+
await fs.create('app/controllers/user/posts_controller.ts', '')
985+
await fs.create('app/controllers/auth/signup_controller.ts', '')
986+
await fs.create('app/controllers/public/home_page.ts', '')
987+
988+
const outputPath = './.adonisjs/backend/controllers.ts'
989+
await transformer.makeEntityIndex(
990+
{ source: './app/controllers' },
991+
{
992+
destination: outputPath,
993+
transformName(name) {
994+
return new StringBuilder(name).removeSuffix('Controller').toString()
995+
},
996+
}
997+
)
998+
999+
assert.snapshot(await fs.contents(outputPath)).matchInline(`
1000+
"export const controllers = {
1001+
AuthSignup: () => import('../../app/controllers/auth/signup_controller.ts'),
1002+
Posts: () => import('../../app/controllers/posts_controller.ts'),
1003+
PublicHomePage: () => import('../../app/controllers/public/home_page.ts'),
1004+
UserPosts: () => import('../../app/controllers/user/posts_controller.ts'),
1005+
}"
1006+
`)
1007+
})
1008+
1009+
test('apply import path transformer', async ({ assert, fs }) => {
1010+
const transformer = new CodeTransformer(fs.baseUrl)
1011+
await fs.create('app/controllers/posts_controller.ts', '')
1012+
await fs.create('app/controllers/user/posts_controller.ts', '')
1013+
await fs.create('app/controllers/auth/signup_controller.ts', '')
1014+
await fs.create('app/controllers/public/home_page.ts', '')
1015+
1016+
const outputPath = './.adonisjs/backend/controllers.ts'
1017+
await transformer.makeEntityIndex(
1018+
{ source: './app/controllers' },
1019+
{
1020+
destination: outputPath,
1021+
transformImport(modulePath) {
1022+
return modulePath.replace(new RegExp('../../app/controllers'), '#controllers')
1023+
},
1024+
}
1025+
)
1026+
1027+
assert.snapshot(await fs.contents(outputPath)).matchInline(`
1028+
"export const controllers = {
1029+
AuthSignupController: () => import('#controllers/auth/signup_controller.ts'),
1030+
PostsController: () => import('#controllers/posts_controller.ts'),
1031+
PublicHomePage: () => import('#controllers/public/home_page.ts'),
1032+
UserPostsController: () => import('#controllers/user/posts_controller.ts'),
1033+
}"
1034+
`)
1035+
})
1036+
})

0 commit comments

Comments
 (0)