Skip to content

Commit d0c0d1c

Browse files
committed
fix symlink tag, support arbitrary (ie, directory) links via @link
Introduce indirect symlink lookup to specifier deriver Use fileset, move exec vfs path resolution :shakes fist: Apply files symlink relative to dirname Use directory function
1 parent 1aad3c6 commit d0c0d1c

11 files changed

+348
-17
lines changed

src/compiler/checker.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -4091,7 +4091,14 @@ namespace ts {
40914091
}
40924092
else {
40934093
const contextFile = getSourceFileOfNode(getOriginalNode(context!.enclosingDeclaration))!;
4094-
return `"${file.moduleName || moduleSpecifiers.getModuleSpecifier(compilerOptions, contextFile, contextFile.path, file.path, context!.tracker.moduleResolverHost!)}"`;
4094+
return `"${file.moduleName || moduleSpecifiers.getModuleSpecifiers(
4095+
symbol,
4096+
compilerOptions,
4097+
contextFile,
4098+
context!.tracker.moduleResolverHost!,
4099+
context!.tracker.moduleResolverHost!.getSourceFiles!(),
4100+
{ importModuleSpecifierPreference: "non-relative" }
4101+
)[0]}"`;
40954102
}
40964103
}
40974104
const declaration = symbol.declarations[0];

src/compiler/moduleSpecifiers.ts

+52-7
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,20 @@ namespace ts.moduleSpecifiers {
1515
// For each symlink/original for a module, returns a list of ways to import that file.
1616
export function getModuleSpecifiers(
1717
moduleSymbol: Symbol,
18-
program: Program,
18+
compilerOptions: CompilerOptions,
1919
importingSourceFile: SourceFile,
2020
host: ModuleSpecifierResolutionHost,
21+
files: ReadonlyArray<SourceFile>,
2122
preferences: ModuleSpecifierPreferences,
2223
): ReadonlyArray<ReadonlyArray<string>> {
2324
const ambient = tryGetModuleNameFromAmbientModule(moduleSymbol);
2425
if (ambient) return [[ambient]];
2526

26-
const compilerOptions = program.getCompilerOptions();
27-
const info = getInfo(compilerOptions, importingSourceFile, importingSourceFile.fileName, host);
28-
const modulePaths = getAllModulePaths(program, getSourceFileOfNode(moduleSymbol.valueDeclaration));
27+
const info = getInfo(compilerOptions, importingSourceFile, importingSourceFile.path, host);
28+
if (!files) {
29+
return Debug.fail("Files list must be present to resolve symlinks in specifier resolution");
30+
}
31+
const modulePaths = getAllModulePaths(files, getSourceFileOfNode(moduleSymbol.valueDeclaration), info.getCanonicalFileName, host);
2932

3033
const global = mapDefined(modulePaths, moduleFileName => getGlobalModuleSpecifier(moduleFileName, info, host, compilerOptions));
3134
return global.length ? global.map(g => [g]) : modulePaths.map(moduleFileName =>
@@ -130,15 +133,57 @@ namespace ts.moduleSpecifiers {
130133
return firstDefined(imports, ({ text }) => pathIsRelative(text) ? fileExtensionIs(text, Extension.Js) : undefined) || false;
131134
}
132135

136+
function discoverProbableSymlinks(files: ReadonlyArray<SourceFile>) {
137+
const symlinks = mapDefined(files, sf =>
138+
sf.resolvedModules && firstDefinedIterator(sf.resolvedModules.values(), res =>
139+
res && res.originalPath && res.resolvedFileName !== res.originalPath ? [res.resolvedFileName, res.originalPath] : undefined));
140+
const result = createMap<string>();
141+
if (symlinks) {
142+
for (const [resolvedPath, originalPath] of symlinks) {
143+
const resolvedParts = getPathComponents(resolvedPath);
144+
const originalParts = getPathComponents(originalPath);
145+
while (resolvedParts[resolvedParts.length - 1] === originalParts[originalParts.length - 1]) {
146+
resolvedParts.pop();
147+
originalParts.pop();
148+
}
149+
result.set(getPathFromPathComponents(originalParts), getPathFromPathComponents(resolvedParts));
150+
}
151+
}
152+
return result;
153+
}
154+
155+
function getAllModulePathsUsingIndirectSymlinks(files: ReadonlyArray<SourceFile>, target: string, getCanonicalFileName: (file: string) => string, host: ModuleSpecifierResolutionHost) {
156+
const links = discoverProbableSymlinks(files);
157+
const paths = arrayFrom(links.keys());
158+
let options: string[] | undefined;
159+
for (const path of paths) {
160+
const resolved = links.get(path)!;
161+
if (startsWith(target, resolved + "/")) {
162+
const relative = getRelativePathFromDirectory(resolved, target, getCanonicalFileName);
163+
const option = resolvePath(path, relative);
164+
if (!host.fileExists || host.fileExists(option)) {
165+
if (!options) options = [];
166+
options.push(option);
167+
}
168+
}
169+
}
170+
const resolvedtarget = host.getCurrentDirectory ? resolvePath(host.getCurrentDirectory(), target) : target;
171+
if (options) {
172+
options.push(resolvedtarget); // Since these are speculative, we also include the original resolved name as a possibility
173+
return options;
174+
}
175+
return [resolvedtarget];
176+
}
177+
133178
/**
134179
* Looks for a existing imports that use symlinks to this module.
135180
* Only if no symlink is available, the real path will be used.
136181
*/
137-
function getAllModulePaths(program: Program, { fileName }: SourceFile): ReadonlyArray<string> {
138-
const symlinks = mapDefined(program.getSourceFiles(), sf =>
182+
function getAllModulePaths(files: ReadonlyArray<SourceFile>, { fileName }: SourceFile, getCanonicalFileName: (file: string) => string, host: ModuleSpecifierResolutionHost): ReadonlyArray<string> {
183+
const symlinks = mapDefined(files, sf =>
139184
sf.resolvedModules && firstDefinedIterator(sf.resolvedModules.values(), res =>
140185
res && res.resolvedFileName === fileName ? res.originalPath : undefined));
141-
return symlinks.length === 0 ? [fileName] : symlinks;
186+
return symlinks.length === 0 ? getAllModulePathsUsingIndirectSymlinks(files, fileName, getCanonicalFileName, host) : symlinks;
142187
}
143188

144189
function getRelativePathNParents(relativePath: string): number {

src/compiler/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5312,6 +5312,7 @@ namespace ts {
53125312
useCaseSensitiveFileNames?(): boolean;
53135313
fileExists?(path: string): boolean;
53145314
readFile?(path: string): string | undefined;
5315+
getSourceFiles?(): ReadonlyArray<SourceFile>; // Used for cached resolutions to find symlinks without traversing the fs (again)
53155316
}
53165317

53175318
/** @deprecated See comment on SymbolWriter */

src/harness/compilerRunner.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,9 @@ class CompilerTest {
187187
this.otherFiles,
188188
this.harnessSettings,
189189
/*options*/ tsConfigOptions,
190-
/*currentDirectory*/ this.harnessSettings.currentDirectory);
190+
/*currentDirectory*/ this.harnessSettings.currentDirectory,
191+
testCaseContent.symlinks
192+
);
191193

192194
this.options = this.result.options;
193195
}

src/harness/harness.ts

+18-4
Original file line numberDiff line numberDiff line change
@@ -1136,6 +1136,7 @@ namespace Harness {
11361136
{ name: "noImplicitReferences", type: "boolean" },
11371137
{ name: "currentDirectory", type: "string" },
11381138
{ name: "symlink", type: "string" },
1139+
{ name: "link", type: "string" },
11391140
// Emitted js baseline will print full paths for every output file
11401141
{ name: "fullEmitPaths", type: "boolean" }
11411142
];
@@ -1207,7 +1208,9 @@ namespace Harness {
12071208
harnessSettings: TestCaseParser.CompilerSettings | undefined,
12081209
compilerOptions: ts.CompilerOptions | undefined,
12091210
// Current directory is needed for rwcRunner to be able to use currentDirectory defined in json file
1210-
currentDirectory: string | undefined): compiler.CompilationResult {
1211+
currentDirectory: string | undefined,
1212+
symlinks?: vfs.FileSet
1213+
): compiler.CompilationResult {
12111214
const options: ts.CompilerOptions & HarnessOptions = compilerOptions ? ts.cloneCompilerOptions(compilerOptions) : { noResolve: false };
12121215
options.target = options.target || ts.ScriptTarget.ES3;
12131216
options.newLine = options.newLine || ts.NewLineKind.CarriageReturnLineFeed;
@@ -1244,6 +1247,9 @@ namespace Harness {
12441247

12451248
const docs = inputFiles.concat(otherFiles).map(documents.TextDocument.fromTestFile);
12461249
const fs = vfs.createFromFileSystem(IO, !useCaseSensitiveFileNames, { documents: docs, cwd: currentDirectory });
1250+
if (symlinks) {
1251+
fs.apply(symlinks);
1252+
}
12471253
const host = new fakes.CompilerHost(fs, options);
12481254
return compiler.compileFiles(host, programFileNames, options);
12491255
}
@@ -1864,6 +1870,7 @@ namespace Harness {
18641870

18651871
// Regex for parsing options in the format "@Alpha: Value of any sort"
18661872
const optionRegex = /^[\/]{2}\s*@(\w+)\s*:\s*([^\r\n]*)/gm; // multiple matches on multiple lines
1873+
const linkRegex = /^[\/]{2}\s*@link\s*:\s*([^\r\n]*)\s*->\s*([^\r\n]*)/gm; // multiple matches on multiple lines
18671874

18681875
export function extractCompilerSettings(content: string): CompilerSettings {
18691876
const opts: CompilerSettings = {};
@@ -1883,6 +1890,7 @@ namespace Harness {
18831890
testUnitData: TestUnitData[];
18841891
tsConfig: ts.ParsedCommandLine | undefined;
18851892
tsConfigFileUnitData: TestUnitData | undefined;
1893+
symlinks?: vfs.FileSet;
18861894
}
18871895

18881896
/** Given a test file containing // @FileName directives, return an array of named units of code to be added to an existing compiler instance */
@@ -1897,10 +1905,16 @@ namespace Harness {
18971905
let currentFileOptions: any = {};
18981906
let currentFileName: any;
18991907
let refs: string[] = [];
1908+
let symlinks: vfs.FileSet | undefined;
19001909

19011910
for (const line of lines) {
1902-
const testMetaData = optionRegex.exec(line);
1903-
if (testMetaData) {
1911+
let testMetaData: RegExpExecArray | null;
1912+
const linkMetaData = linkRegex.exec(line);
1913+
if (linkMetaData) {
1914+
if (!symlinks) symlinks = {};
1915+
symlinks[linkMetaData[2].trim()] = new vfs.Symlink(linkMetaData[1].trim());
1916+
}
1917+
else if (testMetaData = optionRegex.exec(line)) {
19041918
// Comment line, check for global/file @options and record them
19051919
optionRegex.lastIndex = 0;
19061920
const metaDataName = testMetaData[1].toLowerCase();
@@ -1989,7 +2003,7 @@ namespace Harness {
19892003
break;
19902004
}
19912005
}
1992-
return { settings, testUnitData, tsConfig, tsConfigFileUnitData };
2006+
return { settings, testUnitData, tsConfig, tsConfigFileUnitData, symlinks };
19932007
}
19942008
}
19952009

src/harness/vfs.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -911,7 +911,7 @@ namespace vfs {
911911
if (this.stringComparer(vpath.dirname(path), path) === 0) {
912912
throw new TypeError("Roots cannot be symbolic links.");
913913
}
914-
this.symlinkSync(entry.symlink, path);
914+
this.symlinkSync(vpath.resolve(dirname, entry.symlink), path);
915915
this._applyFileExtendedOptions(path, entry);
916916
}
917917
else if (entry instanceof Link) {
@@ -1078,8 +1078,7 @@ namespace vfs {
10781078
if (symlink) {
10791079
for (const link of symlink.split(",").map(link => link.trim())) {
10801080
fs.mkdirpSync(vpath.dirname(link));
1081-
fs.symlinkSync(document.file, link);
1082-
fs.filemeta(link).set("document", document);
1081+
fs.symlinkSync(vpath.resolve(fs.cwd(), document.file), link);
10831082
}
10841083
}
10851084
}

src/services/codefixes/importFixes.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ namespace ts.codefix {
247247
preferences: UserPreferences,
248248
): ReadonlyArray<NewImportInfo> {
249249
const choicesForEachExportingModule = flatMap<SymbolExportInfo, NewImportInfo[]>(moduleSymbols, ({ moduleSymbol, importKind }) => {
250-
const modulePathsGroups = moduleSpecifiers.getModuleSpecifiers(moduleSymbol, program, sourceFile, host, preferences);
250+
const modulePathsGroups = moduleSpecifiers.getModuleSpecifiers(moduleSymbol, program.getCompilerOptions(), sourceFile, host, program.getSourceFiles(), preferences);
251251
return modulePathsGroups.map(group => group.map(moduleSpecifier => ({ moduleSpecifier, importKind })));
252252
});
253253
// Sort to keep the shortest paths first, but keep [relativePath, importRelativeToBaseUrl] groups together
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
//// [tests/cases/compiler/symbolLinkDeclarationEmitModuleNames.ts] ////
2+
3+
//// [application.ts]
4+
import { Constructor } from "@loopback/context";
5+
export type ControllerClass = Constructor<any>;
6+
//// [usage.ts]
7+
import { ControllerClass } from './application';
8+
import { BindingKey } from '@loopback/context';
9+
10+
export const CONTROLLER_CLASS = BindingKey.create<ControllerClass>(null as any); // line in question
11+
//// [value-promise.ts]
12+
export type Constructor<T> = (...args: any[]) => T;
13+
//// [bindingkey.ts]
14+
import { Constructor } from "@loopback/context"
15+
export class BindingKey<T> {
16+
readonly __type: T;
17+
static create<T extends Constructor<any>>(ctor: T) {
18+
return new BindingKey<T>();
19+
}
20+
}
21+
22+
//// [index.ts]
23+
export * from "./src/value-promise";
24+
export * from "./src/bindingkey";
25+
26+
27+
//// [value-promise.js]
28+
"use strict";
29+
exports.__esModule = true;
30+
//// [bindingkey.js]
31+
"use strict";
32+
exports.__esModule = true;
33+
var BindingKey = /** @class */ (function () {
34+
function BindingKey() {
35+
}
36+
BindingKey.create = function (ctor) {
37+
return new BindingKey();
38+
};
39+
return BindingKey;
40+
}());
41+
exports.BindingKey = BindingKey;
42+
//// [index.js]
43+
"use strict";
44+
function __export(m) {
45+
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
46+
}
47+
exports.__esModule = true;
48+
__export(require("./src/bindingkey"));
49+
//// [application.js]
50+
"use strict";
51+
exports.__esModule = true;
52+
//// [usage.js]
53+
"use strict";
54+
exports.__esModule = true;
55+
var context_1 = require("@loopback/context");
56+
exports.CONTROLLER_CLASS = context_1.BindingKey.create(null); // line in question
57+
58+
59+
//// [value-promise.d.ts]
60+
export declare type Constructor<T> = (...args: any[]) => T;
61+
//// [bindingkey.d.ts]
62+
import { Constructor } from "@loopback/context";
63+
export declare class BindingKey<T> {
64+
readonly __type: T;
65+
static create<T extends Constructor<any>>(ctor: T): BindingKey<T>;
66+
}
67+
//// [index.d.ts]
68+
export * from "./src/value-promise";
69+
export * from "./src/bindingkey";
70+
//// [application.d.ts]
71+
import { Constructor } from "@loopback/context";
72+
export declare type ControllerClass = Constructor<any>;
73+
//// [usage.d.ts]
74+
import { BindingKey } from '@loopback/context';
75+
export declare const CONTROLLER_CLASS: BindingKey<import("@loopback/context/src/value-promise").Constructor<any>>;
76+
77+
78+
//// [DtsFileErrors]
79+
80+
81+
tests/cases/compiler/monorepo/context/src/bindingkey.d.ts(1,29): error TS2307: Cannot find module '@loopback/context'.
82+
tests/cases/compiler/monorepo/core/src/application.d.ts(1,29): error TS2307: Cannot find module '@loopback/context'.
83+
tests/cases/compiler/monorepo/core/src/usage.d.ts(1,28): error TS2307: Cannot find module '@loopback/context'.
84+
tests/cases/compiler/monorepo/core/src/usage.d.ts(2,51): error TS2307: Cannot find module '@loopback/context/src/value-promise'.
85+
86+
87+
==== tests/cases/compiler/monorepo/core/src/application.d.ts (1 errors) ====
88+
import { Constructor } from "@loopback/context";
89+
~~~~~~~~~~~~~~~~~~~
90+
!!! error TS2307: Cannot find module '@loopback/context'.
91+
export declare type ControllerClass = Constructor<any>;
92+
93+
==== tests/cases/compiler/monorepo/core/src/usage.d.ts (2 errors) ====
94+
import { BindingKey } from '@loopback/context';
95+
~~~~~~~~~~~~~~~~~~~
96+
!!! error TS2307: Cannot find module '@loopback/context'.
97+
export declare const CONTROLLER_CLASS: BindingKey<import("@loopback/context/src/value-promise").Constructor<any>>;
98+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
99+
!!! error TS2307: Cannot find module '@loopback/context/src/value-promise'.
100+
101+
==== /.src/tests/cases/compiler/monorepo/context/src/value-promise.d.ts (0 errors) ====
102+
export declare type Constructor<T> = (...args: any[]) => T;
103+
104+
==== /.src/tests/cases/compiler/monorepo/context/src/bindingkey.d.ts (1 errors) ====
105+
import { Constructor } from "@loopback/context";
106+
~~~~~~~~~~~~~~~~~~~
107+
!!! error TS2307: Cannot find module '@loopback/context'.
108+
export declare class BindingKey<T> {
109+
readonly __type: T;
110+
static create<T extends Constructor<any>>(ctor: T): BindingKey<T>;
111+
}
112+
113+
==== /.src/tests/cases/compiler/monorepo/context/index.d.ts (0 errors) ====
114+
export * from "./src/value-promise";
115+
export * from "./src/bindingkey";
116+

0 commit comments

Comments
 (0)