turbo-gulp
Version:
Gulp tasks to boost high-quality projects.
373 lines (317 loc) • 12 kB
text/typescript
import { existsSync, FSWatcher } from "fs";
import { Gulp, TaskFunction } from "gulp";
import { Minimatch } from "minimatch";
import { posix as posixPath } from "path";
import { Readable as ReadableStream } from "stream";
import * as typescript from "typescript";
import Vinyl from "vinyl";
import { CleanOptions } from "../options/clean";
import { CopyOptions } from "../options/copy";
import { CompilerOptionsJson, DEV_TSC_OPTIONS, mergeTscOptionsJson } from "../options/tsc";
import { OutModules } from "../options/typescript";
import { Project, ResolvedProject, resolveProject } from "../project";
import { TypescriptConfig } from "../target-tasks/_typescript";
import { getBuildTypescriptTask, getBuildTypescriptWatchTask } from "../target-tasks/build-typescript";
import { getTsconfigJsonTask } from "../target-tasks/tsconfig-json";
import { CleanOptions as _CleanOptions, generateTask as generateCleanTask } from "../task-generators/clean";
import * as copy from "../task-generators/copy";
import { AbsPosixPath, RelPosixPath } from "../types";
import * as matcher from "../utils/matcher";
export type WatchTaskFunction = (TaskFunction & (() => FSWatcher));
/**
* Generate a copy task (and the corresponding watch task) for the copy operations described by `copyOptions`
*
* @param gulp Gulp instance to use for utility methods.
* @param srcDir Base directory for source resolution.
* @param targetDir Base directory for target (build) resolution.
* @param copyOptions Simple copy operations to apply for this copy task.
* @return A tuple with the task function and corresponding watch task function.
*/
export function getCopy(
gulp: Gulp,
srcDir: string,
targetDir: string,
copyOptions: Iterable<CopyOptions>,
): [TaskFunction, TaskFunction] {
const tasks: TaskFunction[] = [];
const watchTasks: WatchTaskFunction[] = [];
for (const options of copyOptions) {
const from: string = options.src === undefined ? srcDir : posixPath.join(srcDir, options.src);
const files: string[] = options.files === undefined ? ["**/*"] : options.files;
const to: string = options.dest === undefined ? targetDir : posixPath.join(targetDir, options.dest);
const completeOptions: copy.Options = {from, files, to};
tasks.push(copy.generateTask(gulp, completeOptions));
watchTasks.push(() => copy.watch(gulp, completeOptions));
}
const task: TaskFunction = gulp.parallel(tasks);
const watch: TaskFunction = gulp.parallel(watchTasks);
return [task, watch];
}
export interface TargetBase {
project: Project;
/**
* Name of the target.
* All the tasks related to this target will be prefixed by this name.
* It will also be used to resolve the default values for some paths, so it must avoid any special characters.
*/
name: string;
/**
* Relative path to the base directory for the sources, relative to `project.rootDir`.
* The default value is `project.srcDir`.
*/
srcDir?: RelPosixPath;
/**
* Relative path to the build directory for this target, relative to `project.rootDir`.
* The default value is `join(project.buildDir, target.name)`.
*/
buildDir?: RelPosixPath;
/**
* Glob patterns for the Typescript sources, relative to `target.srcDir`.
*
* It uses the `minimatch` patterns. Glob stars (wild stars, `**`) use `target.srcDir` as their base directory.
*
* Default: `[join(target.srcDir, "**", "*.ts")]`
*/
scripts?: Iterable<string>;
/**
* Directory containing custom typings, relative to `project.rootDir`.
* Custom typings are typings that are not available on `@types`.
* `null` means that you don't use custom typings.
* The default value will be `join(target.srcDir, "custom-typings")` if it exists (sync test), else `null`.
*/
customTypingsDir?: RelPosixPath | null;
/**
* Overrides for the options of the Typescript compiler.
*/
tscOptions?: CompilerOptionsJson;
/**
* Output modules.
*
* - `Js`: Use the compiler options to emit `*.js` files.
* - `Mjs`: Enforce `es2015` modules and emit `*.mjs` files.
* - `Both`: Emit both `*.js` files using the compiler options and `*.mjs` using `es2015`.
*
* Default: `Js`
*/
outModules?: OutModules;
/**
* Path to the `tsconfig.json` file for this target, relative to `project.rootDir`.
* Use `null` to not generate a `tsconfig.json` task.
*
* The default value is `join(target.srcDir, "tsconfig.json")`.
*/
tsconfigJson?: RelPosixPath | null;
/**
* Override default dependencies or provide optional dependencies.
*/
dependencies?: BaseDependencies;
/**
* A list of copy operations to perform during the build process.
*
* Default: `[]`
*/
copy?: CopyOptions[];
/**
* Minimatch patterns to clean the files create during the `build` and `dist` tasks, relative to `project.root`.
*
* Default:
* {
* dirs: [
* path.join(project.buildDir, target.targetDir),
* path.join(project.distDir, target.targetDir)
* ]
* }
*/
clean?: CleanOptions;
}
/**
* Library with fully resolved paths and dependencies.
*/
export interface ResolvedTargetBase extends TargetBase {
readonly project: ResolvedProject;
readonly srcDir: AbsPosixPath;
readonly buildDir: AbsPosixPath;
readonly scripts: Iterable<string>;
readonly customTypingsDir: AbsPosixPath | null;
readonly tscOptions: CompilerOptionsJson;
readonly outModules: OutModules;
readonly tsconfigJson: AbsPosixPath | null;
readonly dependencies: ResolvedBaseDependencies;
readonly copy?: CopyOptions[];
readonly clean?: CleanOptions;
}
export interface BaseDependencies {
readonly typescript?: typeof typescript;
}
/**
* Fully resolved dependencies, either using defaults or the library provided by the user.
*/
export interface ResolvedBaseDependencies extends BaseDependencies {
readonly typescript: typeof typescript;
}
/**
* Resolve absolute paths and dependencies for the provided target.
*
* @param target Non-resolved target.
* @return Resolved target.
*/
export function resolveTargetBase(target: TargetBase): ResolvedTargetBase {
const project: ResolvedProject = resolveProject(target.project);
const srcDir: AbsPosixPath = typeof target.srcDir === "string" ?
posixPath.join(project.absRoot, target.srcDir) :
project.srcDir;
const buildDir: AbsPosixPath = typeof target.buildDir === "string" ?
posixPath.join(project.absRoot, target.buildDir) :
posixPath.join(project.absBuildDir, target.name);
const scripts: string[] = [];
if (target.scripts === undefined) {
scripts.push(posixPath.join(srcDir, "**", "*.ts"));
} else {
for (const script of target.scripts) {
scripts.push(matcher.asString(matcher.join(srcDir, new Minimatch(script))));
}
}
const defaultCustomTypingsDir: AbsPosixPath = posixPath.join(srcDir, "custom-typings");
const customTypingsDir: AbsPosixPath | null = target.customTypingsDir !== undefined ?
(target.customTypingsDir !== null ? posixPath.join(project.absRoot, target.customTypingsDir) : null) :
(existsSync(defaultCustomTypingsDir) ? defaultCustomTypingsDir : null);
const tscOptions: CompilerOptionsJson = mergeTscOptionsJson(DEV_TSC_OPTIONS, target.tscOptions);
const outModules: OutModules = target.outModules !== undefined ? target.outModules : OutModules.Js;
const tsconfigJson: AbsPosixPath | null = target.tsconfigJson !== undefined ?
(target.tsconfigJson !== null ? posixPath.join(project.absRoot, target.tsconfigJson) : null) :
posixPath.join(srcDir, "tsconfig.json");
const dependencies: ResolvedBaseDependencies = {typescript};
if (target.dependencies !== undefined) {
Object.assign(dependencies, target.dependencies);
}
return {
project,
name: target.name,
srcDir,
buildDir,
scripts,
customTypingsDir,
tscOptions,
outModules,
tsconfigJson,
dependencies,
copy: target.copy,
clean: target.clean,
};
}
/**
* Adds a display name to the supplied task function and returns the task function.
*
* @param name The display name to set.
* @param task The task function to name.
* @return The input task, with its `displayName` property set to `name`.
*/
export function nameTask<T extends TaskFunction>(name: string, task: T): T & {displayName: string} {
task.displayName = name;
return <T & {displayName: string}> task;
}
/**
* Name a task function and register it to the provided gulp instance.
*/
export function addTask(gulp: Gulp, displayName: string, task: TaskFunction): TaskFunction {
gulp.task(nameTask(displayName, task));
return task;
}
/**
* Creates a Vinyl stream source from a Buffer.
*/
export function gulpBufferSrc(filename: string, data: Buffer): NodeJS.ReadableStream {
const src: ReadableStream = new ReadableStream({objectMode: true});
src._read = function () {
this.push(new Vinyl({
path: filename,
contents: data,
}));
this.push(null);
};
return src;
}
/**
* Base tasks available for every target.
*/
export interface BaseTasks {
buildScripts: TaskFunction;
buildCopy?: TaskFunction;
build: TaskFunction;
watch?: TaskFunction;
clean?: TaskFunction;
tsconfigJson?: TaskFunction;
}
/**
* Generates gulp tasks available for every target (base tasks).
*
* @param gulp Gulp instance used to generate tasks manipulating files.
* @param targetOptions Target configuration.
*/
export function generateBaseTasks(gulp: Gulp, targetOptions: TargetBase): BaseTasks {
const target: ResolvedTargetBase = resolveTargetBase(targetOptions);
const result: BaseTasks = <any> {};
// Typescript options
const tsOptions: TypescriptConfig = {
tscOptions: target.tscOptions,
tsconfigJson: target.tsconfigJson,
customTypingsDir: target.customTypingsDir,
packageJson: target.project.absPackageJson,
buildDir: target.buildDir,
srcDir: target.srcDir,
scripts: target.scripts,
outModules: target.outModules,
};
const watchTasks: TaskFunction[] = [];
// build:scripts
result.buildScripts = nameTask(`${target.name}:build:scripts`, getBuildTypescriptTask(gulp, tsOptions));
watchTasks.push(nameTask(`${target.name}:watch:scripts`, getBuildTypescriptWatchTask(gulp, tsOptions)));
// build:copy
if (target.copy !== undefined) {
const [copyTask, copyWatchTask]: [TaskFunction, TaskFunction] = getCopy(
gulp,
target.srcDir,
target.buildDir,
target.copy,
);
result.buildCopy = nameTask(`${target.name}:build:copy`, copyTask);
watchTasks.push(nameTask(`${target.name}:watch:copy`, copyWatchTask));
}
// build
const buildTasks: TaskFunction[] = [result.buildScripts];
if (result.buildCopy !== undefined) {
buildTasks.push(result.buildCopy);
}
result.build = nameTask(`${target.name}:build`, gulp.parallel(buildTasks));
result.watch = nameTask(`${target.name}:watch`, gulp.series(result.build, gulp.parallel(watchTasks)));
// clean
if (target.clean !== undefined) {
const cleanOptions: _CleanOptions = {
base: target.project.absRoot,
dirs: target.clean.dirs,
files: target.clean.files,
};
result.clean = nameTask(`${target.name}:clean`, generateCleanTask(gulp, cleanOptions));
}
// tsconfig.json
if (target.tsconfigJson !== null) {
result.tsconfigJson = nameTask(`${target.name}:tsconfig.json`, getTsconfigJsonTask(tsOptions));
}
return result;
}
/**
* Generates and registers gulp tasks available for every target (base tasks).
*
* @param gulp Gulp instance where the tasks will be registered.
* @param targetOptions Target configuration.
*/
export function registerBaseTasks(gulp: Gulp, targetOptions: TargetBase): BaseTasks {
const tasks: BaseTasks = generateBaseTasks(gulp, targetOptions);
for (const key in tasks) {
const task: TaskFunction | undefined = (<any> tasks)[key];
if (task !== undefined) {
gulp.task(task);
}
}
return tasks;
}