Skip to content

Commit 194861c

Browse files
committed
feat(makeEntityIndex): allow transforming name and output
1 parent fcef8f4 commit 194861c

File tree

4 files changed

+205
-48
lines changed

4 files changed

+205
-48
lines changed

README.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,85 @@ export const policies = {
444444
}
445445
```
446446

447+
### makeEntityIndex
448+
The method is used to create an index file for a collection of entities discovered from one or more root folders. We use this method to create an index file for controllers or generate types for Inertia pages.
449+
450+
```ts
451+
const transformer = new CodeTransformer(appRoot)
452+
453+
const output = await transformer.makeEntityIndex({
454+
source: 'app/controllers',
455+
importAlias: '#controllers'
456+
}, {
457+
destination: '.adonisjs/backend/controllers',
458+
exportName: 'controllers'
459+
})
460+
461+
/**
462+
export const controllers = {
463+
SignupController: () => import('#controllers/auth/signup_controller'),
464+
PostsController: () => import('#controllers/posts_controller'),
465+
HomePage: () => import('#controllers/public/home_page'),
466+
UserPostsController: () => import('#controllers/user/posts_controller'),
467+
}
468+
*/
469+
```
470+
471+
If you would like to remove the `Controller` suffix from the key (which we do in our official generator), then you can specify the `removeNameSuffix` option.
472+
473+
```ts
474+
const output = await transformer.makeEntityIndex({
475+
source: 'app/controllers',
476+
importAlias: '#controllers'
477+
}, {
478+
destination: '.adonisjs/backend/controllers',
479+
exportName: 'controllers',
480+
removeNameSuffix: 'controller'
481+
})
482+
```
483+
484+
For more advanced use-cases, you can specify the `computeBaseName` method to self compute the key name for the collection.
485+
486+
```ts
487+
import StringBuilder from '@poppinss/utils/string_builder'
488+
489+
const output = await transformer.makeEntityIndex({
490+
source: 'app/controllers',
491+
importAlias: '#controllers'
492+
}, {
493+
destination: '.adonisjs/backend/controllers',
494+
exportName: 'controllers',
495+
computeBaseName(filePath, sourcePath) {
496+
const baseName = relative(sourcePath, filePath)
497+
return new StringBuilder(baseName).toUnixSlash().removeExtension().removeSuffix('Controller').toString()
498+
},
499+
})
500+
```
501+
502+
#### Controlling the output
503+
The output is an object with key-value pair in which the value is a lazily imported module. However, you can customize the output to generate a TypeScript type using the `computeOutput` method.
504+
505+
```ts
506+
const output = await transformer.makeEntityIndex(
507+
{ source: './inertia/pages', allowedExtensions: ['.tsx'] },
508+
{
509+
destination: outputPath,
510+
computeOutput(entries) {
511+
return entries
512+
.reduce<string[]>(
513+
(result, entry) => {
514+
result.push(`${entry.name}: typeof import('${entry.importPath}')`)
515+
return result
516+
},
517+
[`declare module '@adonisjs/inertia' {`, 'export interface Pages {']
518+
)
519+
.concat('}', '}')
520+
.join('\n')
521+
},
522+
}
523+
)
524+
```
525+
447526
## Contributing
448527
One of the primary goals of AdonisJS is to have a vibrant community of users and contributors who believe in the framework's principles.
449528

package.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,28 +40,28 @@
4040
"@japa/file-system": "^2.3.2",
4141
"@japa/runner": "^4.2.0",
4242
"@japa/snapshot": "^2.0.8",
43-
"@poppinss/ts-exec": "^1.2.1",
43+
"@poppinss/ts-exec": "^1.4.0",
4444
"@release-it/conventional-changelog": "^10.0.1",
45-
"@types/node": "^22.15.29",
45+
"@types/node": "^24.0.10",
4646
"@types/picomatch": "^4.0.0",
4747
"@types/pretty-hrtime": "^1.0.3",
4848
"c8": "^10.1.3",
4949
"cross-env": "^7.0.3",
5050
"del-cli": "^6.0.0",
51-
"eslint": "^9.28.0",
51+
"eslint": "^9.30.1",
5252
"hot-hook": "^0.4.1-next.0",
5353
"p-event": "^6.0.1",
54-
"prettier": "^3.5.3",
54+
"prettier": "^3.6.2",
5555
"release-it": "^19.0.3",
5656
"tsup": "^8.5.0",
5757
"typescript": "^5.8.3"
5858
},
5959
"dependencies": {
6060
"@adonisjs/env": "^6.2.0",
6161
"@antfu/install-pkg": "^1.1.0",
62-
"@poppinss/cliui": "^6.4.3",
63-
"@poppinss/hooks": "^7.2.5",
64-
"@poppinss/utils": "^7.0.0-next.1",
62+
"@poppinss/cliui": "^6.4.4",
63+
"@poppinss/hooks": "^7.2.6",
64+
"@poppinss/utils": "^7.0.0-next.3",
6565
"chokidar": "^4.0.3",
6666
"dedent": "^1.6.0",
6767
"execa": "^9.6.0",

src/code_transformer/main.ts

Lines changed: 63 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { fsReadAll } from '@poppinss/utils/fs'
1414
import { mkdir, writeFile } from 'node:fs/promises'
1515
import { type OneOrMore } from '@poppinss/utils/types'
1616
import StringBuilder from '@poppinss/utils/string_builder'
17-
import { basename, dirname, join, relative } from 'node:path'
17+
import { basename, dirname, extname, join, relative } from 'node:path'
1818
import { installPackage, detectPackageManager } from '@antfu/install-pkg'
1919
import {
2020
Node,
@@ -470,8 +470,8 @@ export class CodeTransformer {
470470
*
471471
* ```ts
472472
* export const controllers = {
473-
* Login: () => import('#controllers/login_controller'),
474-
* Login: () => import('#controllers/login_controller'),
473+
* LoginController: () => import('#controllers/login_controller'),
474+
* LogoutController: () => import('#controllers/logout_controller'),
475475
* }
476476
* ```
477477
*
@@ -480,12 +480,13 @@ export class CodeTransformer {
480480
* @param importAlias
481481
*/
482482
async makeEntityIndex(
483-
input: OneOrMore<{ source: string; importAlias?: string }>,
483+
input: OneOrMore<{ source: string; importAlias?: string; allowedExtensions?: string[] }>,
484484
output: {
485485
destination: string
486486
exportName?: string
487-
transformName?: (name: string) => string
488-
transformImport?: (modulePath: string) => string
487+
removeNameSuffix?: string
488+
computeBaseName?: (filePath: string, sourcePath: string) => string
489+
computeOutput?: (entries: { name: string; importPath: string }[]) => string
489490
}
490491
) {
491492
const inputs = Array.isArray(input) ? input : [input]
@@ -503,44 +504,81 @@ export class CodeTransformer {
503504
)
504505

505506
const entries = await Promise.all(
506-
inputs.map(async ({ source, importAlias }) => {
507+
inputs.map(async ({ source, importAlias, allowedExtensions }) => {
507508
const sourcePath = join(this.#cwdPath, source)
508509
const filesList = await fsReadAll(sourcePath, {
509-
filter: isScriptFile,
510+
filter: (filePath: string) => {
511+
if (allowedExtensions) {
512+
const ext = extname(filePath)
513+
return allowedExtensions.includes(ext)
514+
}
515+
return isScriptFile(filePath)
516+
},
510517
pathType: 'absolute',
511518
})
512519

520+
const knownBaseNames = new Set()
521+
513522
return filesList.map((filePath) => {
514-
const name = new StringBuilder(string.toUnixSlash(relative(sourcePath, filePath)))
523+
/**
524+
* We assume all filenames are unique across sub-directories, hence we will
525+
* use the baseName of the file. However, if a file with the same name already
526+
* exists, when we will prefix the parent subdirectories to the name.
527+
*/
528+
let baseName = basename(filePath)
529+
if (output.computeBaseName) {
530+
baseName = output.computeBaseName?.(filePath, sourcePath)
531+
} else {
532+
if (knownBaseNames.has(baseName)) {
533+
baseName = string.toUnixSlash(relative(sourcePath, filePath))
534+
}
535+
knownBaseNames.add(baseName)
536+
}
537+
538+
const name = new StringBuilder(baseName)
515539
.removeExtension()
540+
.removeSuffix(output.removeNameSuffix ?? '')
516541
.pascalCase()
517542
.toString()
518543

519-
const importPath = importAlias
520-
? `${importAlias}/${new StringBuilder(string.toUnixSlash(relative(sourcePath, filePath))).removeExtension().toString()}`
544+
/**
545+
* When using an import alias, the baseImportPath will be a relative path
546+
* from the source directory, otherwise it will be a relative between
547+
* the outputDir and the filePath.
548+
*/
549+
const baseImportPath = importAlias
550+
? string.toUnixSlash(relative(sourcePath, filePath))
521551
: string.toUnixSlash(relative(outputDir, filePath))
522552

553+
const importPath = importAlias
554+
? `${importAlias}/${new StringBuilder(baseImportPath).removeExtension().toString()}`
555+
: baseImportPath
556+
523557
return {
524-
name: output.transformName?.(name) ?? name,
525-
importPath: output.transformImport?.(importPath) ?? importPath,
558+
name,
559+
importPath,
526560
}
527561
})
528562
})
529563
)
530564

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('}')
565+
const computeOutput =
566+
output.computeOutput ??
567+
((list) => {
568+
return list
569+
.reduce<string[]>(
570+
(result, entry) => {
571+
debug('adding "%O" to the index', entry)
572+
result.push(` ${entry.name}: () => import('${entry.importPath}'),`)
573+
return result
574+
},
575+
[`export const ${exportName} = {`]
576+
)
577+
.concat('}')
578+
.join('\n')
579+
})
542580

543581
await mkdir(outputDir, { recursive: true })
544-
await writeFile(outputPath, outputContents.join('\n'))
582+
await writeFile(outputPath, computeOutput(entries.flat(2)))
545583
}
546584
}

tests/code_transformer.spec.ts

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
import dedent from 'dedent'
1111
import { test } from '@japa/runner'
12+
import { relative } from 'node:path'
13+
import string from '@poppinss/utils/string'
1214
import { readFile } from 'node:fs/promises'
1315
import type { FileSystem } from '@japa/file-system'
1416
import StringBuilder from '@poppinss/utils/string_builder'
@@ -945,9 +947,9 @@ test.group('Code Transformer | create entity index file', (group) => {
945947

946948
assert.snapshot(await fs.contents(outputPath)).matchInline(`
947949
"export const controllers = {
948-
AuthSignupController: () => import('#controllers/auth/signup_controller'),
950+
SignupController: () => import('#controllers/auth/signup_controller'),
949951
PostsController: () => import('#controllers/posts_controller'),
950-
PublicHomePage: () => import('#controllers/public/home_page'),
952+
HomePage: () => import('#controllers/public/home_page'),
951953
UserPostsController: () => import('#controllers/user/posts_controller'),
952954
}"
953955
`)
@@ -970,15 +972,15 @@ test.group('Code Transformer | create entity index file', (group) => {
970972

971973
assert.snapshot(await fs.contents(outputPath)).matchInline(`
972974
"export const controllers = {
973-
AuthSignupController: () => import('../../app/controllers/auth/signup_controller.ts'),
975+
SignupController: () => import('../../app/controllers/auth/signup_controller.ts'),
974976
PostsController: () => import('../../app/controllers/posts_controller.ts'),
975-
PublicHomePage: () => import('../../app/controllers/public/home_page.ts'),
977+
HomePage: () => import('../../app/controllers/public/home_page.ts'),
976978
UserPostsController: () => import('../../app/controllers/user/posts_controller.ts'),
977979
}"
978980
`)
979981
})
980982

981-
test('apply name transformer', async ({ assert, fs }) => {
983+
test('self compute the baseName', async ({ assert, fs }) => {
982984
const transformer = new CodeTransformer(fs.baseUrl)
983985
await fs.create('app/controllers/posts_controller.ts', '')
984986
await fs.create('app/controllers/user/posts_controller.ts', '')
@@ -990,8 +992,9 @@ test.group('Code Transformer | create entity index file', (group) => {
990992
{ source: './app/controllers' },
991993
{
992994
destination: outputPath,
993-
transformName(name) {
994-
return new StringBuilder(name).removeSuffix('Controller').toString()
995+
computeBaseName(filePath, sourcePath) {
996+
const baseName = string.toUnixSlash(relative(sourcePath, filePath))
997+
return new StringBuilder(baseName).removeExtension().removeSuffix('Controller').toString()
995998
},
996999
}
9971000
)
@@ -1006,7 +1009,7 @@ test.group('Code Transformer | create entity index file', (group) => {
10061009
`)
10071010
})
10081011

1009-
test('apply import path transformer', async ({ assert, fs }) => {
1012+
test('remove name suffix', async ({ assert, fs }) => {
10101013
const transformer = new CodeTransformer(fs.baseUrl)
10111014
await fs.create('app/controllers/posts_controller.ts', '')
10121015
await fs.create('app/controllers/user/posts_controller.ts', '')
@@ -1015,21 +1018,58 @@ test.group('Code Transformer | create entity index file', (group) => {
10151018

10161019
const outputPath = './.adonisjs/backend/controllers.ts'
10171020
await transformer.makeEntityIndex(
1018-
{ source: './app/controllers' },
1021+
{ source: './app/controllers', importAlias: '#controllers' },
10191022
{
10201023
destination: outputPath,
1021-
transformImport(modulePath) {
1022-
return modulePath.replace(new RegExp('../../app/controllers'), '#controllers')
1023-
},
1024+
removeNameSuffix: 'controller',
10241025
}
10251026
)
10261027

10271028
assert.snapshot(await fs.contents(outputPath)).matchInline(`
10281029
"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'),
1030+
Signup: () => import('#controllers/auth/signup_controller'),
1031+
Posts: () => import('#controllers/posts_controller'),
1032+
HomePage: () => import('#controllers/public/home_page'),
1033+
UserPosts: () => import('#controllers/user/posts_controller'),
1034+
}"
1035+
`)
1036+
})
1037+
1038+
test('compute output using a custom function', async ({ assert, fs }) => {
1039+
const transformer = new CodeTransformer(fs.baseUrl)
1040+
await fs.create('inertia/pages/home.tsx', '')
1041+
await fs.create('inertia/pages/auth/signup.tsx', '')
1042+
await fs.create('inertia/pages/auth/login.tsx', '')
1043+
await fs.create('inertia/pages/account/profile.tsx', '')
1044+
1045+
const outputPath = './.adonisjs/backend/inertia_pages.ts'
1046+
await transformer.makeEntityIndex(
1047+
{ source: './inertia/pages', allowedExtensions: ['.tsx'] },
1048+
{
1049+
destination: outputPath,
1050+
computeOutput(entries) {
1051+
return entries
1052+
.reduce<string[]>(
1053+
(result, entry) => {
1054+
result.push(`${entry.name}: typeof import('${entry.importPath}')`)
1055+
return result
1056+
},
1057+
[`declare module '@adonisjs/inertia' {`, 'export interface Pages {']
1058+
)
1059+
.concat('}', '}')
1060+
.join('\n')
1061+
},
1062+
}
1063+
)
1064+
1065+
assert.snapshot(await fs.contents(outputPath)).matchInline(`
1066+
"declare module '@adonisjs/inertia' {
1067+
export interface Pages {
1068+
Profile: typeof import('../../inertia/pages/account/profile.tsx')
1069+
Login: typeof import('../../inertia/pages/auth/login.tsx')
1070+
Signup: typeof import('../../inertia/pages/auth/signup.tsx')
1071+
Home: typeof import('../../inertia/pages/home.tsx')
1072+
}
10331073
}"
10341074
`)
10351075
})

0 commit comments

Comments
 (0)