summaryrefslogtreecommitdiff
path: root/tools/ts_library_builder/build_library.ts
diff options
context:
space:
mode:
Diffstat (limited to 'tools/ts_library_builder/build_library.ts')
-rw-r--r--tools/ts_library_builder/build_library.ts453
1 files changed, 453 insertions, 0 deletions
diff --git a/tools/ts_library_builder/build_library.ts b/tools/ts_library_builder/build_library.ts
new file mode 100644
index 000000000..589e26a82
--- /dev/null
+++ b/tools/ts_library_builder/build_library.ts
@@ -0,0 +1,453 @@
+import { writeFileSync } from "fs";
+import * as prettier from "prettier";
+import {
+ ExpressionStatement,
+ ModuleKind,
+ ModuleResolutionKind,
+ NamespaceDeclarationKind,
+ Project,
+ ScriptTarget,
+ SourceFile,
+ Type,
+ TypeGuards
+} from "ts-simple-ast";
+import {
+ addInterfaceProperty,
+ addSourceComment,
+ addVariableDeclaration,
+ checkDiagnostics,
+ flattenNamespace,
+ getSourceComment,
+ loadDtsFiles,
+ loadFiles,
+ namespaceSourceFile,
+ normalizeSlashes
+} from "./ast_util";
+
+export interface BuildLibraryOptions {
+ /**
+ * The path to the root of the deno repository
+ */
+ basePath: string;
+
+ /**
+ * The path to the current build path
+ */
+ buildPath: string;
+
+ /**
+ * Denotes if the library should be built with debug information (comments
+ * that indicate the source of the types)
+ */
+ debug?: boolean;
+
+ /**
+ * The path to the output library
+ */
+ outFile: string;
+
+ /**
+ * Execute in silent mode or not
+ */
+ silent?: boolean;
+}
+
+/**
+ * A preamble which is appended to the start of the library.
+ */
+// tslint:disable-next-line:max-line-length
+const libPreamble = `// Copyright 2018 the Deno authors. All rights reserved. MIT license.
+
+/// <reference no-default-lib="true" />
+/// <reference lib="esnext" />
+
+`;
+
+// The path to the msg_generated file relative to the build path
+const MSG_GENERATED_PATH = "/gen/msg_generated.ts";
+
+// An array of enums we want to expose pub
+const MSG_GENERATED_ENUMS = ["ErrorKind"];
+
+/** Extracts enums from a source file */
+function extract(sourceFile: SourceFile, enumNames: string[]): string {
+ // Copy specified enums from msg_generated
+ let output = "";
+ for (const enumName of enumNames) {
+ const enumDeclaration = sourceFile.getEnumOrThrow(enumName);
+ enumDeclaration.setHasDeclareKeyword(false);
+ // we are not copying JSDocs or other trivia here because msg_generated only
+ // contains some non-useful JSDocs and comments that are not ideal to copy
+ // over
+ output += enumDeclaration.getText();
+ }
+ return output;
+}
+
+interface FlattenOptions {
+ basePath: string;
+ customSources: { [filePath: string]: string };
+ filePath: string;
+ debug?: boolean;
+ declarationProject: Project;
+ namespaceName: string;
+ targetSourceFile: SourceFile;
+}
+
+/** Flatten a module */
+export function flatten({
+ basePath,
+ customSources,
+ filePath,
+ debug,
+ declarationProject,
+ namespaceName,
+ targetSourceFile
+}: FlattenOptions): void {
+ // Flatten the source file into a single module declaration
+ const statements = flattenNamespace({
+ sourceFile: declarationProject.getSourceFileOrThrow(filePath),
+ rootPath: basePath,
+ customSources,
+ debug
+ });
+
+ // Create the module in the target file
+ const namespace = targetSourceFile.addNamespace({
+ name: namespaceName,
+ hasDeclareKeyword: true,
+ declarationKind: NamespaceDeclarationKind.Module
+ });
+
+ // Add the output of the flattening to the namespace
+ namespace.addStatements(statements);
+}
+
+interface MergeOptions {
+ basePath: string;
+ declarationProject: Project;
+ debug?: boolean;
+ globalVarName: string;
+ filePath: string;
+ inputProject: Project;
+ interfaceName: string;
+ namespaceName: string;
+ targetSourceFile: SourceFile;
+}
+
+/** Take a module and merge into into a single namespace */
+export function merge({
+ basePath,
+ declarationProject,
+ debug,
+ globalVarName,
+ filePath,
+ inputProject,
+ interfaceName,
+ namespaceName,
+ targetSourceFile
+}: MergeOptions) {
+ // We have to build the module/namespace in small pieces which will reflect
+ // how the global runtime environment will be for Deno
+
+ // We need to add a module named `"globals"` which will contain all the global
+ // runtime context
+ const mergedModule = targetSourceFile.addNamespace({
+ name: namespaceName,
+ hasDeclareKeyword: true,
+ declarationKind: NamespaceDeclarationKind.Module
+ });
+
+ // Add the global Window interface
+ const interfaceDeclaration = mergedModule.addInterface({
+ name: interfaceName
+ });
+
+ // Add the global scope augmentation module of the "globals" module
+ const mergedGlobalNamespace = mergedModule.addNamespace({
+ name: "global",
+ declarationKind: NamespaceDeclarationKind.Global
+ });
+
+ // Declare the global variable
+ addVariableDeclaration(mergedGlobalNamespace, globalVarName, interfaceName);
+
+ // Add self reference to the global variable
+ addInterfaceProperty(interfaceDeclaration, globalVarName, interfaceName);
+
+ // Retrieve source file from the input project
+ const sourceFile = inputProject.getSourceFileOrThrow(filePath);
+
+ // we are going to create a map of variables
+ const globalVariables = new Map<
+ string,
+ {
+ type: Type;
+ node: ExpressionStatement;
+ }
+ >();
+
+ // For every augmentation of the global variable in source file, we want
+ // to extract the type and add it to the global variable map
+ sourceFile.forEachChild(node => {
+ if (TypeGuards.isExpressionStatement(node)) {
+ const firstChild = node.getFirstChild();
+ if (!firstChild) {
+ return;
+ }
+ if (TypeGuards.isBinaryExpression(firstChild)) {
+ const leftExpression = firstChild.getLeft();
+ if (
+ TypeGuards.isPropertyAccessExpression(leftExpression) &&
+ leftExpression.getExpression().getText() === globalVarName
+ ) {
+ const windowProperty = leftExpression.getName();
+ if (windowProperty !== globalVarName) {
+ globalVariables.set(windowProperty, {
+ type: firstChild.getType(),
+ node
+ });
+ }
+ }
+ }
+ }
+ });
+
+ // A set of source files that the types we are using are dependent on us
+ // importing
+ const dependentSourceFiles = new Set<SourceFile>();
+
+ // Create a global variable and add the property to the `Window` interface
+ // for each mutation of the `window` variable we observed in `globals.ts`
+ for (const [property, info] of globalVariables) {
+ const type = info.type.getText(info.node);
+ const typeSymbol = info.type.getSymbol();
+ if (typeSymbol) {
+ const valueDeclaration = typeSymbol.getValueDeclaration();
+ if (valueDeclaration) {
+ dependentSourceFiles.add(valueDeclaration.getSourceFile());
+ }
+ }
+ addVariableDeclaration(mergedGlobalNamespace, property, type);
+ addInterfaceProperty(interfaceDeclaration, property, type);
+ }
+
+ // We need to ensure that we only namespace each source file once, so we
+ // will use this map for tracking that.
+ const sourceFileMap = new Map<SourceFile, string>();
+
+ // For each import declaration in source file we will want to convert the
+ // declaration source file into a namespace that exists within the merged
+ // namespace
+ const importDeclarations = sourceFile.getImportDeclarations();
+ for (const declaration of importDeclarations) {
+ const declarationSourceFile = declaration.getModuleSpecifierSourceFile();
+ if (
+ declarationSourceFile &&
+ dependentSourceFiles.has(declarationSourceFile)
+ ) {
+ // the source file will resolve to the original `.ts` file, but the
+ // information we really want is in the emitted `.d.ts` file, so we will
+ // resolve to that file
+ const dtsFilePath = declarationSourceFile
+ .getFilePath()
+ .replace(/\.ts$/, ".d.ts");
+ const dtsSourceFile = declarationProject.getSourceFileOrThrow(
+ dtsFilePath
+ );
+ mergedModule.addStatements(
+ namespaceSourceFile(dtsSourceFile, {
+ debug,
+ namespace: declaration.getNamespaceImportOrThrow().getText(),
+ rootPath: basePath,
+ sourceFileMap
+ })
+ );
+ }
+ }
+
+ if (debug) {
+ addSourceComment(mergedModule, sourceFile, basePath);
+ }
+}
+
+/**
+ * Generate the runtime library for Deno and write it to the supplied out file
+ * name.
+ */
+export function main({
+ basePath,
+ buildPath,
+ debug,
+ outFile,
+ silent
+}: BuildLibraryOptions) {
+ if (!silent) {
+ console.log("-----");
+ console.log("build_lib");
+ console.log();
+ console.log(`basePath: "${basePath}"`);
+ console.log(`buildPath: "${buildPath}"`);
+ console.log(`debug: ${!!debug}`);
+ console.log(`outFile: "${outFile}"`);
+ console.log();
+ }
+
+ // the inputProject will take in the TypeScript files that are internal
+ // to Deno to be used to generate the library
+ const inputProject = new Project({
+ compilerOptions: {
+ baseUrl: basePath,
+ declaration: true,
+ emitDeclarationOnly: true,
+ lib: [],
+ module: ModuleKind.AMD,
+ moduleResolution: ModuleResolutionKind.NodeJs,
+ noLib: true,
+ paths: {
+ "*": ["*", `${buildPath}/*`]
+ },
+ preserveConstEnums: true,
+ strict: true,
+ stripInternal: true,
+ target: ScriptTarget.ESNext
+ }
+ });
+
+ // Add the input files we will need to generate the declarations, `globals`
+ // plus any modules that are importable in the runtime need to be added here
+ // plus the `lib.esnext` which is used as the base library
+ inputProject.addExistingSourceFiles([
+ `${basePath}/node_modules/typescript/lib/lib.esnext.d.ts`,
+ `${basePath}/js/deno.ts`,
+ `${basePath}/js/globals.ts`
+ ]);
+
+ // emit the project, which will be only the declaration files
+ const inputEmitResult = inputProject.emitToMemory();
+
+ // the declaration project will be the target for the emitted files from
+ // the input project, these will be used to transfer information over to
+ // the final library file
+ const declarationProject = new Project({
+ compilerOptions: {
+ baseUrl: basePath,
+ moduleResolution: ModuleResolutionKind.NodeJs,
+ noLib: true,
+ paths: {
+ "*": ["*", `${buildPath}/*`]
+ },
+ strict: true,
+ target: ScriptTarget.ESNext
+ },
+ useVirtualFileSystem: true
+ });
+
+ // we don't want to add to the declaration project any of the original
+ // `.ts` source files, so we need to filter those out
+ const jsPath = normalizeSlashes(`${basePath}/js`);
+ const inputProjectFiles = inputProject
+ .getSourceFiles()
+ .map(sourceFile => sourceFile.getFilePath())
+ .filter(filePath => !filePath.startsWith(jsPath));
+ loadFiles(declarationProject, inputProjectFiles);
+
+ // now we add the emitted declaration files from the input project
+ for (const { filePath, text } of inputEmitResult.getFiles()) {
+ declarationProject.createSourceFile(filePath, text);
+ }
+
+ // the outputProject will contain the final library file we are looking to
+ // build
+ const outputProject = new Project({
+ compilerOptions: {
+ baseUrl: buildPath,
+ moduleResolution: ModuleResolutionKind.NodeJs,
+ noLib: true,
+ strict: true,
+ target: ScriptTarget.ESNext,
+ types: ["text-encoding"]
+ },
+ useVirtualFileSystem: true
+ });
+
+ // There are files we need to load into memory, so that the project "compiles"
+ loadDtsFiles(outputProject);
+ // tslint:disable-next-line:max-line-length
+ const textEncodingFilePath = `${buildPath}/node_modules/@types/text-encoding/index.d.ts`;
+ loadFiles(outputProject, [textEncodingFilePath]);
+ outputProject.addExistingSourceFileIfExists(textEncodingFilePath);
+
+ // libDts is the final output file we are looking to build and we are not
+ // actually creating it, only in memory at this stage.
+ const libDTs = outputProject.createSourceFile(outFile);
+
+ // Deal with `js/deno.ts`
+
+ // `gen/msg_generated.d.ts` contains too much exported information that is not
+ // part of the public API surface of Deno, so we are going to extract just the
+ // information we need.
+ const msgGeneratedDts = inputProject.getSourceFileOrThrow(
+ `${buildPath}${MSG_GENERATED_PATH}`
+ );
+ const msgGeneratedDtsText = extract(msgGeneratedDts, MSG_GENERATED_ENUMS);
+
+ // Generate a object hash of substitutions of modules to use when flattening
+ const customSources = {
+ [msgGeneratedDts.getFilePath()]: `${
+ debug ? getSourceComment(msgGeneratedDts, basePath) : ""
+ }${msgGeneratedDtsText}\n`
+ };
+
+ flatten({
+ basePath,
+ customSources,
+ debug,
+ declarationProject,
+ filePath: `${basePath}/js/deno.d.ts`,
+ namespaceName: `"deno"`,
+ targetSourceFile: libDTs
+ });
+
+ if (!silent) {
+ console.log(`Created module "deno".`);
+ }
+
+ merge({
+ basePath,
+ declarationProject,
+ debug,
+ globalVarName: "window",
+ filePath: `${basePath}/js/globals.ts`,
+ inputProject,
+ interfaceName: "Window",
+ namespaceName: `"globals"`,
+ targetSourceFile: libDTs
+ });
+
+ if (!silent) {
+ console.log(`Created module "globals".`);
+ }
+
+ // Add the preamble
+ libDTs.insertStatements(0, libPreamble);
+
+ // Check diagnostics
+ checkDiagnostics(outputProject);
+
+ // Output the final library file
+ libDTs.saveSync();
+ const libDTsText = prettier.format(
+ outputProject.getFileSystem().readFileSync(outFile, "utf8"),
+ { parser: "typescript" }
+ );
+ if (!silent) {
+ console.log(`Outputting library to: "${outFile}"`);
+ console.log(` Length: ${libDTsText.length}`);
+ }
+ writeFileSync(outFile, libDTsText, { encoding: "utf8" });
+ if (!silent) {
+ console.log("-----");
+ console.log();
+ }
+}