nx/packages/nuxt/src/plugins/plugin.ts

305 lines
7.4 KiB
TypeScript

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<string, TargetConfiguration>
> {
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<NuxtPluginOptions> = [
'**/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<NuxtPluginOptions> = [
'**/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<string, TargetConfiguration> = {};
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;
}