diff --git a/factories/inertia_factory.ts b/factories/inertia_factory.ts index 0f8d3a1..93512df 100644 --- a/factories/inertia_factory.ts +++ b/factories/inertia_factory.ts @@ -7,6 +7,7 @@ * file that was distributed with this source code. */ +import type { ViteDevServer } from 'vite' import type { ViteRuntime } from 'vite/runtime' import { HttpContext } from '@adonisjs/core/http' import { AppFactory } from '@adonisjs/core/factories/app' @@ -30,7 +31,7 @@ export class InertiaFactory { #parameters: FactoryParameters = { ctx: new HttpContextFactory().create(), } - #viteRuntime?: ViteRuntime + #vite?: { runtime: ViteRuntime; devServer: ViteDevServer } #getApp() { return new AppFactory().create(new URL('./', import.meta.url), () => {}) as ApplicationService @@ -52,8 +53,8 @@ export class InertiaFactory { return this } - withViteRuntime(runtime: { executeEntrypoint: (path: string) => Promise }) { - this.#viteRuntime = runtime as any + withVite(options: { runtime: ViteRuntime; devServer: ViteDevServer }) { + this.#vite = options return this } @@ -69,6 +70,6 @@ export class InertiaFactory { async create() { const config = await defineConfig(this.#parameters.config || {}).resolver(this.#getApp()) - return new Inertia(this.#parameters.ctx, config, this.#viteRuntime) + return new Inertia(this.#parameters.ctx, config, this.#vite?.runtime, this.#vite?.devServer) } } diff --git a/package.json b/package.json index 1019f16..e6affa0 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "crc-32": "^1.2.2", "edge-error": "^4.0.1", "html-entities": "^2.4.0", + "locate-path": "^7.2.0", "qs": "^6.11.2" }, "peerDependencies": { diff --git a/providers/inertia_provider.ts b/providers/inertia_provider.ts index 2720377..f341770 100644 --- a/providers/inertia_provider.ts +++ b/providers/inertia_provider.ts @@ -10,12 +10,12 @@ /// import { configProvider } from '@adonisjs/core' +import { BriskRoute } from '@adonisjs/core/http' import { RuntimeException } from '@poppinss/utils' import type { ApplicationService } from '@adonisjs/core/types' import InertiaMiddleware from '../src/inertia_middleware.js' import type { InertiaConfig, ResolvedConfig } from '../src/types.js' -import { BriskRoute } from '@adonisjs/core/http' declare module '@adonisjs/core/http' { interface BriskRoute { @@ -48,6 +48,9 @@ export default class InertiaProvider { edgeExports.default.use(edgePluginInertia()) } + /** + * Register inertia middleware + */ async register() { this.app.container.singleton(InertiaMiddleware, async () => { const inertiaConfigProvider = this.app.config.get('inertia') @@ -64,6 +67,9 @@ export default class InertiaProvider { }) } + /** + * Register edge plugin and brisk route macro + */ async boot() { await this.registerEdgePlugin() diff --git a/src/define_config.ts b/src/define_config.ts index 0a456bf..45c885a 100644 --- a/src/define_config.ts +++ b/src/define_config.ts @@ -11,25 +11,31 @@ import { configProvider } from '@adonisjs/core' import type { ConfigProvider } from '@adonisjs/core/types' import { VersionCache } from './version_cache.js' +import { FilesDetector } from './files_detector.js' import type { InertiaConfig, ResolvedConfig } from './types.js' +import { slash } from '@poppinss/utils' /** * Define the Inertia configuration */ export function defineConfig(config: InertiaConfig): ConfigProvider { return configProvider.create(async (app) => { + const detector = new FilesDetector(app) const versionCache = new VersionCache(app.appRoot, config.assetsVersion) await versionCache.computeVersion() return { + versionCache, rootView: config.rootView ?? 'root', sharedData: config.sharedData || {}, - versionCache, + entrypoint: slash(config.entrypoint ?? (await detector.detectEntrypoint('resources/app.ts'))), ssr: { enabled: config.ssr?.enabled ?? false, pages: config.ssr?.pages, - entrypoint: config.ssr?.entrypoint ?? app.makePath('resources/ssr.ts'), - bundle: config.ssr?.bundle ?? app.makePath('ssr/ssr.js'), + entrypoint: + config.ssr?.entrypoint ?? (await detector.detectSsrEntrypoint('resources/ssr.ts')), + + bundle: config.ssr?.bundle ?? (await detector.detectSsrBundle('ssr/ssr.js')), }, } }) diff --git a/src/files_detector.ts b/src/files_detector.ts new file mode 100644 index 0000000..0af16d7 --- /dev/null +++ b/src/files_detector.ts @@ -0,0 +1,66 @@ +/* + * @adonisjs/inertia + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { locatePath } from 'locate-path' +import { Application } from '@adonisjs/core/app' + +export class FilesDetector { + constructor(protected app: Application) {} + + /** + * Try to locate the entrypoint file based + * on the conventional locations + */ + async detectEntrypoint(defaultPath: string) { + const possiblesLocations = [ + './resources/app.ts', + './resources/app.tsx', + './resources/application/app.ts', + './resources/application/app.tsx', + './resources/app.jsx', + './resources/app.js', + './resources/application/app.jsx', + './resources/application/app.js', + ] + + const path = await locatePath(possiblesLocations, { cwd: this.app.appRoot }) + return this.app.makePath(path || defaultPath) + } + + /** + * Try to locate the SSR entrypoint file based + * on the conventional locations + */ + async detectSsrEntrypoint(defaultPath: string) { + const possiblesLocations = [ + './resources/ssr.ts', + './resources/ssr.tsx', + './resources/application/ssr.ts', + './resources/application/ssr.tsx', + './resources/ssr.jsx', + './resources/ssr.js', + './resources/application/ssr.jsx', + './resources/application/ssr.js', + ] + + const path = await locatePath(possiblesLocations, { cwd: this.app.appRoot }) + return this.app.makePath(path || defaultPath) + } + + /** + * Try to locate the SSR bundle file based + * on the conventional locations + */ + async detectSsrBundle(defaultPath: string) { + const possiblesLocations = ['./ssr/ssr.js', './ssr/ssr.mjs'] + + const path = await locatePath(possiblesLocations, { cwd: this.app.appRoot }) + return this.app.makePath(path || defaultPath) + } +} diff --git a/src/inertia.ts b/src/inertia.ts index 5909002..e9d20c7 100644 --- a/src/inertia.ts +++ b/src/inertia.ts @@ -9,9 +9,11 @@ /// +import type { ViteDevServer } from 'vite' import type { ViteRuntime } from 'vite/runtime' import type { HttpContext } from '@adonisjs/core/http' +import { ServerRenderer } from './server_renderer.js' import type { Data, MaybePromise, @@ -31,13 +33,16 @@ const kLazySymbol = Symbol('lazy') */ export class Inertia { #sharedData: SharedData = {} + #serverRenderer: ServerRenderer constructor( protected ctx: HttpContext, protected config: ResolvedConfig, - protected viteRuntime?: ViteRuntime + protected viteRuntime?: ViteRuntime, + protected viteDevServer?: ViteDevServer ) { this.#sharedData = config.sharedData + this.#serverRenderer = new ServerRenderer(config, viteRuntime, viteDevServer) } /** @@ -121,24 +126,13 @@ export class Inertia { /** * Render the page on the server - * - * On development, we use the Vite Runtime API - * On production, we just import and use the SSR bundle generated by Vite */ async #renderOnServer(pageObject: PageObject, viewProps?: Record) { - let render: { default: (page: any) => Promise<{ head: string; body: string }> } - - if (this.viteRuntime) { - render = await this.viteRuntime.executeEntrypoint(this.config.ssr.entrypoint!) - } else { - render = await import(this.config.ssr.bundle!) - } - - const result = await render.default(pageObject) + const { head, body } = await this.#serverRenderer.render(pageObject) return this.ctx.view.render(this.config.rootView, { ...viewProps, - page: { ssrHead: result.head, ssrBody: result.body, ...pageObject }, + page: { ssrHead: head, ssrBody: body, ...pageObject }, }) } diff --git a/src/inertia_middleware.ts b/src/inertia_middleware.ts index 7ec2249..46992bf 100644 --- a/src/inertia_middleware.ts +++ b/src/inertia_middleware.ts @@ -7,7 +7,9 @@ * file that was distributed with this source code. */ +import type { ViteDevServer } from 'vite' import type { Vite } from '@adonisjs/vite' +import type { ViteRuntime } from 'vite/runtime' import type { HttpContext } from '@adonisjs/core/http' import type { NextFn } from '@adonisjs/core/types/http' @@ -28,19 +30,21 @@ declare module '@adonisjs/core/http' { * set appropriate headers/status */ export default class InertiaMiddleware { - #runtime: ReturnType | undefined + #runtime?: ViteRuntime + #viteDevServer?: ViteDevServer constructor( protected config: ResolvedConfig, - vite?: Vite + protected vite?: Vite ) { this.#runtime = vite?.getRuntime() + this.#viteDevServer = vite?.getDevServer() } async handle(ctx: HttpContext, next: NextFn) { const { response, request } = ctx - ctx.inertia = new Inertia(ctx, this.config, this.#runtime) + ctx.inertia = new Inertia(ctx, this.config, this.#runtime, this.#viteDevServer) await next() diff --git a/src/server_renderer.ts b/src/server_renderer.ts new file mode 100644 index 0000000..85b769b --- /dev/null +++ b/src/server_renderer.ts @@ -0,0 +1,150 @@ +/* + * @adonisjs/inertia + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { ViteRuntime } from 'vite/runtime' +import type { ModuleNode, ViteDevServer } from 'vite' +import type { PageObject, ResolvedConfig } from './types.js' + +const styleFileRE = /\.(css|less|sass|scss|styl|stylus|pcss|postcss)($|\?)/ + +/** + * Responsible for rendering page on the server + * + * - In development, we use the Vite Runtime API + * - In production, we just import and use the SSR + * bundle generated by Vite + */ +export class ServerRenderer { + constructor( + protected config: ResolvedConfig, + protected viteRuntime?: ViteRuntime, + protected viteDevServer?: ViteDevServer + ) {} + + /** + * If the module is a style module + */ + #isStyle(mod: ModuleNode) { + if (styleFileRE.test(mod.url) || (mod.id && /\?vue&type=style/.test(mod.id))) { + return true + } + return false + } + + /** + * Collect CSS files from the module graph recursively + */ + #collectCss( + mod: ModuleNode, + styleUrls: Set, + visitedModules: Set, + importer?: ModuleNode + ): void { + if (!mod.url) return + + /** + * Prevent visiting the same module twice + */ + if (visitedModules.has(mod.url)) return + visitedModules.add(mod.url) + + if (this.#isStyle(mod) && (!importer || !this.#isStyle(importer))) { + if (mod.url.startsWith('/')) { + styleUrls.add(mod.url) + } else if (mod.url.startsWith('\0')) { + // virtual modules are prefixed with \0 + styleUrls.add(`/@id/__x00__${mod.url.substring(1)}`) + } else { + styleUrls.add(`/@id/${mod.url}`) + } + } + + mod.importedModules.forEach((dep) => this.#collectCss(dep, styleUrls, visitedModules, mod)) + } + + /** + * Generate the preload tag for a CSS file + */ + #getPreloadTag(href: string) { + return `` + } + + /** + * Find a page module from the entrypoint module + * + * The implementation is dumb, we are just looking for the first module + * imported by the entrypoint module that matches the regex + */ + #findPageModule(entryMod: ModuleNode | undefined, pageObject: PageObject) { + const pattern = `${pageObject.component.replace(/\//g, '\\/')}.(tsx|vue|svelte|jsx|ts|js)$` + const regex = new RegExp(pattern) + + return [...(entryMod?.ssrImportedModules || [])].find((dep) => regex.test(dep.url)) + } + + /** + * Render the page on the server + * + * On development, we use the Vite Runtime API + * On production, we just import and use the SSR bundle generated by Vite + */ + async render(pageObject: PageObject) { + let render: { default: (page: any) => Promise<{ head: string; body: string }> } + let preloadTags: string[] = [] + + /** + * Use the Vite Runtime API to execute the entrypoint + * if we are in development mode + */ + if (this.viteRuntime && this.viteDevServer) { + render = await this.viteRuntime.executeEntrypoint(this.config.ssr.entrypoint!) + + /** + * We need to collect the CSS files to preload them + * Otherwise, we gonna have a FOUC each time we full reload the page + * + * First, we need to get the client-side entrypoint module + */ + const entryMod = this.viteDevServer.moduleGraph.getModuleById(this.config.entrypoint) + + /** + * We should also get the page component that will be rendered. So + * we analyze the module graph to find the module that matches the + * page component + * + * Then execute it with Vite Runtime so the module graph is populated + */ + const pageMod = this.#findPageModule(entryMod, pageObject) + if (pageMod) await this.viteRuntime.executeUrl(pageMod.url) + + /** + * Then we can finally collect the CSS files + */ + const preloadUrls = new Set() + const visitedModules = new Set() + + if (pageMod) this.#collectCss(pageMod, preloadUrls, visitedModules) + if (entryMod) this.#collectCss(entryMod, preloadUrls, visitedModules) + + preloadTags = Array.from(preloadUrls).map(this.#getPreloadTag) + } else { + /** + * Otherwise, just import the SSR bundle + */ + render = await import(this.config.ssr.bundle!) + } + + /** + * Call the render function and return head and body + */ + const result = await render.default(pageObject) + const head = preloadTags.concat(result.head) + return { head, body: result.body } + } +} diff --git a/src/types.ts b/src/types.ts index 5398dc7..00969fb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -36,6 +36,11 @@ export interface InertiaConfig { */ rootView?: string + /** + * Path to your client-side entrypoint file. + */ + entrypoint?: string + /** * The version of your assets. Every client request will be checked against this version. * If the version is not the same, the client will do a full reload. @@ -80,6 +85,7 @@ export interface ResolvedConfig { rootView: string versionCache: VersionCache sharedData: SharedData + entrypoint: string ssr: { enabled: boolean entrypoint: string diff --git a/tests/configure.spec.ts b/tests/configure.spec.ts index 0bfcb57..7686929 100644 --- a/tests/configure.spec.ts +++ b/tests/configure.spec.ts @@ -9,33 +9,9 @@ import { test } from '@japa/runner' import { FileSystem } from '@japa/file-system' -import { IgnitorFactory } from '@adonisjs/core/factories' import Configure from '@adonisjs/core/commands/configure' -import { BASE_URL } from '../tests_helpers/index.js' - -async function setupApp() { - const ignitor = new IgnitorFactory() - .withCoreProviders() - .withCoreConfig() - .create(BASE_URL, { - importer: (filePath) => { - if (filePath.startsWith('./') || filePath.startsWith('../')) { - return import(new URL(filePath, BASE_URL).href) - } - - return import(filePath) - }, - }) - - const app = ignitor.createApp('web') - await app.init().then(() => app.boot()) - - const ace = await app.container.make('ace') - ace.ui.switchMode('raw') - - return { ace, app } -} +import { setupApp } from '../tests_helpers/index.js' async function setupFakeAdonisproject(fs: FileSystem) { await Promise.all([ diff --git a/tests/define_config.spec.ts b/tests/define_config.spec.ts new file mode 100644 index 0000000..f02d41b --- /dev/null +++ b/tests/define_config.spec.ts @@ -0,0 +1,53 @@ +/* + * @adonisjs/inertia + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { join } from 'node:path' +import { test } from '@japa/runner' +import { slash } from '@poppinss/utils' + +import { defineConfig } from '../index.js' +import { setupApp } from '../tests_helpers/index.js' + +test.group('Define Config', () => { + test('detect entrypoint automatically - "{$self}"') + .with(['resources/application/app.tsx', 'resources/app.ts', 'resources/app.tsx']) + .run(async ({ assert, fs }, filePath) => { + const { app } = await setupApp() + const configProvider = defineConfig({}) + await fs.create(filePath, '') + + const result = await configProvider.resolver(app) + + assert.deepEqual(result.entrypoint, slash(join(app.makePath(filePath)))) + }) + + test('detect bundle automatically - "{$self}"') + .with(['ssr/ssr.js', 'ssr/ssr.mjs']) + .run(async ({ assert, fs }, filePath) => { + const { app } = await setupApp() + const configProvider = defineConfig({}) + await fs.create(filePath, '') + + const result = await configProvider.resolver(app) + + assert.deepEqual(result.ssr.bundle, join(app.makePath(filePath))) + }) + + test('detect ssr entrypoint automatically - "{$self}"') + .with(['resources/application/ssr.tsx', 'resources/ssr.ts', 'resources/ssr.tsx']) + .run(async ({ assert, fs }, filePath) => { + const { app } = await setupApp() + const configProvider = defineConfig({}) + await fs.create(filePath, '') + + const result = await configProvider.resolver(app) + + assert.deepEqual(result.ssr.entrypoint, join(app.makePath(filePath))) + }) +}) diff --git a/tests/inertia.spec.ts b/tests/inertia.spec.ts index bba5aef..06f1ad0 100644 --- a/tests/inertia.spec.ts +++ b/tests/inertia.spec.ts @@ -7,12 +7,13 @@ * file that was distributed with this source code. */ +import { join } from 'node:path' import { test } from '@japa/runner' import { HttpContext } from '@adonisjs/core/http' import { HttpContextFactory, RequestFactory } from '@adonisjs/core/factories/http' -import { setupViewMacroMock } from '../tests_helpers/index.js' import { InertiaFactory } from '../factories/inertia_factory.js' +import { setupViewMacroMock, setupVite } from '../tests_helpers/index.js' test.group('Inertia', () => { test('location should returns x-inertia-location with 409 code', async ({ assert }) => { @@ -259,28 +260,21 @@ test.group('Inertia', () => { test.group('Inertia | Ssr', () => { test('if viteRuntime is available, use entrypoint file to render the page', async ({ assert, + fs, }) => { setupViewMacroMock() + const vite = await setupVite({ build: { rollupOptions: { input: 'foo.ts' } } }) + + await fs.create('foo.ts', 'export default () => ({ head: "head", body: "foo.ts" })') const inertia = await new InertiaFactory() - .merge({ - config: { - ssr: { - enabled: true, - entrypoint: 'foo.ts', - }, - }, - }) - .withViteRuntime({ - async executeEntrypoint(path) { - return { default: () => ({ head: 'head', body: path }) } - }, - }) + .merge({ config: { ssr: { enabled: true, entrypoint: 'foo.ts' } } }) + .withVite(vite) .create() const result: any = await inertia.render('foo') - assert.deepEqual(result.props.page.ssrHead, 'head') + assert.deepEqual(result.props.page.ssrHead, ['head']) assert.deepEqual(result.props.page.ssrBody, 'foo.ts') }) @@ -293,72 +287,154 @@ test.group('Inertia | Ssr', () => { await fs.create('foo.js', 'export default () => ({ head: "head", body: "foo.ts" })') const inertia = await new InertiaFactory() + .merge({ config: { ssr: { enabled: true, bundle: new URL('foo.js', fs.baseUrl).href } } }) + .create() + + const result: any = await inertia.render('foo') + + assert.deepEqual(result.props.page.ssrBody, 'foo.ts') + assert.deepEqual(result.props.page.ssrHead, ['head']) + }) + + test('enable only for listed pages', async ({ assert, fs }) => { + setupViewMacroMock() + const vite = await setupVite({ build: { rollupOptions: { input: 'foo.ts' } } }) + + await fs.create('foo.ts', 'export default () => ({ head: "head", body: "foo.ts" })') + + const inertia = await new InertiaFactory() + .withVite(vite) + .merge({ config: { ssr: { enabled: true, entrypoint: 'foo.ts', pages: ['foo'] } } }) + .create() + + const result: any = await inertia.render('foo') + const result2: any = await inertia.render('bar') + + assert.deepEqual(result.props.page.ssrBody, 'foo.ts') + assert.notExists(result2.props.page.ssrBody) + }) + + test('should pass page object to the view', async ({ assert, fs }) => { + setupViewMacroMock() + const vite = await setupVite({ build: { rollupOptions: { input: 'foo.ts' } } }) + + await fs.create('foo.ts', 'export default () => ({ head: "head", body: "foo.ts" })') + + const inertia = await new InertiaFactory() + .withVite(vite) + .merge({ config: { ssr: { enabled: true, entrypoint: 'foo.ts' } } }) + .create() + + const result: any = await inertia.render('foo') + + assert.deepEqual(result.props.page.component, 'foo') + assert.deepEqual(result.props.page.version, '1') + }) +}) + +test.group('Inertia | Ssr | CSS Preloading', () => { + test('collect and preload css files of entrypoint', async ({ assert, fs }) => { + setupViewMacroMock() + const vite = await setupVite({ build: { rollupOptions: { input: 'foo.ts' } } }) + + await fs.create( + 'foo.ts', + ` + import './style.css' + export default () => ({ head: "head", body: "foo.ts" }) + ` + ) + + await fs.create('style.css', 'body { color: red }') + + const inertia = await new InertiaFactory() + .withVite(vite) .merge({ config: { - ssr: { - enabled: true, - bundle: new URL('foo.js', fs.baseUrl).href, - }, + entrypoint: join(fs.basePath, 'foo.ts'), + ssr: { enabled: true, entrypoint: 'foo.ts' }, }, }) .create() const result: any = await inertia.render('foo') - assert.deepEqual(result.props.page.ssrBody, 'foo.ts') - assert.deepEqual(result.props.page.ssrHead, 'head') + assert.deepEqual(result.props.page.ssrHead, [ + '', + 'head', + ]) }) - test('enable only for listed pages', async ({ assert }) => { + test('collect recursively css files of entrypoint', async ({ assert, fs }) => { setupViewMacroMock() + const vite = await setupVite({ build: { rollupOptions: { input: 'foo.ts' } } }) + + await fs.create( + 'foo.ts', + ` + import './foo2.ts' + import './style.css' + export default () => ({ head: "head", body: "foo.ts" }) + ` + ) + + await fs.create('foo2.ts', `import './style2.css'`) + await fs.create('style.css', 'body { color: red }') + await fs.create('style2.css', 'body { color: blue }') const inertia = await new InertiaFactory() - .withViteRuntime({ - async executeEntrypoint(path) { - return { default: () => ({ head: 'head', body: path }) } - }, - }) + .withVite(vite) .merge({ config: { - ssr: { - enabled: true, - entrypoint: 'foo.ts', - pages: ['foo'], - }, + entrypoint: join(fs.basePath, 'foo.ts'), + ssr: { enabled: true, entrypoint: 'foo.ts', pages: ['foo'] }, }, }) .create() const result: any = await inertia.render('foo') - const result2: any = await inertia.render('bar') - assert.deepEqual(result.props.page.ssrBody, 'foo.ts') - assert.notExists(result2.props.page.ssrBody) + assert.deepEqual(result.props.page.ssrHead, [ + '', + '', + 'head', + ]) }) - test('should pass page object to the view', async ({ assert }) => { + test('collect css rendered page', async ({ assert, fs }) => { setupViewMacroMock() + const vite = await setupVite({ build: { rollupOptions: { input: 'foo.ts' } } }) + + await fs.create( + 'app.ts', + ` + import './style.css' + + import.meta.glob('./pages/**/*.tsx') + export default () => ({ head: "head", body: "foo.ts" }) + ` + ) + await fs.create('style.css', 'body { color: red }') + + await fs.create('./pages/home/main.tsx', `import './style2.css'`) + await fs.create('./pages/home/style2.css', 'body { color: blue }') const inertia = await new InertiaFactory() - .withViteRuntime({ - async executeEntrypoint(path) { - return { default: () => ({ head: 'head', body: path }) } - }, - }) + .withVite(vite) .merge({ config: { - ssr: { - enabled: true, - entrypoint: 'foo.ts', - pages: ['foo'], - }, + entrypoint: join(fs.basePath, 'app.ts'), + ssr: { enabled: true, entrypoint: 'app.ts' }, }, }) .create() - const result: any = await inertia.render('foo') + const result: any = await inertia.render('home/main') - assert.deepEqual(result.props.page.component, 'foo') - assert.deepEqual(result.props.page.version, '1') + assert.deepEqual(result.props.page.ssrHead, [ + '', + '', + 'head', + ]) }) }) diff --git a/tests/middleware.spec.ts b/tests/middleware.spec.ts index e54b385..caa1886 100644 --- a/tests/middleware.spec.ts +++ b/tests/middleware.spec.ts @@ -24,6 +24,7 @@ test.group('Middleware', () => { const middleware = new InertiaMiddleware({ rootView: 'root', sharedData: {}, + entrypoint: 'app.ts', versionCache: new VersionCache(new URL(import.meta.url), '1'), ssr: { enabled: false, bundle: '', entrypoint: '' }, }) @@ -46,6 +47,7 @@ test.group('Middleware', () => { const middleware = new InertiaMiddleware({ rootView: 'root', sharedData: {}, + entrypoint: 'app.ts', versionCache: new VersionCache(new URL(import.meta.url), '1'), ssr: { enabled: false, bundle: '', entrypoint: '' }, }) @@ -75,6 +77,7 @@ test.group('Middleware', () => { const middleware = new InertiaMiddleware({ rootView: 'root', sharedData: {}, + entrypoint: 'app.ts', versionCache: new VersionCache(new URL(import.meta.url), '1'), ssr: { enabled: false, bundle: '', entrypoint: '' }, }) @@ -104,6 +107,7 @@ test.group('Middleware', () => { const middleware = new InertiaMiddleware({ rootView: 'root', sharedData: {}, + entrypoint: 'app.ts', versionCache: new VersionCache(new URL(import.meta.url), '1'), ssr: { enabled: false, bundle: '', entrypoint: '' }, }) @@ -131,6 +135,7 @@ test.group('Middleware', () => { const middleware = new InertiaMiddleware({ rootView: 'root', sharedData: {}, + entrypoint: 'app.ts', versionCache: new VersionCache(new URL(import.meta.url), '1'), ssr: { enabled: false, bundle: '', entrypoint: '' }, }) @@ -152,6 +157,7 @@ test.group('Middleware', () => { const middleware = new InertiaMiddleware({ rootView: 'root', sharedData: {}, + entrypoint: 'app.ts', versionCache: version, ssr: { enabled: false, bundle: '', entrypoint: '' }, }) @@ -183,6 +189,7 @@ test.group('Middleware', () => { const middleware = new InertiaMiddleware({ rootView: 'root', sharedData: {}, + entrypoint: 'app.ts', versionCache: version, ssr: { enabled: false, bundle: '', entrypoint: '' }, }) diff --git a/tests/plugins/api_client.spec.ts b/tests/plugins/api_client.spec.ts index 34e4f14..07db086 100644 --- a/tests/plugins/api_client.spec.ts +++ b/tests/plugins/api_client.spec.ts @@ -74,6 +74,7 @@ test.group('Japa plugin | Api Client', (group) => { versionCache: new VersionCache(new URL(import.meta.url), '1'), rootView: 'root', sharedData: {}, + entrypoint: 'app.ts', ssr: { enabled: false, bundle: '', entrypoint: '' }, }) @@ -115,6 +116,7 @@ test.group('Japa plugin | Api Client', (group) => { versionCache: new VersionCache(new URL(import.meta.url), '1'), rootView: 'root', sharedData: {}, + entrypoint: 'app.ts', ssr: { enabled: false, bundle: '', entrypoint: '' }, }) @@ -149,6 +151,7 @@ test.group('Japa plugin | Api Client', (group) => { versionCache: new VersionCache(new URL(import.meta.url), '1'), rootView: 'root', sharedData: {}, + entrypoint: 'app.ts', ssr: { enabled: false, bundle: '', entrypoint: '' }, }) diff --git a/tests/provider.spec.ts b/tests/provider.spec.ts index affd3fd..5ac8be6 100644 --- a/tests/provider.spec.ts +++ b/tests/provider.spec.ts @@ -14,7 +14,7 @@ const IMPORTER = (filePath: string) => { } test.group('Inertia Provider', () => { - test('register inertia middleware singleton', async ({ assert }) => { + test('register inertia middleware singleton', async ({ assert, cleanup }) => { const ignitor = new IgnitorFactory() .merge({ rcFileContents: { @@ -35,6 +35,8 @@ test.group('Inertia Provider', () => { await app.init() await app.boot() + cleanup(() => app.terminate()) + assert.instanceOf(await app.container.make(InertiaMiddleware), InertiaMiddleware) }) }) diff --git a/tests_helpers/index.ts b/tests_helpers/index.ts index 791a779..0b9ac7a 100644 --- a/tests_helpers/index.ts +++ b/tests_helpers/index.ts @@ -1,12 +1,20 @@ -import type { Test } from '@japa/runner/core' +import { ViteRuntime } from 'vite/runtime' import { getActiveTest } from '@japa/runner' +import type { Test } from '@japa/runner/core' import { HttpContext } from '@adonisjs/core/http' import { pluginAdonisJS } from '@japa/plugin-adonisjs' import { ApiClient, apiClient } from '@japa/api-client' import { ApplicationService } from '@adonisjs/core/types' +import { IgnitorFactory } from '@adonisjs/core/factories' import { NamedReporterContract } from '@japa/runner/types' import { runner, syncReporter } from '@japa/runner/factories' import { IncomingMessage, ServerResponse, createServer } from 'node:http' +import { + createServer as createViteServer, + createViteRuntime, + ViteDevServer, + InlineConfig, +} from 'vite' import { inertiaApiClient } from '../src/plugins/japa/api_client.js' @@ -59,3 +67,60 @@ export async function runJapaTest(app: ApplicationService, callback: Parameters< }) .runTest('testing japa integration', callback) } + +/** + * Spin up a Vite server for the test + */ +export async function setupVite(options: InlineConfig): Promise<{ + devServer: ViteDevServer + runtime: ViteRuntime +}> { + const test = getActiveTest() + if (!test) throw new Error('Cannot use setupVite outside a test') + + /** + * Create a dummy file to ensure the root directory exists + * otherwise Vite will throw an error + */ + await test.context.fs.create('dummy.txt', 'dummy') + + const devServer = await createViteServer({ + server: { middlewareMode: true, hmr: false }, + clearScreen: false, + logLevel: 'silent', + root: test.context.fs.basePath, + ...options, + }) + + const runtime = await createViteRuntime(devServer) + + test.cleanup(() => devServer.close()) + + return { devServer, runtime } +} + +/** + * Setup an AdonisJS app for testing + */ +export async function setupApp() { + const ignitor = new IgnitorFactory() + .withCoreProviders() + .withCoreConfig() + .create(BASE_URL, { + importer: (filePath) => { + if (filePath.startsWith('./') || filePath.startsWith('../')) { + return import(new URL(filePath, BASE_URL).href) + } + + return import(filePath) + }, + }) + + const app = ignitor.createApp('web') + await app.init().then(() => app.boot()) + + const ace = await app.container.make('ace') + ace.ui.switchMode('raw') + + return { ace, app } +}