266 lines
6.9 KiB
TypeScript
266 lines
6.9 KiB
TypeScript
import {
|
|
CreateDependencies,
|
|
CreateNodes,
|
|
CreateNodesContext,
|
|
detectPackageManager,
|
|
NxJsonConfiguration,
|
|
readJsonFile,
|
|
TargetConfiguration,
|
|
writeJsonFile,
|
|
} from '@nx/devkit';
|
|
import { dirname, extname, join } from 'path';
|
|
|
|
import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs';
|
|
import { existsSync, readdirSync } from 'fs';
|
|
|
|
import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory';
|
|
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
|
|
import { getLockFileName } from '@nx/js';
|
|
|
|
export interface NextPluginOptions {
|
|
buildTargetName?: string;
|
|
devTargetName?: string;
|
|
startTargetName?: string;
|
|
serveStaticTargetName?: string;
|
|
}
|
|
|
|
const cachePath = join(projectGraphCacheDirectory, 'next.hash');
|
|
const targetsCache = existsSync(cachePath) ? readTargetsCache() : {};
|
|
|
|
const calculatedTargets: Record<
|
|
string,
|
|
Record<string, TargetConfiguration>
|
|
> = {};
|
|
|
|
function readTargetsCache(): Record<
|
|
string,
|
|
Record<string, TargetConfiguration>
|
|
> {
|
|
return readJsonFile(cachePath);
|
|
}
|
|
|
|
function writeTargetsToCache(
|
|
targets: Record<string, Record<string, TargetConfiguration>>
|
|
) {
|
|
writeJsonFile(cachePath, targets);
|
|
}
|
|
|
|
export const createDependencies: CreateDependencies = () => {
|
|
writeTargetsToCache(calculatedTargets);
|
|
return [];
|
|
};
|
|
|
|
export const createNodes: CreateNodes<NextPluginOptions> = [
|
|
'**/next.config.{js,cjs,mjs}',
|
|
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 = calculateHashForCreateNodes(projectRoot, options, context, [
|
|
getLockFileName(detectPackageManager(context.workspaceRoot)),
|
|
]);
|
|
|
|
const targets =
|
|
targetsCache[hash] ??
|
|
(await buildNextTargets(configFilePath, projectRoot, options, context));
|
|
|
|
calculatedTargets[hash] = targets;
|
|
|
|
return {
|
|
projects: {
|
|
[projectRoot]: {
|
|
root: projectRoot,
|
|
targets,
|
|
},
|
|
},
|
|
};
|
|
},
|
|
];
|
|
|
|
async function buildNextTargets(
|
|
nextConfigPath: string,
|
|
projectRoot: string,
|
|
options: NextPluginOptions,
|
|
context: CreateNodesContext
|
|
) {
|
|
const nextConfig = await getNextConfig(nextConfigPath, context);
|
|
const namedInputs = getNamedInputs(projectRoot, context);
|
|
|
|
const targets: Record<string, TargetConfiguration> = {};
|
|
|
|
targets[options.buildTargetName] = await getBuildTargetConfig(
|
|
namedInputs,
|
|
projectRoot,
|
|
nextConfig
|
|
);
|
|
|
|
targets[options.devTargetName] = getDevTargetConfig(projectRoot);
|
|
|
|
targets[options.startTargetName] = getStartTargetConfig(options, projectRoot);
|
|
|
|
targets[options.serveStaticTargetName] = getStaticServeTargetConfig(options);
|
|
|
|
return targets;
|
|
}
|
|
|
|
async function getBuildTargetConfig(
|
|
namedInputs: { [inputName: string]: any[] },
|
|
projectRoot: string,
|
|
nextConfig: any
|
|
) {
|
|
const nextOutputPath = await getOutputs(projectRoot, nextConfig);
|
|
// Set output path here so that `withNx` can pick it up.
|
|
const targetConfig: TargetConfiguration = {
|
|
command: `next build`,
|
|
options: {
|
|
cwd: projectRoot,
|
|
},
|
|
dependsOn: ['^build'],
|
|
cache: true,
|
|
inputs: getInputs(namedInputs),
|
|
outputs: [nextOutputPath, `${nextOutputPath}/!(cache)`],
|
|
};
|
|
return targetConfig;
|
|
}
|
|
|
|
function getDevTargetConfig(projectRoot: string) {
|
|
const targetConfig: TargetConfiguration = {
|
|
command: `next dev`,
|
|
options: {
|
|
cwd: projectRoot,
|
|
},
|
|
};
|
|
|
|
return targetConfig;
|
|
}
|
|
|
|
function getStartTargetConfig(options: NextPluginOptions, projectRoot: string) {
|
|
const targetConfig: TargetConfiguration = {
|
|
command: `next start`,
|
|
options: {
|
|
cwd: projectRoot,
|
|
},
|
|
dependsOn: [options.buildTargetName],
|
|
};
|
|
|
|
return targetConfig;
|
|
}
|
|
|
|
function getStaticServeTargetConfig(options: NextPluginOptions) {
|
|
const targetConfig: TargetConfiguration = {
|
|
executor: '@nx/web:file-server',
|
|
options: {
|
|
buildTarget: options.buildTargetName,
|
|
staticFilePath: '{projectRoot}/out',
|
|
port: 3000,
|
|
// Routes are found correctly with serve-static
|
|
spa: false,
|
|
},
|
|
};
|
|
|
|
return targetConfig;
|
|
}
|
|
|
|
async function getOutputs(projectRoot, nextConfig) {
|
|
let dir = '.next';
|
|
const { PHASE_PRODUCTION_BUILD } = require('next/constants');
|
|
|
|
if (typeof nextConfig === 'function') {
|
|
// Works for both async and sync functions.
|
|
const configResult = await Promise.resolve(
|
|
nextConfig(PHASE_PRODUCTION_BUILD, { defaultConfig: {} })
|
|
);
|
|
if (configResult?.distDir) {
|
|
dir = configResult?.distDir;
|
|
}
|
|
} else if (typeof nextConfig === 'object' && nextConfig?.distDir) {
|
|
// If nextConfig is an object, directly use its 'distDir' property.
|
|
dir = nextConfig.distDir;
|
|
}
|
|
|
|
if (projectRoot === '.') {
|
|
return `{projectRoot}/${dir}`;
|
|
} else {
|
|
return `{workspaceRoot}/${projectRoot}/${dir}`;
|
|
}
|
|
}
|
|
|
|
async function getNextConfig(
|
|
configFilePath: string,
|
|
context: CreateNodesContext
|
|
): Promise<any> {
|
|
const resolvedPath = join(context.workspaceRoot, configFilePath);
|
|
|
|
let module;
|
|
if (extname(configFilePath) === '.mjs') {
|
|
module = await loadEsmModule(resolvedPath);
|
|
} else {
|
|
module = load(resolvedPath);
|
|
}
|
|
return module.default ?? module;
|
|
}
|
|
|
|
function normalizeOptions(options: NextPluginOptions): NextPluginOptions {
|
|
options ??= {};
|
|
options.buildTargetName ??= 'build';
|
|
options.devTargetName ??= 'dev';
|
|
options.startTargetName ??= 'start';
|
|
options.serveStaticTargetName ??= 'serve-static';
|
|
return options;
|
|
}
|
|
|
|
function getInputs(
|
|
namedInputs: NxJsonConfiguration['namedInputs']
|
|
): TargetConfiguration['inputs'] {
|
|
return [
|
|
...('production' in namedInputs
|
|
? ['default', '^production']
|
|
: ['default', '^default']),
|
|
{
|
|
externalDependencies: ['next'],
|
|
},
|
|
];
|
|
}
|
|
|
|
const packageInstallationDirectories = ['node_modules', '.yarn'];
|
|
/**
|
|
* Load the module after ensuring that the require cache is cleared.
|
|
*/
|
|
function load(path: string): any {
|
|
// Clear cache if the path is in the cache
|
|
if (require.cache[path]) {
|
|
for (const key of Object.keys(require.cache)) {
|
|
if (!packageInstallationDirectories.some((dir) => key.includes(dir))) {
|
|
delete require.cache[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Then require
|
|
return require(path);
|
|
}
|
|
|
|
/**
|
|
* Lazily compiled dynamic import loader function.
|
|
*/
|
|
let dynamicLoad: (<T>(modulePath: string | URL) => Promise<T>) | undefined;
|
|
|
|
export function loadEsmModule<T>(modulePath: string | URL): Promise<T> {
|
|
const modulePathWithCacheBust = `${modulePath}?version=${Date.now()}`;
|
|
dynamicLoad ??= new Function(
|
|
'modulePath',
|
|
`return import(modulePath);`
|
|
) as Exclude<typeof dynamicLoad, undefined>;
|
|
|
|
return dynamicLoad(modulePathWithCacheBust);
|
|
}
|