Skip to content

[WIP] Add custom transformer plugins #54278

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 13 commits into from
2 changes: 2 additions & 0 deletions .github/workflows/new-release-branch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ jobs:
sed -i -e 's/const versionMajorMinor = ".*"/const versionMajorMinor = "${{ inputs.core_major_minor }}"/g' src/compiler/corePublic.ts
sed -i -e 's/const versionMajorMinor = ".*"/const versionMajorMinor = "${{ inputs.core_major_minor }}"/g' tests/baselines/reference/api/typescript.d.ts
sed -i -e 's/const versionMajorMinor = ".*"/const versionMajorMinor = "${{ inputs.core_major_minor }}"/g' tests/baselines/reference/api/tsserverlibrary.d.ts
sed -i -e 's/const versionMajorMinor = ".*"/const versionMajorMinor = "${{ inputs.core_major_minor }}"/g' tests/baselines/reference/api/tsclibrary.d.ts
sed -i -e 's/const version\(: string\)\{0,1\} = .*;/const version = "${{ inputs.package_version }}" as string;/g' src/compiler/corePublic.ts
npm ci
npm install # update package-lock.json to ensure the version bump is included
Expand All @@ -79,6 +80,7 @@ jobs:
git add src/compiler/corePublic.ts
git add tests/baselines/reference/api/typescript.d.ts
git add tests/baselines/reference/api/tsserverlibrary.d.ts
git add tests/baselines/reference/api/tsclibrary.d.ts
git add --force ./lib
git config user.email "[email protected]"
git config user.name "TypeScript Bot"
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/set-version.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ jobs:
sed -i -e 's/const versionMajorMinor = ".*"/const versionMajorMinor = "${{ inputs.core_major_minor }}"/g' src/compiler/corePublic.ts
sed -i -e 's/const versionMajorMinor = ".*"/const versionMajorMinor = "${{ inputs.core_major_minor }}"/g' tests/baselines/reference/api/typescript.d.ts
sed -i -e 's/const versionMajorMinor = ".*"/const versionMajorMinor = "${{ inputs.core_major_minor }}"/g' tests/baselines/reference/api/tsserverlibrary.d.ts
sed -i -e 's/const versionMajorMinor = ".*"/const versionMajorMinor = "${{ inputs.core_major_minor }}"/g' tests/baselines/reference/api/tsclibrary.d.ts
sed -i -e 's/const version\(: string\)\{0,1\} = .*;/const version = "${{ inputs.package_version }}" as string;/g' src/compiler/corePublic.ts
npm ci
npm install # update package-lock.json to ensure the version bump is included
Expand All @@ -83,6 +84,7 @@ jobs:
git add src/compiler/corePublic.ts
git add tests/baselines/reference/api/typescript.d.ts
git add tests/baselines/reference/api/tsserverlibrary.d.ts
git add tests/baselines/reference/api/tsclibrary.d.ts
git add --force ./lib
git config user.email "[email protected]"
git config user.name "TypeScript Bot"
Expand Down
16 changes: 14 additions & 2 deletions Herebyfile.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ function entrypointBuildTask(options) {
return { build, bundle, shim, main, watch };
}

const { main: tsc, watch: watchTsc } = entrypointBuildTask({
const { main: tsc, build: buildTsc, watch: watchTsc } = entrypointBuildTask({
name: "tsc",
description: "Builds the command-line compiler",
buildDeps: [generateDiagnostics],
Expand Down Expand Up @@ -492,9 +492,20 @@ export const dtsLssl = task({
},
});

export const dtsTsc = task({
name: "dts-tsc",
description: "Bundles tsclibrary.d.ts",
dependencies: [buildTsc],
run: async () => {
if (needsUpdate("./built/local/tsc/tsconfig.tsbuildinfo", ["./built/local/tsclibrary.d.ts", "./built/local/tsclibrary.internal.d.ts"])) {
await runDtsBundler("./built/local/tsc/_namespaces/ts.d.ts", "./built/local/tsclibrary.d.ts");
}
},
});

export const dts = task({
name: "dts",
dependencies: [dtsServices, dtsLssl],
dependencies: [dtsServices, dtsLssl, dtsTsc],
});

const testRunner = "./built/local/run.js";
Expand Down Expand Up @@ -888,6 +899,7 @@ export const produceLKG = task({
"built/local/tsserver.js",
"built/local/tsserverlibrary.js",
"built/local/tsserverlibrary.d.ts",
"built/local/tsclibrary.d.ts",
"built/local/typescript.js",
"built/local/typescript.d.ts",
"built/local/typingsInstaller.js",
Expand Down
1 change: 1 addition & 0 deletions scripts/produceLKG.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ async function copyScriptOutputs() {
async function copyDeclarationOutputs() {
await copyFromBuiltLocal("tsserverlibrary.d.ts");
await copyFromBuiltLocal("typescript.d.ts");
await copyFromBuiltLocal("tsclibrary.d.ts");
}

async function writeGitAttributes() {
Expand Down
1 change: 1 addition & 0 deletions src/compiler/_namespaces/ts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* Generated file to emulate the ts namespace. */

export * from "../corePublic.js";
export * from "../pluginUtilities.js";
export * from "../core.js";
export * from "../debug.js";
export * from "../semver.js";
Expand Down
9 changes: 9 additions & 0 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,15 @@ export const commonOptionsWithBuild: CommandLineOption[] = [
description: Diagnostics.Set_the_language_of_the_messaging_from_TypeScript_This_does_not_affect_emit,
defaultValueDescription: Diagnostics.Platform_specific,
},
{
name: "allowPlugins",
type: "boolean",
affectsSemanticDiagnostics: true,
affectsEmit: true,
affectsBuildInfo: true,
defaultValueDescription: undefined,
isCommandLineOnly: true,
},
];

/** @internal */
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -6513,6 +6513,10 @@
"category": "Error",
"code": 6931
},
"Option '--allowPlugins' must be specified when compiler plugins are present.": {
"category": "Error",
"code": 6932
},

"Variable '{0}' implicitly has an '{1}' type.": {
"category": "Error",
Expand Down
30 changes: 30 additions & 0 deletions src/compiler/pluginUtilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { CompilerOptions } from "./types.js";

/** @internal */
export type Entrypoint = "tsc" | "typescript" | "tsserver" | "testRunner";

let currentEntrypoint: Entrypoint | undefined;
let currentTsNamespace: any;

/** @internal */
export function setTypeScriptNamespace(entrypoint: Entrypoint, ts: any) {
if (currentEntrypoint !== undefined) throw new Error("ts namespace already set");
currentEntrypoint = entrypoint;
currentTsNamespace = ts;
}

/** @internal */
export function getTypeScriptNamespace(): any {
if (currentTsNamespace === undefined) throw new Error("ts namespace unset");
return currentTsNamespace;
}

/** @internal */
export function shouldAllowPlugins(options: CompilerOptions): boolean {
switch (currentEntrypoint) {
case "tsserver":
case "typescript":
return true;
}
return options.allowPlugins ?? false;
}
44 changes: 43 additions & 1 deletion src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
createTypeChecker,
createTypeReferenceDirectiveResolutionCache,
CustomTransformers,
CustomTransformersModuleFactory,
Debug,
DeclarationWithTypeParameterChildren,
Diagnostic,
Expand Down Expand Up @@ -156,6 +157,7 @@ import {
getTsBuildInfoEmitOutputFilePath,
getTsConfigObjectLiteralExpression,
getTsConfigPropArrayElementValue,
getTypeScriptNamespace,
getTypesPackageName,
HasChangedAutomaticTypeDirectiveNames,
hasChangesInResolutions,
Expand Down Expand Up @@ -222,6 +224,7 @@ import {
mapDefined,
maybeBind,
memoize,
mergeCustomTransformers,
MethodDeclaration,
ModeAwareCache,
ModeAwareCacheKey,
Expand Down Expand Up @@ -292,6 +295,7 @@ import {
ScriptTarget,
setParent,
setParentRecursive,
shouldAllowPlugins,
skipTrivia,
skipTypeChecking,
some,
Expand Down Expand Up @@ -500,6 +504,7 @@ export function createCompilerHostWorker(
readDirectory: (path, extensions, include, exclude, depth) => system.readDirectory(path, extensions, include, exclude, depth),
createDirectory: d => system.createDirectory(d),
createHash: maybeBind(system, system.createHash),
require: maybeBind(system, system.require),
};
return compilerHost;
}
Expand Down Expand Up @@ -2828,6 +2833,34 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
return hasEmitBlockingDiagnostics.has(toPath(emitFileName));
}

function getCustomTransformers() {
if (!host.require) {
return emptyArray;
}
Comment on lines +2837 to +2839
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe allow CompilerOptions to feed transformer factories directly, it will be better for API users.


const compilerOptions = program.getCompilerOptions();
if (!shouldAllowPlugins(compilerOptions)) {
return emptyArray;
}

const customTransformers = mapDefined(compilerOptions.plugins, config => {
if (config.type !== "transformer") return undefined;

// TODO(jakebailey): The LS plugin loader is more complicated than this; copy.
const result = host.require!(program.getCurrentDirectory(), config.path);
Copy link

@samchon samchon May 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should start from location of tsconfig.json file, but you're using current-working-directory.

This is the one of different part with ttypescript (and ts-patch).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is theoretically the tsconfig dir, but yes, this code is very WIP.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested in test/tsconfig.json file and program.getCurrentDirectory() was typia/.

  • typia/
    • node_modules/
    • package.json
    • tsconfig.json
    • test/
      • tsconfig.json

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Oops, but all of this is going to be replaced anyway. This branch is effectively demo code at the moment.

// TODO(jakebailey): error handling, only do this once per, etc
Debug.assertIsDefined(result.module);

const factory = result.module as CustomTransformersModuleFactory;
Debug.assert(typeof factory === "function");

const plugin = factory({ typescript: getTypeScriptNamespace() });
return plugin.create({ program, config });
});

return customTransformers ?? emptyArray;
}

function emitWorker(
program: Program,
sourceFile: SourceFile | undefined,
Expand Down Expand Up @@ -2860,14 +2893,15 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg

performance.mark("beforeEmit");

const mergedCustomTransformers = mergeCustomTransformers(...getCustomTransformers(), customTransformers);
const emitResult = typeChecker.runWithCancellationToken(
cancellationToken,
() =>
emitFiles(
emitResolver,
getEmitHost(writeFileCallback),
sourceFile,
getTransformers(options, customTransformers, emitOnly),
getTransformers(options, mergedCustomTransformers, emitOnly),
emitOnly,
/*onlyBuildInfo*/ false,
forceDtsEmit,
Expand Down Expand Up @@ -4541,6 +4575,14 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
});
}

if (options.plugins && !shouldAllowPlugins(options)) {
for (const plugin of options.plugins) {
if (plugin.type === undefined) continue; // Language service plugins. TODO(jakebailey): give these a type, require type, deprecate missing type?
programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Option_allowPlugins_must_be_specified_when_compiler_plugins_are_present));
break;
}
}

// Verify that all the emit files are unique and don't overwrite input files
function verifyEmitFilePath(emitFileName: string | undefined, emitFilesSeen: Set<string>) {
if (emitFileName) {
Expand Down
16 changes: 16 additions & 0 deletions src/compiler/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Bundle,
chainBundle,
CompilerOptions,
concatenate,
createEmitHelperFactory,
CustomTransformer,
CustomTransformerFactory,
Expand Down Expand Up @@ -116,6 +117,21 @@ export function getTransformers(compilerOptions: CompilerOptions, customTransfor
};
}

/** @internal */
export function mergeCustomTransformers(...customTransformers: (CustomTransformers | undefined)[]): CustomTransformers | undefined {
if (!some(customTransformers)) return undefined;

const result: CustomTransformers = {};
for (const transformer of customTransformers) {
if (!transformer) continue;
result.before = concatenate(result.before, transformer.before);
result.after = concatenate(result.after, transformer.after);
result.afterDeclarations = concatenate(result.afterDeclarations, transformer.afterDeclarations);
}

return result;
}

function getScriptTransformers(compilerOptions: CompilerOptions, customTransformers?: CustomTransformers, emitOnly?: boolean | EmitOnly) {
if (emitOnly) return emptyArray;

Expand Down
1 change: 1 addition & 0 deletions src/compiler/tsbuildPublic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export interface BuildOptions {
/** @internal */ locale?: string;
/** @internal */ generateCpuProfile?: string;
/** @internal */ generateTrace?: string;
allowPlugins?: boolean;

[option: string]: CompilerOptionsValue | undefined;
}
Expand Down
34 changes: 32 additions & 2 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
SymlinkCache,
ThisContainer,
} from "./_namespaces/ts.js";
import * as ts from "./_namespaces/ts.js";

// branded string type used to store absolute, normalized and canonicalized paths
// arbitrary file name can be converted to Path via toPath function
Expand Down Expand Up @@ -7157,8 +7158,18 @@ export enum ModuleDetectionKind {
Force = 3,
}

export type PluginConfig = PluginImport | TransformerPluginImport;

export interface PluginImport {
type?: undefined;
name: string;
/** @internal */
global?: boolean;
}

export interface TransformerPluginImport {
type: "transformer";
path: string;
}

export interface ProjectReference {
Expand Down Expand Up @@ -7195,7 +7206,7 @@ export enum PollingWatchKind {
FixedChunkSize,
}

export type CompilerOptionsValue = string | number | boolean | (string | number)[] | string[] | MapLike<string[]> | PluginImport[] | ProjectReference[] | null | undefined; // eslint-disable-line no-restricted-syntax
export type CompilerOptionsValue = string | number | boolean | (string | number)[] | string[] | MapLike<string[]> | PluginConfig[] | ProjectReference[] | null | undefined; // eslint-disable-line no-restricted-syntax

export interface CompilerOptions {
/** @internal */ all?: boolean;
Expand Down Expand Up @@ -7304,7 +7315,7 @@ export interface CompilerOptions {
* @internal
*/
pathsBasePath?: string;
/** @internal */ plugins?: PluginImport[];
/** @internal */ plugins?: PluginConfig[];
preserveConstEnums?: boolean;
noImplicitOverride?: boolean;
preserveSymlinks?: boolean;
Expand Down Expand Up @@ -7354,6 +7365,8 @@ export interface CompilerOptions {
/** @internal */ showConfig?: boolean;
useDefineForClassFields?: boolean;
/** @internal */ tscBuild?: boolean;
customTransformers?: string[];
allowPlugins?: boolean;

[option: string]: CompilerOptionsValue | TsConfigSourceFile | undefined;
}
Expand Down Expand Up @@ -7971,6 +7984,7 @@ export interface CompilerHost extends ModuleResolutionHost {
createHash?(data: string): string;
getParsedCommandLine?(fileName: string): ParsedCommandLine | undefined;
/** @internal */ useSourceOfProjectReferenceRedirect?(): boolean;
/** @internal */ require?(baseDir: string, moduleName: string): ModuleImportResult;

// TODO: later handle this in better way in builder host instead once the api for tsbuild finalizes and doesn't use compilerHost as base
/** @internal */ createDirectory?(directory: string): void;
Expand Down Expand Up @@ -10267,3 +10281,19 @@ export interface SyntacticTypeNodeBuilderResolver {
requiresAddingImplicitUndefined(parameter: ParameterDeclaration | JSDocParameterTag): boolean;
isDefinitelyReferenceToGlobalSymbolObject(node: Node): boolean;
}

export type CustomTransformersModuleFactory = (mod: { typescript: typeof ts; }) => CustomTransformersModule;

export interface CustomTransformersModuleWithName {
name: string;
module: CustomTransformersModule;
}

export interface CustomTransformersModule {
create(createInfo: CustomTransformersCreateInfo): CustomTransformers;
}

export interface CustomTransformersCreateInfo {
program: Program;
config: any;
}
Loading