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
58 changes: 56 additions & 2 deletions napi/angular-compiler/test/build-optimizer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'nod
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'

import { normalizePath } from 'vite'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { normalizePath, resolveConfig, type InlineConfig } from 'vite'
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'

import { buildOptimizerPlugin } from '../vite-plugin/angular-build-optimizer-plugin.js'

Expand Down Expand Up @@ -181,3 +181,57 @@ describe('build optimizer: package sideEffects', () => {
expect(await optimize('@angular/build/fesm2022/build.mjs', plugin)).not.toContain('__PURE__')
})
})

describe('build optimizer: ngServerMode', () => {
function resolveBuildConfig(config: InlineConfig, mode = 'production') {
return resolveConfig(
{
configFile: false,
logLevel: 'silent',
mode,
plugins: [
buildOptimizerPlugin({ jit: false, sourcemap: false, thirdPartySourcemaps: false }),
],
...config,
},
'build',
)
}

// The `ngServerMode` each environment of the resolved build config defines.
async function ngServerModes(config: InlineConfig, mode?: string) {
const resolved = await resolveBuildConfig(config, mode)
return Object.fromEntries(
Object.entries(resolved.environments).map(([name, env]) => [name, env.define?.ngServerMode]),
)
}

it('defines it per environment when one config builds the client and the server', async () => {
expect(
await ngServerModes({ environments: { client: {}, ssr: {}, edge: { consumer: 'server' } } }),
).toEqual({ client: 'false', ssr: 'true', edge: 'true' })
})

it('defines it as true for a separate `vite build --ssr`', async () => {
expect(await ngServerModes({ build: { ssr: 'src/server.ts' } })).toMatchObject({ ssr: 'true' })
})

afterEach(() => {
vi.unstubAllEnvs()
})

it('only defines it in server environments outside production', async () => {
// The plugin also treats `NODE_ENV=production` as production.
vi.stubEnv('NODE_ENV', 'development')
expect(await ngServerModes({ environments: { client: {}, ssr: {} } }, 'development')).toEqual({
client: undefined,
ssr: 'true',
})
})

it('leaves it out of the defines all environments share', async () => {
const resolved = await resolveBuildConfig({})
expect(resolved.define).not.toHaveProperty('ngServerMode')
expect(resolved.oxc && resolved.oxc.define).not.toHaveProperty('ngServerMode')
})
})
44 changes: 44 additions & 0 deletions napi/angular-compiler/test/ssr-manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,5 +109,49 @@ describe('SSR Manifest Generation (Issue #60)', () => {
const plugin = ssrManifestPlugin({})
expect(plugin.apply).toBe('build')
})

it('should inject the manifests only in server environments', () => {
// One config can build the client and the server environments
// (`vite build --app`); `vite build --ssr` builds the `ssr` one.
const plugin = ssrManifestPlugin({})
;(plugin.configResolved as Function)({ root: '/app', base: '/' })
const transform = (consumer: 'client' | 'server') =>
(plugin.transform as Function).call(
{ environment: { config: { consumer } } },
'const engine = new AngularNodeAppEngine();',
'/app/src/server.ts',
)

expect(transform('client')).toBeUndefined()
expect(transform('server')?.code).toContain('ɵsetAngularAppManifest')
})

it('should not inject the manifests twice', () => {
// A server environment can bundle another one's output (Nitro).
const plugin = ssrManifestPlugin({})
;(plugin.configResolved as Function)({ root: '/app', base: '/' })
const transform = (code: string) =>
(plugin.transform as Function).call(
{ environment: { config: { consumer: 'server' } } },
code,
'/app/.nitro/vite/services/ssr/index.mjs',
)

const once = transform('const engine = new AngularNodeAppEngine();').code
expect(transform(once)).toBeUndefined()
})

it('should do nothing outside Vite (plain rolldown has no environment)', async () => {
const plugin = ssrManifestPlugin({})
;(plugin.configResolved as Function)({ root: '/app', base: '/' })
await (plugin.buildStart as Function).call({})
expect(
(plugin.transform as Function).call(
{},
'const engine = new AngularNodeAppEngine();',
'/app/src/server.ts',
),
).toBeUndefined()
})
})
})
28 changes: 14 additions & 14 deletions napi/angular-compiler/vite-plugin/angular-build-optimizer-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,39 +87,39 @@ export function buildOptimizerPlugin({
apply: 'build',
config(userConfig) {
isProd = userConfig.mode === 'production' || process.env['NODE_ENV'] === 'production'
const isSSR = !!userConfig.build?.ssr
const ngServerMode = `${isSSR}`

if (isProd) {
return {
define: {
ngJitMode: jit ? 'true' : 'false',
ngI18nClosureMode: 'false',
ngDevMode: 'false',
ngServerMode,
},
oxc: {
define: {
ngDevMode: 'false',
ngJitMode: jit ? 'true' : 'false',
ngI18nClosureMode: 'false',
ngServerMode,
},
},
}
}

// In dev SSR mode, set ngServerMode even without the full production defines
if (isSSR) {
const defines: Record<string, string> = { ngServerMode }
return {
define: defines,
oxc: {
define: defines,
},
}
return undefined
},
// `ngServerMode` is set per environment: one config can build the browser
// and the server bundles (`vite build --app`, `builder.buildApp`), and
// `vite build --ssr` builds the `ssr` environment. Like the Angular CLI, a
// server bundle gets `true` and a production browser bundle `false`.
configEnvironment(name, config) {
// Vite's default when `consumer` is not set.
const consumer = config.consumer ?? (name === 'client' ? 'client' : 'server')
if (consumer === 'server') {
return { define: { ngServerMode: 'true' } }
}
if (isProd) {
return { define: { ngServerMode: 'false' } }
}

return undefined
},
transform: {
Expand Down
22 changes: 10 additions & 12 deletions napi/angular-compiler/vite-plugin/angular-ssr-manifest-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
* "Angular app engine manifest is not set."
*
* This plugin:
* 1. Detects SSR builds (when Vite's build.ssr is true)
* 1. Detects server builds (environments whose `consumer` is `server`, which
* includes the `ssr` environment `vite build --ssr` builds)
* 2. Auto-injects manifest setup into files that use AngularNodeAppEngine/AngularAppEngine
* 3. Provides the index.html as a server asset for SSR rendering
*
Expand Down Expand Up @@ -112,7 +113,6 @@ export interface SsrManifestPluginOptions {
* Vite plugin that generates Angular SSR manifests for AngularNodeAppEngine.
*/
export function ssrManifestPlugin(options: SsrManifestPluginOptions): Plugin {
let isSSR = false
let resolvedConfig: ResolvedConfig
let ssrEntryPath: string
let indexHtmlContent: string | undefined
Expand All @@ -121,15 +121,9 @@ export function ssrManifestPlugin(options: SsrManifestPluginOptions): Plugin {
name: '@oxc-angular/vite-ssr-manifest',
apply: 'build',

config(userConfig) {
isSSR = !!userConfig.build?.ssr
},

configResolved(config) {
resolvedConfig = config

if (!isSSR) return

const workspaceRoot = config.root

// Determine the SSR bootstrap entry (main.server.ts)
Expand All @@ -139,7 +133,8 @@ export function ssrManifestPlugin(options: SsrManifestPluginOptions): Plugin {
},

async buildStart() {
if (!isSSR) return
// `environment` is only set by Vite, not by plain rolldown / tsdown.
if (this.environment?.config.consumer !== 'server') return

// Read index.html for the server asset
const indexHtmlPath = resolve(resolvedConfig.root, 'index.html')
Expand All @@ -153,14 +148,17 @@ export function ssrManifestPlugin(options: SsrManifestPluginOptions): Plugin {
},

transform(code, id) {
if (!isSSR) return
if (this.environment?.config.consumer !== 'server') return

// Inject manifest setup into files that use AngularNodeAppEngine or AngularAppEngine
// These are the SSR entry points that need the manifest before constructing the engine
// These are the SSR entry points that need the manifest before constructing the engine.
// Code that already sets the manifest is skipped: a server environment can bundle
// the output of another one (Nitro builds the `ssr` service, then bundles it).
if (
!id.includes('node_modules') &&
!id.startsWith('\0') &&
(code.includes('AngularNodeAppEngine') || code.includes('AngularAppEngine'))
(code.includes('AngularNodeAppEngine') || code.includes('AngularAppEngine')) &&
!code.includes('ɵsetAngularAppManifest')
) {
const baseHref = resolvedConfig.base || '/'

Expand Down
Loading