@bufbuild/protoplugin
Version:
Helps to create your own Protocol Buffers code generators.
142 lines (141 loc) • 5.75 kB
JavaScript
// Copyright 2021-2025 Buf Technologies, Inc.
//
// Licensed 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.
import ts from "typescript";
import { createDefaultMapFromNodeModules, createSystem, createVirtualCompilerHost, } from "@typescript/vfs";
/**
* Create a transpiler using the given compiler options, which will compile the
* content provided in the files array.
*
* Note: this library intentionally transpiles with a pinned older version of
* TypeScript for stability. This version is denoted in this workspace's
* package.json. For the default set of compiler options, we use a lenient
* set of options because the general goal is to emit code as best as we can.
* For a list of the options used, see `const defaultOptions` in function transpile.
*
* If this is not desirable for plugin authors, they are free to provide their
* own transpile function as part of the plugin initialization. If one is
* provided, it will be invoked instead and the framework's auto-transpilation
* will be bypassed.
*
* In addition, note that there is a dependency on @typescript/vfs in the
* top-level package as well as this package. This is to avoid npm hoisting
* @typescript/vfs to the top-level node_modules directory, which then causes
* type mismatches when trying to use it with this package's version of
* TypeScript. Ideally we would use something like Yarn's nohoist here, but
* npm does not support that yet.
*/
function createTranspiler(options, files) {
const fsMap = createDefaultMapFromNodeModules({
target: options.target,
});
for (const file of files) {
fsMap.set(file.name, file.content);
}
const system = createSystem(fsMap);
const host = createVirtualCompilerHost(system, options, ts);
return ts.createProgram({
rootNames: [...fsMap.keys()],
options,
host: host.compilerHost,
});
}
export function transpile(files, transpileJs, transpileDts, jsImportStyle) {
const defaultOptions = {
// Type checking
strict: false,
// modules
module: ts.ModuleKind.ES2020,
moduleResolution: ts.ModuleResolutionKind.Node10,
noResolve: true,
resolveJsonModule: false,
// emit
emitBOM: false,
importsNotUsedAsValues: ts.ImportsNotUsedAsValues.Preserve,
newLine: ts.NewLineKind.LineFeed,
preserveValueImports: false,
// JavaScript Support
allowJs: true,
checkJs: false,
// Language and Environment
lib: [],
moduleDetection: ts.ModuleDetectionKind.Force,
target: ts.ScriptTarget.ES2017,
// Completeness
skipLibCheck: true,
skipDefaultLibCheck: false,
};
const options = Object.assign(Object.assign({}, defaultOptions), { declaration: transpileDts, emitDeclarationOnly: transpileDts && !transpileJs });
if (jsImportStyle == "legacy_commonjs") {
options.module = ts.ModuleKind.CommonJS;
}
// Create the transpiler (a ts.Program object)
const program = createTranspiler(options, files);
const results = [];
let err;
const result = program.emit(undefined, (fileName, data, _writeByteOrderMark, _onError, sourceFiles) => {
// We have to go through some hoops here because the header we add to each
// file is not part of the AST. So we find the TypeScript file we
// generated for each emitted file and add the header to each output ourselves.
if (!sourceFiles) {
err = new Error(`unable to map emitted file "${fileName}" to a source file: missing source files`);
return;
}
if (sourceFiles.length !== 1) {
err = new Error(`unable to map emitted file "${fileName}" to a source file: expected 1 source file, got ${sourceFiles.length}`);
return;
}
const file = files.find((x) => sourceFiles[0].fileName === x.name);
if (!file) {
err = new Error(`unable to map emitted file "${fileName}" to a source file: not found`);
return;
}
results.push({
name: fileName,
preamble: file.preamble,
content: data,
});
});
if (err) {
throw err;
}
if (result.emitSkipped) {
// When compilation fails, this error message is printed to stderr.
const diagnostics = formatDiagnostics(result.diagnostics);
throw Error(`A problem occurred during transpilation and files were not generated. Contact the plugin author for support.\n\n${diagnostics}`);
}
return results;
}
function formatDiagnostics(diagnostics) {
const sorted = ts.sortAndDeduplicateDiagnostics(diagnostics);
if (sorted.length == 0) {
return "";
}
const first = sorted.slice(0, 3);
const formatHost = {
getCanonicalFileName(fileName) {
return fileName;
},
getCurrentDirectory() {
return ".";
},
getNewLine() {
return "\n";
},
};
let out = ts.formatDiagnostics(first, formatHost).trim();
if (first.length < sorted.length) {
out += `\n${sorted.length - first.length} more diagnostics elided`;
}
return out;
}