import type { NuxtOptions } from '@nuxt/schema'; import { CreateDependencies, CreateNodes, CreateNodesContext, createNodesFromFiles, CreateNodesV2, detectPackageManager, getPackageManagerCommand, readJsonFile, TargetConfiguration, workspaceRoot, writeJsonFile, } from '@nx/devkit'; import { loadConfigFile } from '@nx/devkit/src/utils/config-utils'; import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs'; import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes'; import { workspaceDataDirectory } from 'nx/src/utils/cache-directory'; import { getLockFileName } from '@nx/js'; import { dirname, isAbsolute, join, relative } from 'path'; import { existsSync, readdirSync } from 'fs'; import { loadNuxtKitDynamicImport } from '../utils/executor-utils'; import { addBuildAndWatchDepsTargets } from '@nx/js/src/plugins/typescript/util'; const cachePath = join(workspaceDataDirectory, 'nuxt.hash'); const targetsCache = readTargetsCache(); const pmc = getPackageManagerCommand(); function readTargetsCache(): Record< string, Record > { return existsSync(cachePath) ? readJsonFile(cachePath) : {}; } function writeTargetsToCache() { const oldCache = readTargetsCache(); writeJsonFile(cachePath, { ...oldCache, ...targetsCache, }); } export interface NuxtPluginOptions { buildTargetName?: string; serveTargetName?: string; serveStaticTargetName?: string; buildStaticTargetName?: string; buildDepsTargetName?: string; watchDepsTargetName?: string; } export const createNodesV2: CreateNodesV2 = [ '**/nuxt.config.{js,ts,mjs,mts,cjs,cts}', async (files, options, context) => { //TODO(@nrwl/nx-vue-reviewers): This should batch hashing like our other plugins. const result = await createNodesFromFiles( createNodes[1], files, options, context ); writeTargetsToCache(); return result; }, ]; export const createNodes: CreateNodes = [ '**/nuxt.config.{js,ts,mjs,mts,cjs,cts}', async (configFilePath, options, context) => { const projectRoot = dirname(configFilePath); // Do not create a project if package.json and project.json isn't there. const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot)); if ( !siblingFiles.includes('package.json') && !siblingFiles.includes('project.json') ) { return {}; } options = normalizeOptions(options); const hash = await calculateHashForCreateNodes( projectRoot, options, context, [getLockFileName(detectPackageManager(context.workspaceRoot))] ); targetsCache[hash] ??= await buildNuxtTargets( configFilePath, projectRoot, options, context ); return { projects: { [projectRoot]: { root: projectRoot, targets: targetsCache[hash], }, }, }; }, ]; async function buildNuxtTargets( configFilePath: string, projectRoot: string, options: NuxtPluginOptions, context: CreateNodesContext ) { const nuxtConfig: { buildDir: string; } = await getInfoFromNuxtConfig(configFilePath, context, projectRoot); const { buildOutputs } = getOutputs(nuxtConfig, projectRoot); const namedInputs = getNamedInputs(projectRoot, context); const targets: Record = {}; targets[options.buildTargetName] = buildTarget( options.buildTargetName, namedInputs, buildOutputs, projectRoot ); targets[options.serveTargetName] = serveTarget(projectRoot); targets[options.serveStaticTargetName] = serveStaticTarget(options); targets[options.buildStaticTargetName] = buildStaticTarget( options.buildStaticTargetName, namedInputs, buildOutputs, projectRoot ); addBuildAndWatchDepsTargets( context.workspaceRoot, projectRoot, targets, options, pmc ); return targets; } function buildTarget( buildTargetName: string, namedInputs: { [inputName: string]: any[]; }, buildOutputs: string[], projectRoot: string ) { return { command: `nuxt build`, options: { cwd: projectRoot }, cache: true, dependsOn: [`^${buildTargetName}`], inputs: [ ...('production' in namedInputs ? ['production', '^production'] : ['default', '^default']), { externalDependencies: ['nuxt'], }, ], outputs: buildOutputs, }; } function serveTarget(projectRoot: string) { const targetConfig: TargetConfiguration = { command: `nuxt dev`, options: { cwd: projectRoot, }, }; return targetConfig; } function serveStaticTarget(options: NuxtPluginOptions) { const targetConfig: TargetConfiguration = { executor: '@nx/web:file-server', options: { buildTarget: `${options.buildStaticTargetName}`, staticFilePath: '{projectRoot}/dist', port: 4200, // Routes are found correctly with serve-static spa: false, }, }; return targetConfig; } function buildStaticTarget( buildStaticTargetName: string, namedInputs: { [inputName: string]: any[]; }, buildOutputs: string[], projectRoot: string ) { const targetConfig: TargetConfiguration = { command: `nuxt build --prerender`, options: { cwd: projectRoot }, cache: true, dependsOn: [`^${buildStaticTargetName}`], inputs: [ ...('production' in namedInputs ? ['production', '^production'] : ['default', '^default']), { externalDependencies: ['nuxt'], }, ], outputs: buildOutputs, }; return targetConfig; } async function getInfoFromNuxtConfig( configFilePath: string, context: CreateNodesContext, projectRoot: string ): Promise<{ buildDir: string; }> { let config: NuxtOptions; if (process.env.NX_ISOLATE_PLUGINS !== 'false') { config = await ( await loadNuxtKitDynamicImport() ).loadNuxtConfig({ configFile: configFilePath, }); } else { config = await loadConfigFile(join(context.workspaceRoot, configFilePath)); } return { buildDir: config?.buildDir ?? // Match .nuxt default build dir from '@nuxt/schema' // See: https://github.com/nuxt/nuxt/blob/871404ae5673425aeedde82f123ea58aa7c6facf/packages/schema/src/config/common.ts#L117-L119 '.nuxt', }; } function getOutputs( nuxtConfig: { buildDir: string }, projectRoot: string ): { buildOutputs: string[]; } { const buildOutputPath = normalizeOutputPath( nuxtConfig?.buildDir, projectRoot ); return { buildOutputs: [buildOutputPath, '{projectRoot}/.output'], }; } function normalizeOutputPath( outputPath: string | undefined, projectRoot: string ): string { if (!outputPath) { if (projectRoot === '.') { return `{projectRoot}`; } else { return `{workspaceRoot}/{projectRoot}`; } } else { if (isAbsolute(outputPath)) { return `{workspaceRoot}/${relative(workspaceRoot, outputPath)}`; } else { if (outputPath.startsWith('..')) { return join('{workspaceRoot}', join(projectRoot, outputPath)); } else { return join('{projectRoot}', outputPath); } } } } function normalizeOptions(options: NuxtPluginOptions): NuxtPluginOptions { options ??= {}; options.buildTargetName ??= 'build'; options.serveTargetName ??= 'serve'; options.serveStaticTargetName ??= 'serve-static'; options.buildStaticTargetName ??= 'build-static'; return options; }