scripts/check-type.js (222 lines of code) (raw):
#!/usr/bin/env node
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// @ts-check
const { exit } = require("node:process");
const { join, dirname, normalize, sep } = require("node:path");
const { readdir, stat } = require("node:fs/promises");
const { existsSync } = require("node:fs");
const { chdir, cwd } = require("node:process");
const { createRequire } = require("node:module");
const SUPERSET_ROOT = dirname(__dirname);
const PACKAGE_ARG_REGEX = /^package=/;
const EXCLUDE_DECLARATION_DIR_REGEX = /^excludeDeclarationDir=/;
const DECLARATION_FILE_REGEX = /\.d\.ts$/;
void (async () => {
const args = process.argv.slice(2);
const {
matchedArgs: [packageArg, excludeDeclarationDirArg],
remainingArgs,
} = extractArgs(args, [PACKAGE_ARG_REGEX, EXCLUDE_DECLARATION_DIR_REGEX]);
if (!packageArg) {
console.error("package is not specified");
exit(1);
}
const packageRootDir = await getPackage(packageArg);
const updatedArgs = removePackageSegment(remainingArgs, packageRootDir);
const argsStr = updatedArgs.join(" ");
const excludedDeclarationDirs = getExcludedDeclarationDirs(
excludeDeclarationDirArg
);
let declarationFiles = await getFilesRecursively(
packageRootDir,
DECLARATION_FILE_REGEX,
excludedDeclarationDirs
);
declarationFiles = removePackageSegment(declarationFiles, packageRootDir);
const declarationFilesStr = declarationFiles.join(" ");
const packageRootDirAbsolute = join(SUPERSET_ROOT, packageRootDir);
const tsConfig = getTsConfig(packageRootDirAbsolute);
const command = `--noEmit --allowJs --composite false --project ${tsConfig} ${argsStr} ${declarationFilesStr}`;
try {
chdir(packageRootDirAbsolute);
// Please ensure that tscw-config is installed in the package being type-checked.
const tscw = packageRequire("tscw-config");
const child = await tscw`${command}`;
if (child.stdout) {
console.log(child.stdout);
}
if (child.stderr) {
console.error(child.stderr);
}
exit(child.exitCode);
} catch (e) {
console.error("Failed to execute type checking:", e);
console.error("Package:", packageRootDir);
console.error("Command:", `tscw ${command}`);
exit(1);
}
})();
/**
*
* @param {string} fullPath
* @param {string[]} excludedDirs
*/
function shouldExcludeDir(fullPath, excludedDirs) {
return excludedDirs.some((excludedDir) => {
const normalizedExcludedDir = normalize(excludedDir);
const normalizedPath = normalize(fullPath);
return (
normalizedExcludedDir === normalizedPath ||
normalizedPath
.split(sep)
.filter((segment) => segment)
.includes(normalizedExcludedDir)
);
});
}
/**
* @param {string} dir
* @param {RegExp} regex
* @param {string[]} excludedDirs
*
* @returns {Promise<string[]>}
*/
async function getFilesRecursively(dir, regex, excludedDirs) {
try {
const files = await readdir(dir, { withFileTypes: true });
const recursivePromises = [];
const result = [];
for (const file of files) {
const fullPath = join(dir, file.name);
if (file.isDirectory() && !shouldExcludeDir(fullPath, excludedDirs)) {
recursivePromises.push(
getFilesRecursively(fullPath, regex, excludedDirs)
);
} else if (regex.test(file.name)) {
result.push(fullPath);
}
}
const recursiveResults = await Promise.all(recursivePromises);
return result.concat(...recursiveResults);
} catch (e) {
console.error(`Error reading directory: ${dir}`);
console.error(e);
exit(1);
}
}
/**
*
* @param {string} packageArg
* @returns {Promise<string>}
*/
async function getPackage(packageArg) {
const packageDir = packageArg.split("=")[1].replace(/\/$/, "");
try {
const stats = await stat(packageDir);
if (!stats.isDirectory()) {
console.error(
`Please specify a valid package, ${packageDir} is not a directory.`
);
exit(1);
}
} catch (e) {
console.error(`Error reading package: ${packageDir}`);
console.error(e);
exit(1);
}
return packageDir;
}
/**
*
* @param {string | undefined} excludeDeclarationDirArg
* @returns {string[]}
*/
function getExcludedDeclarationDirs(excludeDeclarationDirArg) {
const excludedDirs = ["node_modules"];
return !excludeDeclarationDirArg
? excludedDirs
: excludeDeclarationDirArg
.split("=")[1]
.split(",")
.map((dir) => dir.replace(/\/$/, "").trim())
.concat(excludedDirs);
}
/**
*
* @param {string[]} args
* @param {RegExp[]} regexes
* @returns {{ matchedArgs: (string | undefined)[], remainingArgs: string[] }}
*/
function extractArgs(args, regexes) {
/**
* @type {(string | undefined)[]}
*/
const matchedArgs = [];
const remainingArgs = [...args];
regexes.forEach((regex) => {
const index = remainingArgs.findIndex((arg) => regex.test(arg));
if (index !== -1) {
const [arg] = remainingArgs.splice(index, 1);
matchedArgs.push(arg);
} else {
matchedArgs.push(undefined);
}
});
return { matchedArgs, remainingArgs };
}
/**
* Remove the package segment from path.
*
* For example: `superset-frontend/foo/bar.ts` -> `foo/bar.ts`
*
* @param {string[]} args
* @param {string} package
* @returns {string[]}
*/
function removePackageSegment(args, package) {
const packageSegment = package.concat(sep);
return args.map((arg) => {
const normalizedPath = normalize(arg);
if (normalizedPath.startsWith(packageSegment)) {
return normalizedPath.slice(packageSegment.length);
}
return arg;
});
}
/**
*
* @param {string} dir
*/
function getTsConfig(dir) {
const defaultTsConfig = "tsconfig.json";
const tsConfig = join(dir, defaultTsConfig);
if (!existsSync(tsConfig)) {
console.error(`Error: ${defaultTsConfig} not found in ${dir}`);
exit(1);
}
return tsConfig;
}
/**
*
* @param {string} module
*/
function packageRequire(module) {
try {
const localRequire = createRequire(join(cwd(), "node_modules"));
return localRequire(module);
} catch (e) {
console.error(
`Error: ${module} is not installed in ${cwd()}. Please install it first.`
);
exit(1);
}
}