Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/typescript/src/vscode/extensionApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface ContentMapperManifest {
readonly cwd?: ContentMapperUri | undefined;
readonly compilerOptions?: readonly string[] | undefined;
readonly dynamicConfig?: boolean | undefined;
readonly outputExtensions?: Readonly<Record<string, string>> | undefined;
}

export interface ContentMapperContribution {
Expand Down
18 changes: 16 additions & 2 deletions packages/vscode-typescript/src/contentMapperContributions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface SerializedContentMapperContribution {
readonly cwd?: string;
readonly compilerOptions?: readonly string[];
readonly dynamicConfig?: boolean;
readonly outputExtensions?: Readonly<Record<string, string>>;
};
};
}
Expand All @@ -33,6 +34,9 @@ export function serializeContentMapperContributions(
exec: [...contribution.inferredProjectContribution.manifest.exec],
cwd: contribution.inferredProjectContribution.manifest.cwd?.fsPath,
compilerOptions: contribution.inferredProjectContribution.manifest.compilerOptions && [...contribution.inferredProjectContribution.manifest.compilerOptions],
...(contribution.inferredProjectContribution.manifest.outputExtensions && {
outputExtensions: { ...contribution.inferredProjectContribution.manifest.outputExtensions },
}),
},
},
});
Expand All @@ -46,8 +50,11 @@ export function validateContentMapperRegistration(contributorId: string, contrib
throw new TypeError("Content mapper contributor ID must not be empty.");
}
for (const contribution of contributions) {
if (contribution.extensions.length === 0 || contribution.extensions.some(extension => !extension.startsWith(".") || extension.length === 1)) {
throw new TypeError("Content mapper contributions require non-empty extensions beginning with '.'.");
if (contribution.extensions.length === 0 || contribution.extensions.some(extension => !isExtension(extension))) {
throw new TypeError("Content mapper contributions require non-empty extensions beginning with '.' and containing no path separators.");
}
if (contribution.extensions.some(extension => /\.(?:[cm]?[jt]s|[jt]sx|json)$/i.test(extension))) {
throw new TypeError("Content mapper extensions must not end in a built-in extension.");
}
const inferredProjectContribution = contribution.inferredProjectContribution;
if (inferredProjectContribution?.options === null || Array.isArray(inferredProjectContribution?.options) || inferredProjectContribution?.options !== undefined && typeof inferredProjectContribution.options !== "object") {
Expand All @@ -59,9 +66,16 @@ export function validateContentMapperRegistration(contributorId: string, contrib
if (inferredProjectContribution?.manifest.cwd && inferredProjectContribution.manifest.cwd.scheme !== "file") {
throw new TypeError("Content mapper contribution cwd must be a file URI.");
}
if (inferredProjectContribution?.manifest.outputExtensions && Object.entries(inferredProjectContribution.manifest.outputExtensions).some(([source, output]) => !isExtension(source) || !isExtension(output))) {
throw new TypeError("Content mapper output extensions must be non-empty, begin with '.', and contain no path separators.");
}
}
}

function isExtension(value: string): boolean {
return value.length > 1 && value.startsWith(".") && !/[\\/]/.test(value);
}

export function documentMatchesContentMapperContributions(
document: { readonly uri: { readonly path: string; }; },
registrations: ReadonlyMap<string, readonly ContentMapperContribution[]>,
Expand Down
37 changes: 37 additions & 0 deletions packages/vscode-typescript/test/contentMapperContributions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type ContentMapperContribution,
documentMatchesContentMapperContributions,
serializeContentMapperContributions,
validateContentMapperRegistration,
} from "../src/contentMapperContributions";

const documentedContribution = {
Expand All @@ -16,11 +17,46 @@ const documentedContribution = {
exec: ["node", "mapper.js"],
compilerOptions: ["strict"],
dynamicConfig: true,
outputExtensions: { ".vue": ".js" },
},
},
} satisfies ContentMapperContribution;

describe("content mapper contributions", { concurrency: true }, () => {
test("rejects path separators in source and output extensions", () => {
for (const extension of [".vue/foo", ".vue\\foo"]) {
assert.throws(() => validateContentMapperRegistration("mapper", [{ extensions: [extension] }]), /path separators/);
for (const outputExtensions of [{ [extension]: ".js" }, { ".vue": extension }]) {
assert.throws(() =>
validateContentMapperRegistration("mapper", [{
...documentedContribution,
inferredProjectContribution: {
manifest: { ...documentedContribution.inferredProjectContribution.manifest, outputExtensions },
},
}]), /path separators/);
}
}
});

test("accepts compound extensions with non-reserved suffixes", () => {
assert.doesNotThrow(() =>
validateContentMapperRegistration("mapper", [{
extensions: [".y.z", ".component.tsx.vue"],
}])
);
});

test("rejects builtin extensions and compound extensions ending in them", () => {
for (const extension of [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".json", ".d.ts", ".d.mts", ".d.cts"]) {
for (const source of [extension, extension.toUpperCase(), `.component${extension}`, `.component${extension.toUpperCase()}`]) {
assert.throws(() =>
validateContentMapperRegistration("mapper", [{
extensions: [source],
}]), /built-in extension/);
}
}
});

test("content mapper extensions match document paths case-insensitively", () => {
const registrations = new Map<string, readonly ContentMapperContribution[]>([[
"publisher.extension",
Expand Down Expand Up @@ -49,6 +85,7 @@ describe("content mapper contributions", { concurrency: true }, () => {
cwd: undefined,
compilerOptions: ["strict"],
dynamicConfig: true,
outputExtensions: { ".vue": ".js" },
},
},
}]);
Expand Down
1 change: 1 addition & 0 deletions tools/scripts/lsp/generate.mts
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,7 @@ const customStructures: Structure[] = [
{ name: "cwd", type: { kind: "base", name: "string" }, optional: true, documentation: "Absolute working directory for the mapper process." },
{ name: "compilerOptions", type: { kind: "array", element: { kind: "base", name: "string" } }, optional: true, documentation: "Compiler option names forwarded to the mapper." },
{ name: "dynamicConfig", type: { kind: "base", name: "boolean" }, optional: true, documentation: "Whether the mapper uses project-scoped dynamic configuration." },
{ name: "outputExtensions", type: { kind: "map", key: { kind: "base", name: "string" }, value: { kind: "base", name: "string" } }, optional: true, documentation: "Default source-to-runtime output extension mappings." },
],
documentation: "Inline content mapper manifest supplied by a contributing extension.",
},
Expand Down
45 changes: 37 additions & 8 deletions tsc/internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -8449,6 +8449,9 @@ func (c *Checker) checkImportCallExpression(node *ast.Node) *Type {
return c.createPromiseReturnType(node, c.anyType) //nolint:customlint // no arguments to check
}
specifier := args[0]
if c.compilerOptions.RewriteRelativeImportExtensions.IsTrue() && !ast.IsStringLiteralLike(specifier) {
c.checkExternalEmitHelpers(node, ExternalEmitHelpersRewriteRelativeImportExtension)
}
specifierType := c.checkExpressionCached(specifier)
var optionsType *Type
if len(args) > 1 {
Expand Down Expand Up @@ -8501,6 +8504,11 @@ func (c *Checker) checkImportCallExpression(node *ast.Node) *Type {
* @returns On success, the expression's signature's return type. On failure, anyType.
*/
func (c *Checker) checkCallExpression(node *ast.Node, checkMode CheckMode) *Type {
if c.compilerOptions.RewriteRelativeImportExtensions.IsTrue() &&
ast.IsInJSFile(node) && ast.IsRequireCall(node, false /*requireStringLiteralLikeArgument*/) &&
!ast.IsStringLiteralLike(node.Arguments()[0]) {
c.checkExternalEmitHelpers(node, ExternalEmitHelpersRewriteRelativeImportExtension)
}
c.checkGrammarTypeArguments(node, node.TypeArgumentList())
signature := c.getResolvedSignature(node, nil /*candidatesOutArray*/, checkMode)
if signature == c.resolvingSignature {
Expand Down Expand Up @@ -15507,6 +15515,12 @@ func (c *Checker) resolveExternalModule(
}

if errorNode != nil {
rewrites := c.program.ContentMapperExtensionRewrites()
caseSensitivity := c.program.CaseSensitivity()
specifier := tspath.ToModuleSpecifier(moduleReference)
_, mappedSpecifier := specifier.RewriteExtension(rewrites, caseSensitivity)
checkDeclarationRewrite := c.compilerOptions.GetEmitDeclarations() && mappedSpecifier &&
tspath.PathIsRelative(moduleReference) && !importingSourceFile.IsDeclarationFile
if resolvedModule.ResolvedUsingTsExtension && tspath.IsDeclarationFileName(moduleReference) {
if ast.FindAncestor(location, ast.IsEmittableImport) != nil {
tsExtension := tspath.TryExtractTSExtension(moduleReference)
Expand Down Expand Up @@ -15543,12 +15557,18 @@ func (c *Checker) resolveExternalModule(
)
}
} else if c.compilerOptions.RewriteRelativeImportExtensions.IsTrue() &&
location.Flags&ast.NodeFlagsAmbient == 0 &&
(location.Flags&ast.NodeFlagsAmbient == 0 || checkDeclarationRewrite) &&
!tspath.IsDeclarationFileName(moduleReference) &&
!ast.IsLiteralImportTypeNode(location) &&
!ast.IsPartOfTypeOnlyImportOrExportDeclaration(location) {
shouldRewrite := core.ShouldRewriteModuleSpecifier(moduleReference, c.compilerOptions)
if !resolvedModule.ResolvedUsingTsExtension && shouldRewrite {
(!ast.IsLiteralImportTypeNode(location) && !ast.IsPartOfTypeOnlyImportOrExportDeclaration(location) ||
checkDeclarationRewrite) {
shouldRewrite := specifier.ShouldRewriteExtension(rewrites, caseSensitivity)
mappedSource := false
if sourceFile.ContentMapper() != "" {
_, mappedSource = sourceFile.FileName().RewriteExtension(rewrites, caseSensitivity)
}
resolvedUsingRewritableExtension := resolvedModule.ResolvedUsingTsExtension ||
resolvedModule.ResolvedUsingExtraExtensions && sourceFile.ContentMapper() != "" && mappedSource
if !resolvedUsingRewritableExtension && shouldRewrite {
relativeToSourceFile := resolvedModule.ResolvedFileName.AsString()
if relativePath, ok := c.program.CaseSensitivity().RelativePathFromFile(
importingSourceFile.FileName(),
Expand All @@ -15562,13 +15582,18 @@ func (c *Checker) resolveExternalModule(
diagnostics.This_relative_import_path_is_unsafe_to_rewrite_because_it_looks_like_a_file_name_but_actually_resolves_to_0,
relativeToSourceFile,
)
} else if resolvedModule.ResolvedUsingTsExtension && !shouldRewrite && c.program.SourceFileMayBeEmitted(sourceFile, false) {
} else if resolvedUsingRewritableExtension && !shouldRewrite && (mappedSource || c.program.SourceFileMayBeEmitted(sourceFile, false)) {
extension := tspath.GetAnyExtensionFromPath(moduleReference, nil, tspath.CaseSensitive)
if mappedSource {
rewrite, _ := sourceFile.FileName().GetExtensionRewrite(rewrites, caseSensitivity)
extension = rewrite.Source
}
c.error(
errorNode,
diagnostics.This_import_uses_a_0_extension_to_resolve_to_an_input_TypeScript_file_but_will_not_be_rewritten_during_emit_because_it_is_not_a_relative_path,
tspath.GetAnyExtensionFromPath(moduleReference, nil, tspath.CaseSensitive),
extension,
)
} else if resolvedModule.ResolvedUsingTsExtension && shouldRewrite {
} else if resolvedUsingRewritableExtension && shouldRewrite {
if redirect := c.program.GetRedirectForResolution(sourceFile); redirect != nil {
ownRootDir := c.program.CommonSourceDirectory()
otherRootDir := redirect.CommonSourceDirectory()
Expand Down Expand Up @@ -29106,6 +29131,10 @@ func (c *Checker) checkExternalEmitHelpers(location *ast.Node, helpers ExternalE
if !c.hasSignatureWithArityGreaterThan(symbol, 4) {
c.error(location, diagnostics.This_syntax_requires_an_imported_helper_named_1_with_2_parameters_which_is_not_compatible_with_the_one_in_0_Consider_upgrading_your_version_of_0, externalHelpersModuleNameText, name, 5)
}
} else if helper&ExternalEmitHelpersRewriteRelativeImportExtension != 0 && len(c.program.ContentMapperExtensionRewrites()) > 0 {
if !c.hasSignatureWithArityGreaterThan(symbol, 3) {
c.error(location, diagnostics.This_syntax_requires_an_imported_helper_named_1_with_2_parameters_which_is_not_compatible_with_the_one_in_0_Consider_upgrading_your_version_of_0, externalHelpersModuleNameText, name, 4)
}
}
}
}
Expand Down
13 changes: 13 additions & 0 deletions tsc/internal/checker/nodebuilderimpl.go
Original file line number Diff line number Diff line change
Expand Up @@ -1342,9 +1342,22 @@ func (b *NodeBuilderImpl) moduleSpecifierResultForSymbol(result moduleSpecifierR
if importAttributesType != nil && b.moduleSpecifierResolvesToSymbol(result.specifier, importAttributesType, symbol) {
result.importAttributesType = importAttributesType
}
result.specifier = b.rewriteContentMappedModuleSpecifier(result.specifier)
return result
}

func (b *NodeBuilderImpl) rewriteContentMappedModuleSpecifier(specifier tspath.ModuleSpecifier) tspath.ModuleSpecifier {
if b.ctx.internalFlags&nodebuilder.InternalFlagsRewriteModuleSpecifiers != 0 && b.ch.compilerOptions.RewriteRelativeImportExtensions.IsTrue() {
rewrites := b.ctx.host.ContentMapperExtensionRewrites()
caseSensitivity := b.ctx.host.CaseSensitivity()
if specifier.ShouldRewriteExtension(rewrites, caseSensitivity) {
rewritten, _ := specifier.RewriteExtension(rewrites, caseSensitivity)
return rewritten
}
}
return specifier
}

func (b *NodeBuilderImpl) moduleSpecifierResolvesToSymbol(specifier tspath.ModuleSpecifier, importAttributesType *Type, symbol *ast.Symbol) bool {
location := b.ctx.enclosingDeclaration
if location == nil && b.ctx.enclosingFile != nil {
Expand Down
6 changes: 5 additions & 1 deletion tsc/internal/checker/nodecopy.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/microsoft/TypeScript/tsc/internal/core"
"github.com/microsoft/TypeScript/tsc/internal/nodebuilder"
"github.com/microsoft/TypeScript/tsc/internal/printer"
"github.com/microsoft/TypeScript/tsc/internal/tspath"
)

func (b *NodeBuilderImpl) reuseNode(node *ast.Node) *ast.Node {
Expand Down Expand Up @@ -270,7 +271,10 @@ func (b *NodeBuilderImpl) getModuleSpecifierOverride(parent *ast.Node, lit *ast.
func (b *NodeBuilderImpl) rewriteModuleSpecifier(parent *ast.Node, lit *ast.Node) *ast.Node {
newName := b.getModuleSpecifierOverride(parent, lit)
if len(newName) == 0 {
return lit
newName = b.rewriteContentMappedModuleSpecifier(tspath.ToModuleSpecifier(lit.Text())).AsString()
if newName == lit.Text() {
return lit
}
}
res := b.f.NewStringLiteral(newName, ast.TokenFlagsNone)
b.e.SetOriginal(res, lit)
Expand Down
4 changes: 4 additions & 0 deletions tsc/internal/compiler/emitHost.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ func (host *emitHost) ContentMapperExtensions() []string {
return host.program.ContentMapperExtensions()
}

func (host *emitHost) ContentMapperExtensionRewrites() []tspath.ExtensionRewrite {
return host.program.ContentMapperExtensionRewrites()
}

func (host *emitHost) CaseSensitivity() tspath.CaseSensitivity {
return host.program.CaseSensitivity()
}
Expand Down
12 changes: 7 additions & 5 deletions tsc/internal/compiler/emitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,13 @@ func getScriptTransformers(emitContext *printer.EmitContext, host printer.EmitHo
}

opts := transformers.TransformOptions{
Context: emitContext,
CompilerOptions: options,
Resolver: referenceResolver,
EmitResolver: emitResolver,
GetEmitModuleFormatOfFile: host.GetEmitModuleFormatOfFile,
Context: emitContext,
CompilerOptions: options,
Resolver: referenceResolver,
EmitResolver: emitResolver,
GetEmitModuleFormatOfFile: host.GetEmitModuleFormatOfFile,
ContentMapperExtensionRewrites: host.ContentMapperExtensionRewrites(),
CaseSensitivity: host.CaseSensitivity(),
}

// transform TypeScript syntax
Expand Down
14 changes: 12 additions & 2 deletions tsc/internal/compiler/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,11 @@ func (p *Program) GetContentMapper(file *ast.SourceFile) *contentmapper.Mapper {
return nil
}

func (p *Program) ContentMapperExtensions() []string { return p.opts.Config.ContentMapperExtensions() }
func (p *Program) ContentMapperExtensions() []string { return p.opts.Config.ContentMapperExtensions() }

func (p *Program) ContentMapperExtensionRewrites() []tspath.ExtensionRewrite {
return p.opts.Config.ContentMapperExtensionRewrites()
}
func (p *Program) CommandLine() *tsoptions.ParsedCommandLine { return p.opts.Config }
func (p *Program) Host() CompilerHost { return p.hosts.Host }
func (p *Program) Tracing() *tracing.Tracing { return p.hosts.Tracing }
Expand Down Expand Up @@ -1395,13 +1399,19 @@ func (p *Program) verifyCompilerOptions() {
}
}

externalOutputSourceFiles := core.Filter(p.SourceFiles(), func(sourceFile *ast.SourceFile) bool {
return sourceFile.ContentMapper() != "" &&
!p.IsSourceFileFromExternalLibrary(sourceFile) &&
p.GetProjectReferenceFromSource(sourceFile.PathKey()) == nil
})
outputpaths.ForEachEmittedFile(p, options, func(emitFileNames *outputpaths.OutputPaths, sourceFile *ast.SourceFile) bool {
verifyEmitFilePath(emitFileNames.JsFilePath())
verifyEmitFilePath(emitFileNames.SourceMapFilePath())
verifyEmitFilePath(emitFileNames.DeclarationFilePath())
verifyEmitFilePath(emitFileNames.DeclarationMapPath())
verifyEmitFilePath(emitFileNames.ExternalOutputFilePath())
return false
}, p.getSourceFilesToEmit(nil, false, false), false)
}, p.getSourceFilesToEmit(nil, false, false), externalOutputSourceFiles, false)
verifyEmitFilePath(p.opts.Config.GetBuildInfoFileName())
}
}
Expand Down
Loading
Loading