Skip to content
Merged
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: 1 addition & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* @adonisjs/assembler

Check failure on line 2 in index.ts

View workflow job for this annotation

GitHub Actions / test / test_windows (latest)

Property 'toUpperCase' does not exist on type 'number'.

Check failure on line 2 in index.ts

View workflow job for this annotation

GitHub Actions / test / test_windows (latest)

Property 'toUpperCase' does not exist on type 'number'.

Check failure on line 2 in index.ts

View workflow job for this annotation

GitHub Actions / test / test_linux (latest)

Property 'toUpperCase' does not exist on type 'number'.

Check failure on line 2 in index.ts

View workflow job for this annotation

GitHub Actions / test / test_linux (latest)

Property 'toUpperCase' does not exist on type 'number'.

Check failure on line 2 in index.ts

View workflow job for this annotation

GitHub Actions / test / test_windows (latest)

Property 'toUpperCase' does not exist on type 'number'.

Check failure on line 2 in index.ts

View workflow job for this annotation

GitHub Actions / test / test_windows (latest)

Property 'toUpperCase' does not exist on type 'number'.

Check failure on line 2 in index.ts

View workflow job for this annotation

GitHub Actions / test / test_linux (latest)

Property 'toUpperCase' does not exist on type 'number'.

Check failure on line 2 in index.ts

View workflow job for this annotation

GitHub Actions / test / test_linux (latest)

Property 'toUpperCase' does not exist on type 'number'.
*
* (c) AdonisJS
*
Expand All @@ -8,6 +8,7 @@
*/

export { Bundler } from './src/bundler.ts'
export { CodeGen } from './src/codegen.ts'
export { DevServer } from './src/dev_server.ts'
export { TestRunner } from './src/test_runner.ts'
export { FileBuffer } from './src/file_buffer.ts'
Expand Down
24 changes: 12 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,39 +45,39 @@
"@japa/runner": "^5.3.0",
"@japa/snapshot": "^2.0.10",
"@poppinss/ts-exec": "^1.4.4",
"@release-it/conventional-changelog": "^11.0.1",
"@types/node": "^26.1.1",
"@release-it/conventional-changelog": "^12.0.0",
"@types/node": "^26.2.0",
"@types/picomatch": "^4.0.3",
"@types/pretty-hrtime": "^1.0.3",
"c8": "^11.0.0",
"c8": "^12.0.0",
"cross-env": "^10.1.0",
"del-cli": "^7.0.0",
"eslint": "^10.6.0",
"eslint": "^10.8.1",
"hot-hook": "^1.0.0",
"p-event": "^7.1.0",
"prettier": "^3.9.4",
"release-it": "^20.2.1",
"tsdown": "^0.22.3",
"prettier": "^3.9.6",
"release-it": "^21.0.2",
"tsdown": "^0.22.14",
"typedoc": "^0.28.20",
"typescript": "^6.0.3"
},
"dependencies": {
"@adonisjs/env": "^7.0.0",
"@antfu/install-pkg": "^1.1.0",
"@ast-grep/napi": "^0.44.1",
"@antfu/install-pkg": "^2.0.1",
"@ast-grep/napi": "^0.45.1",
"@poppinss/cliui": "^6.8.1",
"@poppinss/hooks": "^7.3.0",
"@poppinss/hooks": "^7.3.1",
"@poppinss/utils": "^7.0.1",
"chokidar": "^5.0.0",
"dedent": "^1.7.2",
"execa": "^9.6.1",
"fast-glob": "^3.3.3",
"fdir": "^6.5.0",
"get-port": "^7.2.0",
"get-tsconfig": "^4.14.0",
"get-tsconfig": "^4.14.3",
"import-meta-resolve": "^4.2.0",
"junk": "^4.0.1",
"open": "^11.0.0",
"open": "^11.0.1",
"parse-imports": "^3.0.0",
"picomatch": "^4.0.5",
"pretty-hrtime": "^1.0.3",
Expand Down
17 changes: 15 additions & 2 deletions src/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,17 @@ export const SUPPORTED_PACKAGE_MANAGERS: {
installCommand: 'pnpm i --prod',
},
'bun': {
packageManagerFiles: ['bun.lockb'],
packageManagerFiles: ['bun.lock', 'bun.lockb'],
installCommand: 'bun install --production',
},
'nub': {
packageManagerFiles: ['nub.lock'],
installCommand: 'nub install --prod',
},
'aube': {
packageManagerFiles: ['aube-lock.yaml'],
installCommand: 'aube install --prod',
},
}

/**
Expand Down Expand Up @@ -175,7 +183,12 @@ export class Bundler {
if (pkgManager === 'deno') {
return 'npm'
}
if (pkgManager === 'pnpm@6') {

/**
* Rush controls pnpm from the monorepo root. The compiled application is
* standalone, so it should use the regular pnpm metadata and command.
*/
if (pkgManager === 'pnpm@6' || pkgManager === 'pnpm-rush') {
return 'pnpm'
}

Expand Down
193 changes: 193 additions & 0 deletions src/codegen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
* @adonisjs/assembler
*
* (c) AdonisJS
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { cliui } from '@poppinss/cliui'
import { fileURLToPath } from 'node:url'
import type Hooks from '@poppinss/hooks'
import string from '@poppinss/utils/string'

import { loadHooks, processRoutes } from './utils.ts'
import { IndexGenerator } from './index_generator/main.ts'
import { type RoutesScanner } from './code_scanners/routes_scanner/main.ts'
import { type CodeGenOptions, type CodeGenSnapshot } from './types/common.ts'
import { type CommonHooks, type HookParams, type RouterHooks } from './types/hooks.ts'

/**
* CodeGen runs the codegen performed by the dev-server, without the server
* and without the file watcher.
*
* The dev-server, the bundler and the test runner all generate code as a
* side-effect of doing something else. CodeGen exists to perform the same work
* on its own, so the contents of the ".adonisjs" directory can be refreshed
* deterministically. For example, inside a CI pipeline before typechecking or
* creating a build.
*
* The class covers both the codegen stages.
*
* - The static stage runs the "init" hooks against an index generator and
* writes the index files. Nothing but the file-system is needed for it.
*
* - The runtime stage runs the routes hooks. It needs a snapshot of the state
* that only a booted application knows about, therefore the snapshot has to
* be supplied by the caller. Assembler never boots the app itself.
*
* The stages run in that order, because an application imports the generated
* index files from its preload files and hence cannot be booted before they
* exist.
*
* Unlike the dev-server, errors are not swallowed. A codegen run either
* completes or throws, so the caller can exit with a non-zero code.
*
* @example
* const codegen = new CodeGen(cwd, { hooks: rcFile.hooks })
* codegen.ui.logger = command.logger
*
* await codegen.run(async () => {
* await warmUpApp()
* return { routes: router.toJSON() }
* })
*/
export class CodeGen {
/**
* Hooks to execute custom actions during the codegen process
*/
#hooks!: Hooks<
{
[K in keyof CommonHooks]: [HookParams<K>, HookParams<K>]
} & {
[K in keyof RouterHooks]: [HookParams<K>, HookParams<K>]
}
>

/**
* Index generator for managing auto-generated index files
*/
#indexGenerator!: IndexGenerator

/**
* Routes scanner created when a routes snapshot has been supplied and
* there are hooks interested in scanning them
*/
#routesScanner?: RoutesScanner

/**
* CLI UI instance for displaying colorful messages and progress information
*/
ui = cliui()

/**
* The current working directory URL
*/
cwd: URL

/**
* The current working project directory path as string
*/
cwdPath: string

/**
* CodeGen configuration options including hooks
*/
options: CodeGenOptions

/**
* Create a new codegen instance
*
* @param cwd - The current working directory URL
* @param options - CodeGen configuration options
*/
constructor(cwd: URL, options: CodeGenOptions) {
this.cwd = cwd
this.options = options
this.cwdPath = string.toUnixSlash(fileURLToPath(this.cwd))
}

/**
* Reference to the index generator used by the last run
*/
get indexGenerator(): IndexGenerator | undefined {
return this.#indexGenerator
}

/**
* Reference to the routes scanner used by the last run. Stays undefined
* when no routes were supplied or no hook asked for them to be scanned
*/
get routesScanner(): RoutesScanner | undefined {
return this.#routesScanner
}

/**
* Generate the codegen files.
*
* The index files are written first, because the application cannot be booted
* without them. The preload files of an application import the generated
* barrel files, for example "start/routes.ts" imports the controllers barrel,
* so those files have to exist on disk before anything imports them. The
* dev-server generates them ahead of spawning the app for the same reason.
*
* Therefore the caller boots the application from inside the "collectSnapshot"
* callback and returns the state only a booted app knows about. Skip the
* callback to generate the index files alone.
*
* @param collectSnapshot - Called once the index files exist. Boot the app
* here and return the runtime state to generate the rest from
*
* @example
* // Index files only
* await codegen.run()
*
* // Index files, then everything derived from the routes
* await codegen.run(async () => {
* await app.boot()
* await app.warmUp()
* router.commit()
* return { routes: router.toJSON() }
* })
*/
async run(collectSnapshot?: () => CodeGenSnapshot | Promise<CodeGenSnapshot>): Promise<void> {
this.ui.logger.info('loading hooks...')
this.#hooks = await loadHooks(this.options.hooks, [
'init',
'routesCommitted',
'routesScanning',
'routesScanned',
])

this.#indexGenerator = new IndexGenerator(this.cwdPath, this.ui.logger)

/**
* Static stage. Run the init hooks and write the index files. Nothing but
* the file-system is needed for it
*/
await this.#hooks.runner('init').run(this, this.#hooks, this.#indexGenerator)
this.#hooks.clear('init')

this.ui.logger.info('generating indexes...')
await this.#indexGenerator.generate()

if (!collectSnapshot) {
return
}

/**
* Runtime stage. The index files exist by now, so the caller is able to
* boot the app and hand over the state we cannot collect ourselves
*/
const snapshot = await collectSnapshot()
if (snapshot.routes) {
/**
* The cast is needed because a hooks instance is invariant over its
* events map. This instance carries the "init" hook as well, which the
* helper has no interest in
*/
this.#routesScanner = await processRoutes(this.#hooks, this, this.cwdPath, snapshot.routes)
}
}
}
39 changes: 15 additions & 24 deletions src/dev_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,17 @@ import type {
HotHookMessage,
} from './types/common.ts'
import { IndexGenerator } from './index_generator/main.ts'
import { RoutesScanner } from './code_scanners/routes_scanner/main.ts'
import { getPort, loadHooks, readTsConfig, runNode, throttle, watch } from './utils.ts'
import { type RoutesScanner } from './code_scanners/routes_scanner/main.ts'
import {
getPort,
loadHooks,
readTsConfig,
runNode,
throttle,
watch,
hasRoutesHooks,
processRoutes,
} from './utils.ts'
import {
type HookParams,
type RouterHooks,
Expand Down Expand Up @@ -557,14 +566,11 @@ export class DevServer {
*/
#processRoutes = throttle(async (routesFileLocation: string) => {
try {
const scanRoutes = this.#hooks.has('routesScanning') || this.#hooks.has('routesScanned')
const shareRoutes = this.#hooks.has('routesCommitted')

/**
* Remove the routes file and return early when there are no
* hooks listening for routes related events
*/
if (!scanRoutes && !shareRoutes) {
if (!hasRoutesHooks(this.#hooks)) {
unlink(routesFileLocation).catch(() => {})
return
}
Expand All @@ -577,25 +583,10 @@ export class DevServer {
unlink(routesFileLocation).catch(() => {})

/**
* Notify about the existence of routes
*/
if (shareRoutes) {
await this.#hooks.runner('routesCommitted').run(this, routesList)
}

/**
* Scan routes and notify scanning and scanned hooks
* Share the routes with the hooks and scan them. The sequence is shared
* with the codegen, hence it lives inside a util
*/
if (scanRoutes) {
this.#routesScanner = new RoutesScanner(this.cwdPath, [])
await this.#hooks.runner('routesScanning').run(this, this.#routesScanner)

for (const domain of Object.keys(routesList)) {
await this.#routesScanner.scan(routesList[domain])
}

await this.#hooks.runner('routesScanned').run(this, this.#routesScanner)
}
this.#routesScanner = await processRoutes(this.#hooks, this, this.cwdPath, routesList)
} catch (error) {
this.ui.logger.error('Unable to process and scan routes because of the following error')
this.ui.logger.fatal(error as Error)
Expand Down
12 changes: 5 additions & 7 deletions src/file_system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,16 +286,14 @@ export class FileSystem {
}

const testFilePatterns = testSuites.flatMap((suite) => suite.files)
const picomatcchOptions = { cwd: this.#cwd }

/**
* Initiate picomatch matchers we will need to identify metafiles
*/
this.#isMetaFileWithReloadsEnabled = picomatch(metaFilesWithReloads, picomatcchOptions)
this.#isMetaFileWithReloadsDisabled = picomatch(metaFilesWithoutReloads, picomatcchOptions)
this.#isTestFile = picomatch(testFilePatterns, picomatcchOptions)
this.#isIncluded = picomatch(this.#includes, picomatcchOptions)
this.#isExcluded = picomatch(this.#excludes, picomatcchOptions)
this.#isMetaFileWithReloadsEnabled = picomatch(metaFilesWithReloads)
this.#isMetaFileWithReloadsDisabled = picomatch(metaFilesWithoutReloads)
this.#isTestFile = picomatch(testFilePatterns)
this.#isIncluded = picomatch(this.#includes)
this.#isExcluded = picomatch(this.#excludes)

debug('initiating file system %O', {
includes: this.#includes,
Expand Down
3 changes: 2 additions & 1 deletion src/types/code_transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,4 +251,5 @@ export type HookNode =
* // Used in bundler configuration
* const bundler = new Bundler(cwd, ts, { packageManager: 'yarn@berry' })
*/
export type SupportedPackageManager = 'npm' | 'yarn' | 'yarn@berry' | 'pnpm' | 'bun'
export type SupportedPackageManager =
'npm' | 'yarn' | 'yarn@berry' | 'pnpm' | 'bun' | 'nub' | 'aube'
Loading
Loading