242 lines
6.8 KiB
TypeScript
242 lines
6.8 KiB
TypeScript
import { cacheDir, ExecutorContext, logger } from '@nx/devkit';
|
|
import { exec, execSync } from 'node:child_process';
|
|
import { join } from 'node:path';
|
|
import { existsSync, removeSync } from 'fs-extra';
|
|
import { createAsyncIterable } from '@nx/devkit/src/utils/async-iterable';
|
|
import { NormalizedSwcExecutorOptions, SwcCliOptions } from '../schema';
|
|
import { printDiagnostics } from '../typescript/print-diagnostics';
|
|
import { runTypeCheck, TypeCheckOptions } from '../typescript/run-type-check';
|
|
import { relative } from 'path';
|
|
|
|
function getSwcCmd(
|
|
{
|
|
swcCliOptions: { swcrcPath, destPath, stripLeadingPaths },
|
|
root,
|
|
projectRoot,
|
|
originalProjectRoot,
|
|
sourceRoot,
|
|
inline,
|
|
}: NormalizedSwcExecutorOptions,
|
|
watch = false
|
|
) {
|
|
const swcCLI = require.resolve('@swc/cli/bin/swc.js');
|
|
let inputDir: string;
|
|
// TODO(v20): remove inline feature
|
|
if (inline) {
|
|
inputDir = originalProjectRoot.split('/')[0];
|
|
} else {
|
|
if (sourceRoot) {
|
|
inputDir = relative(projectRoot, sourceRoot);
|
|
} else {
|
|
// If sourceRoot is not provided, check if `src` exists and use that instead.
|
|
// This is important for root projects to avoid compiling too many directories.
|
|
inputDir = existsSync(join(root, projectRoot, 'src')) ? 'src' : '.';
|
|
}
|
|
}
|
|
|
|
let swcCmd = `node ${swcCLI} ${
|
|
inputDir || '.'
|
|
} -d ${destPath} --config-file=${swcrcPath} ${
|
|
stripLeadingPaths ? '--strip-leading-paths' : ''
|
|
}`;
|
|
return watch ? swcCmd.concat(' --watch') : swcCmd;
|
|
}
|
|
|
|
function getTypeCheckOptions(normalizedOptions: NormalizedSwcExecutorOptions) {
|
|
const { sourceRoot, projectRoot, watch, tsConfig, root, outputPath } =
|
|
normalizedOptions;
|
|
const inputDir =
|
|
// If `--strip-leading-paths` SWC option is used, we need to transpile from `src` directory.
|
|
!normalizedOptions.swcCliOptions.stripLeadingPaths
|
|
? projectRoot
|
|
: sourceRoot
|
|
? sourceRoot
|
|
: existsSync(join(root, projectRoot, 'src'))
|
|
? join(projectRoot, 'src')
|
|
: projectRoot;
|
|
|
|
const typeCheckOptions: TypeCheckOptions = {
|
|
mode: 'emitDeclarationOnly',
|
|
tsConfigPath: tsConfig,
|
|
outDir: outputPath,
|
|
workspaceRoot: root,
|
|
rootDir: inputDir,
|
|
};
|
|
|
|
if (watch) {
|
|
typeCheckOptions.incremental = true;
|
|
typeCheckOptions.cacheDir = cacheDir;
|
|
}
|
|
|
|
return typeCheckOptions;
|
|
}
|
|
|
|
export async function compileSwc(
|
|
context: ExecutorContext,
|
|
normalizedOptions: NormalizedSwcExecutorOptions,
|
|
postCompilationCallback: () => Promise<void>
|
|
) {
|
|
logger.log(`Compiling with SWC for ${context.projectName}...`);
|
|
|
|
if (normalizedOptions.clean) {
|
|
removeSync(normalizedOptions.outputPath);
|
|
}
|
|
|
|
const swcCmdLog = execSync(getSwcCmd(normalizedOptions), {
|
|
encoding: 'utf8',
|
|
cwd: normalizedOptions.swcCliOptions.swcCwd,
|
|
});
|
|
logger.log(swcCmdLog.replace(/\n/, ''));
|
|
const isCompileSuccess = swcCmdLog.includes('Successfully compiled');
|
|
|
|
if (normalizedOptions.skipTypeCheck) {
|
|
await postCompilationCallback();
|
|
return { success: isCompileSuccess };
|
|
}
|
|
|
|
const { errors, warnings } = await runTypeCheck(
|
|
getTypeCheckOptions(normalizedOptions)
|
|
);
|
|
const hasErrors = errors.length > 0;
|
|
const hasWarnings = warnings.length > 0;
|
|
|
|
if (hasErrors || hasWarnings) {
|
|
await printDiagnostics(errors, warnings);
|
|
}
|
|
|
|
await postCompilationCallback();
|
|
return {
|
|
success: !hasErrors && isCompileSuccess,
|
|
outfile: normalizedOptions.mainOutputPath,
|
|
};
|
|
}
|
|
|
|
export async function* compileSwcWatch(
|
|
context: ExecutorContext,
|
|
normalizedOptions: NormalizedSwcExecutorOptions,
|
|
postCompilationCallback: () => Promise<void>
|
|
) {
|
|
const getResult = (success: boolean) => ({
|
|
success,
|
|
outfile: normalizedOptions.mainOutputPath,
|
|
});
|
|
|
|
let typeCheckOptions: TypeCheckOptions;
|
|
let initialPostCompile = true;
|
|
|
|
if (normalizedOptions.clean) {
|
|
removeSync(normalizedOptions.outputPath);
|
|
}
|
|
|
|
return yield* createAsyncIterable<{ success: boolean; outfile: string }>(
|
|
async ({ next, done }) => {
|
|
let processOnExit: () => void;
|
|
let stdoutOnData: () => void;
|
|
let stderrOnData: () => void;
|
|
let watcherOnExit: () => void;
|
|
|
|
const swcWatcher = exec(getSwcCmd(normalizedOptions, true), {
|
|
cwd: normalizedOptions.swcCliOptions.swcCwd,
|
|
});
|
|
|
|
processOnExit = () => {
|
|
swcWatcher.kill();
|
|
done();
|
|
process.off('SIGINT', processOnExit);
|
|
process.off('SIGTERM', processOnExit);
|
|
process.off('exit', processOnExit);
|
|
};
|
|
|
|
stdoutOnData = async (data?: string) => {
|
|
process.stdout.write(data);
|
|
if (!data.startsWith('Watching')) {
|
|
const swcStatus = data.includes('Successfully');
|
|
|
|
if (initialPostCompile) {
|
|
await postCompilationCallback();
|
|
initialPostCompile = false;
|
|
}
|
|
|
|
if (normalizedOptions.skipTypeCheck) {
|
|
next(getResult(swcStatus));
|
|
return;
|
|
}
|
|
|
|
if (!typeCheckOptions) {
|
|
typeCheckOptions = getTypeCheckOptions(normalizedOptions);
|
|
}
|
|
|
|
const delayed = delay(5000);
|
|
next(
|
|
getResult(
|
|
await Promise.race([
|
|
delayed
|
|
.start()
|
|
.then(() => ({ tscStatus: false, type: 'timeout' })),
|
|
runTypeCheck(typeCheckOptions).then(({ errors, warnings }) => {
|
|
const hasErrors = errors.length > 0;
|
|
if (hasErrors) {
|
|
printDiagnostics(errors, warnings);
|
|
}
|
|
return {
|
|
tscStatus: !hasErrors,
|
|
type: 'tsc',
|
|
};
|
|
}),
|
|
]).then(({ type, tscStatus }) => {
|
|
if (type === 'tsc') {
|
|
delayed.cancel();
|
|
return tscStatus && swcStatus;
|
|
}
|
|
|
|
return swcStatus;
|
|
})
|
|
)
|
|
);
|
|
}
|
|
};
|
|
|
|
stderrOnData = (err?: any) => {
|
|
process.stderr.write(err);
|
|
if (err.includes('Debugger attached.')) {
|
|
return;
|
|
}
|
|
next(getResult(false));
|
|
};
|
|
|
|
watcherOnExit = () => {
|
|
done();
|
|
swcWatcher.off('exit', watcherOnExit);
|
|
};
|
|
|
|
swcWatcher.stdout.on('data', stdoutOnData);
|
|
swcWatcher.stderr.on('data', stderrOnData);
|
|
|
|
process.on('SIGINT', processOnExit);
|
|
process.on('SIGTERM', processOnExit);
|
|
process.on('exit', processOnExit);
|
|
|
|
swcWatcher.on('exit', watcherOnExit);
|
|
}
|
|
);
|
|
}
|
|
|
|
function delay(ms: number): { start: () => Promise<void>; cancel: () => void } {
|
|
let timerId: ReturnType<typeof setTimeout> = undefined;
|
|
return {
|
|
start() {
|
|
return new Promise<void>((resolve) => {
|
|
timerId = setTimeout(() => {
|
|
resolve();
|
|
}, ms);
|
|
});
|
|
},
|
|
cancel() {
|
|
if (timerId) {
|
|
clearTimeout(timerId);
|
|
timerId = undefined;
|
|
}
|
|
},
|
|
};
|
|
}
|