-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
107677: ui,build: write typescript declarations to multiple directories in watch mode r=sjbarag a=sjbarag Previously, running `pnpm tsc:watch` for incremental type-checking of `cluster-ui` would only write files to `./dist/types/*`. This worked fine, but the introduction of a push model for webpack output[^1] meant it was possible to have compiled JS without declarations in some other directory. Push typescript declarations into multiple directories as well, using a small integration with the TypeScript compiler API[^2]. [^1]: 7a6ec95 (ui,build: push cluster-ui assets into external folder during watch mode, 2023-07-24) [^2]: https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API/167d197d290bec04b626b91b6f453123ef309e58#writing-an-incremental-program-watcher Release note: None Epic: none Co-authored-by: Sean Barag <[email protected]>
- Loading branch information
Showing
6 changed files
with
261 additions
and
86 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
132 changes: 132 additions & 0 deletions
132
pkg/ui/workspaces/cluster-ui/build/typescript/watchWithMultipleDests.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
// Copyright 2023 The Cockroach Authors. | ||
// | ||
// Use of this software is governed by the Business Source License | ||
// included in the file licenses/BSL.txt. | ||
// | ||
// As of the Change Date specified in that file, in accordance with | ||
// the Business Source License, use of this software will be governed | ||
// by the Apache License, Version 2.0, included in the file | ||
// licenses/APL.txt. | ||
|
||
const fs = require("fs"); | ||
const path = require("path"); | ||
|
||
const argv = require("minimist")(process.argv.slice(2)); | ||
const ts = require("typescript"); | ||
|
||
const { cleanDestinationPaths, tildeify } = require("../util"); | ||
|
||
/** | ||
* A minimal wrapper around tsc --watch, implemented using the typescript JS API | ||
* to support writing emitted files to multiple directories. | ||
* This function never returns, as it hosts a filesystem 'watcher'. | ||
* @params {Object} options - a bag of options | ||
* @params {string[]} options.destinations - an array of directories to emit declarations to. | ||
* The default from tsconfig.json will be automatically | ||
* prepended to this list. | ||
* @returns never | ||
* @see https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API/167d197d290bec04b626b91b6f453123ef309e58#writing-an-incremental-program-watcher | ||
*/ | ||
function watch(options) { | ||
const destinations = options.destinations; | ||
|
||
// Find a tsconfig.json. | ||
const configPath = ts.findConfigFile( | ||
/* searchPath */ "./", | ||
ts.sys.fileExists, | ||
"tsconfig.json", | ||
); | ||
if (!configPath) { | ||
throw new Error("Could not find a valid 'tsconfig.json'"); | ||
} | ||
|
||
// Create a wrapper around the default ts.sys.writeFile that also writes files | ||
// to each destination. | ||
const tsSysWriteFile = ts.sys.writeFile; | ||
/** Wraps ts.sys.writeFile to write files to multiple destinations. */ | ||
function writeFile(fileName, data, writeByteOrderMark) { | ||
// First, write to the intended path. Providing a writeFile function | ||
// means TypeScript won't do this on its own. | ||
tsSysWriteFile(fileName, data, writeByteOrderMark); | ||
|
||
// Get a path to fileName relative to the root of this package. | ||
// Luckily, configPath is always the absolute path to a file at the root. | ||
const relPath = path.relative(ts.sys.getCurrentDirectory(), fileName); | ||
for (const dst of destinations) { | ||
const absDstPath = path.join(dst, relPath); | ||
tsSysWriteFile(absDstPath, data, writeByteOrderMark); | ||
} | ||
} | ||
|
||
// Create a watching compiler host that we'll pass to ts.createWatchProgram | ||
// later. | ||
const host = ts.createWatchCompilerHost( | ||
configPath, | ||
/* optionsToExtend */ {}, | ||
{ | ||
...ts.sys, | ||
writeFile, | ||
}, | ||
ts.createEmitAndSemanticDiagnosticsBuilderProgram, | ||
); | ||
|
||
// Create a wrapper around the default createProgram hook (called whenever a | ||
// compilation pass starts) to log a helpful message. Note that in TS 4.2, | ||
// there's no way to suppress typescript's default terminal-clearing behavior | ||
// when a program is created. To keep anyone from forgetting, print this | ||
// message every time. | ||
const origCreateProgram = host.createProgram; | ||
host.createProgram = (rootNames, options, host, oldProgram, configFileParsingDiagnostics, projectReferences) => { | ||
const compilerOptions = options || {}; | ||
const currentDir = host.getCurrentDirectory(); | ||
// Compute the declaration directory relative to the project root. | ||
const relDeclarationDir = path.relative( | ||
currentDir, | ||
compilerOptions.declarationDir || compilerOptions.outDir || "" | ||
); | ||
|
||
console.log("Declarations will be written to:") | ||
for (const dst of ["./"].concat(destinations)) { | ||
console.log(` ${tildeify(path.join(dst, relDeclarationDir))}`); | ||
} | ||
|
||
return origCreateProgram(rootNames, options, host, oldProgram, configFileParsingDiagnostics, projectReferences); | ||
} | ||
|
||
// Create an initial program, watch files, and incrementally update that | ||
// program object. | ||
ts.createWatchProgram(host); | ||
} | ||
|
||
const isHelp = argv.h || argv.help; | ||
const hasPositionalArgs = argv._.length !== 0; | ||
if (isHelp || hasPositionalArgs) { | ||
const argv1 = path.relative(path.join(__dirname, "../../"), argv[1]); | ||
const help = ` | ||
${argv1} - a minimal replacement for 'tsc --watch' that copies generated files to extra directories. | ||
Usage: | ||
${argv1} [--copy-to DIR]... | ||
Flags: | ||
--copy-to DIR path to copy emitted files to, in addition to the default in | ||
tsconfig.json. Can be specified multiple times. | ||
-h, --help prints this message | ||
`; | ||
|
||
if (hasPositionalArgs) { | ||
console.error("Unexpected positional arguments:", argv._); | ||
console.error(); | ||
} | ||
console.error(help.trim()); | ||
process.exit(hasPositionalArgs ? 1 : 0); | ||
} | ||
|
||
const copyToArgs = argv["copy-to"]; | ||
const destinations = typeof copyToArgs === "string" | ||
? [ copyToArgs ] | ||
: (copyToArgs || []); | ||
|
||
watch({ | ||
destinations: cleanDestinationPaths(destinations), | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
// Copyright 2023 The Cockroach Authors. | ||
// | ||
// Use of this software is governed by the Business Source License | ||
// included in the file licenses/BSL.txt. | ||
// | ||
// As of the Change Date specified in that file, in accordance with | ||
// the Business Source License, use of this software will be governed | ||
// by the Apache License, Version 2.0, included in the file | ||
// licenses/APL.txt. | ||
|
||
const fs = require("fs"); | ||
const os = require("os"); | ||
const path = require("path"); | ||
const semver = require("semver"); | ||
|
||
/** | ||
* Cleans up destination paths to ensure they point to valid directories, | ||
* automatically adding node_modules/@cockroachlabs/cluster-ui-XX-Y (for the | ||
* current XX-Y in this package's package.json) if a destination isn't specific | ||
* to a single CRDB version. | ||
* @param {string[]} destinations - an array of paths | ||
* @param {Object} [logger=console] - the logger to use when reporting messages (defaults to `console`) | ||
* @returns {string[]} `destinations`, but cleaned up | ||
*/ | ||
function cleanDestinationPaths(destinations, logger=console) { | ||
// Extract the major and minor versions of this cluster-ui build. | ||
const pkgVersion = getPkgVersion(); | ||
|
||
return destinations.map(dstOpt => { | ||
const dst = detildeify(dstOpt); | ||
|
||
// The user provided paths to a specific cluster-ui version. | ||
if (dst.includes("@cockroachlabs/cluster-ui-")) { | ||
// Remove a possibly-trailing '/' literal. | ||
const dstClean = dst[dst.length - 1] === "/" | ||
? dst.slice(0, dst.length - 1) | ||
: dst; | ||
|
||
return dstClean; | ||
} | ||
|
||
// If the user provided a path to a project, look for a top-level | ||
// node_modules/ within that directory | ||
const dirents = fs.readdirSync(dst, { encoding: "utf-8", withFileTypes: true }); | ||
for (const dirent of dirents) { | ||
if (dirent.name === "node_modules" && dirent.isDirectory()) { | ||
return path.join( | ||
dst, | ||
`./node_modules/@cockroachlabs/cluster-ui-${pkgVersion.major}-${pkgVersion.minor}`, | ||
); | ||
} | ||
} | ||
|
||
const hasPnpmLock = dirents.some((dirent) => dirent.name === "pnpm-lock.yaml"); | ||
if (hasPnpmLock) { | ||
logger.error(`Directory ${dst} doesn't have a node_modules directory, but does have a pnpm-lock.yaml.`); | ||
logger.error(`Do you need to run 'pnpm install' there?`); | ||
throw "missing node_modules"; | ||
} | ||
|
||
logger.error(`Directory ${dst} doesn't have a node_modules directory, and does not appear to be`); | ||
logger.error(`a JS package.`); | ||
throw "unknown destination"; | ||
}); | ||
} | ||
|
||
/** | ||
* Extracts the major and minor version number from the cluster-ui package. | ||
* @returns {object} - an object containing the major (`.major`) and minor | ||
* (`.minor`) versions of the package | ||
*/ | ||
function getPkgVersion() { | ||
const pkgJsonStr = fs.readFileSync( | ||
path.join(__dirname, "../../package.json"), | ||
"utf-8", | ||
); | ||
const pkgJson = JSON.parse(pkgJsonStr); | ||
const version = semver.parse(pkgJson.version); | ||
return { | ||
major: version.major, | ||
minor: version.minor, | ||
}; | ||
} | ||
|
||
/** | ||
* Replaces the user's home directory with '~' in the provided path. The | ||
* opposite of `detildeify`. | ||
* @param {string} path - the path to replace a home directory in | ||
* @returns {string} `path` but with the user's home directory swapped for '~' | ||
*/ | ||
function tildeify(path) { | ||
return path.replace(os.homedir(), "~"); | ||
} | ||
|
||
/** | ||
* Replaces '~' with the user's home directory in the provided path. The | ||
* opposite of `tildeify`. | ||
* @param {string} path - the path to replace a '~' in | ||
* @returns {string} `path` but with '~' swapped for the user's home directory. | ||
*/ | ||
function detildeify(path) { | ||
return path.replace("~", os.homedir()); | ||
} | ||
|
||
|
||
module.exports = { | ||
tildeify, | ||
detildeify, | ||
cleanDestinationPaths, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.