1namespace ts.tscWatch { 2 describe("unittests:: tsc-watch:: forceConsistentCasingInFileNames", () => { 3 const loggerFile: File = { 4 path: `${projectRoot}/logger.ts`, 5 content: `export class logger { }` 6 }; 7 const anotherFile: File = { 8 path: `${projectRoot}/another.ts`, 9 content: `import { logger } from "./logger"; new logger();` 10 }; 11 const tsconfig: File = { 12 path: `${projectRoot}/tsconfig.json`, 13 content: JSON.stringify({ 14 compilerOptions: { forceConsistentCasingInFileNames: true } 15 }) 16 }; 17 18 function verifyConsistentFileNames({ subScenario, changes }: { subScenario: string; changes: TscWatchCompileChange[]; }) { 19 verifyTscWatch({ 20 scenario: "forceConsistentCasingInFileNames", 21 subScenario, 22 commandLineArgs: ["--w", "--p", tsconfig.path], 23 sys: () => createWatchedSystem([loggerFile, anotherFile, tsconfig, libFile]), 24 changes 25 }); 26 } 27 28 verifyConsistentFileNames({ 29 subScenario: "when changing module name with different casing", 30 changes: [ 31 { 32 caption: "Change module name from logger to Logger", 33 change: sys => sys.writeFile(anotherFile.path, anotherFile.content.replace("./logger", "./Logger")), 34 timeouts: runQueuedTimeoutCallbacks, 35 } 36 ] 37 }); 38 39 verifyConsistentFileNames({ 40 subScenario: "when renaming file with different casing", 41 changes: [ 42 { 43 caption: "Change name of file from logger to Logger", 44 change: sys => sys.renameFile(loggerFile.path, `${projectRoot}/Logger.ts`), 45 timeouts: runQueuedTimeoutCallbacks, 46 } 47 ] 48 }); 49 50 verifyTscWatch({ 51 scenario: "forceConsistentCasingInFileNames", 52 subScenario: "when relative information file location changes", 53 commandLineArgs: ["--w", "--p", ".", "--explainFiles"], 54 sys: () => { 55 const moduleA: File = { 56 path: `${projectRoot}/moduleA.ts`, 57 content: `import a = require("./ModuleC")` 58 }; 59 const moduleB: File = { 60 path: `${projectRoot}/moduleB.ts`, 61 content: `import a = require("./moduleC")` 62 }; 63 const moduleC: File = { 64 path: `${projectRoot}/moduleC.ts`, 65 content: `export const x = 10;` 66 }; 67 const tsconfig: File = { 68 path: `${projectRoot}/tsconfig.json`, 69 content: JSON.stringify({ compilerOptions: { forceConsistentCasingInFileNames: true } }) 70 }; 71 return createWatchedSystem([moduleA, moduleB, moduleC, libFile, tsconfig], { currentDirectory: projectRoot }); 72 }, 73 changes: [ 74 { 75 caption: "Prepend a line to moduleA", 76 change: sys => sys.prependFile(`${projectRoot}/moduleA.ts`, `// some comment 77 `), 78 timeouts: runQueuedTimeoutCallbacks, 79 } 80 ], 81 }); 82 83 verifyTscWatch({ 84 scenario: "forceConsistentCasingInFileNames", 85 subScenario: "jsxImportSource option changed", 86 commandLineArgs: ["--w", "--p", ".", "--explainFiles"], 87 sys: () => createWatchedSystem([ 88 libFile, 89 { 90 path: `${projectRoot}/node_modules/react/Jsx-runtime/index.d.ts`, 91 content: `export namespace JSX { 92 interface Element {} 93 interface IntrinsicElements { 94 div: { 95 propA?: boolean; 96 }; 97 } 98} 99export function jsx(...args: any[]): void; 100export function jsxs(...args: any[]): void; 101export const Fragment: unique symbol; 102`, 103 }, 104 { 105 path: `${projectRoot}/node_modules/react/package.json`, 106 content: JSON.stringify({ name: "react", version: "0.0.1" }) 107 }, 108 { 109 path: `${projectRoot}/index.tsx`, 110 content: `export const App = () => <div propA={true}></div>;` 111 }, 112 { 113 path: `${projectRoot}/tsconfig.json`, 114 content: JSON.stringify({ 115 compilerOptions: { jsx: "react-jsx", jsxImportSource: "react", forceConsistentCasingInFileNames: true }, 116 files: ["node_modules/react/Jsx-Runtime/index.d.ts", "index.tsx"] 117 }) 118 } 119 ], { currentDirectory: projectRoot }), 120 changes: emptyArray, 121 }); 122 123 function verifyWindowsStyleRoot(subScenario: string, windowsStyleRoot: string, projectRootRelative: string) { 124 verifyTscWatch({ 125 scenario: "forceConsistentCasingInFileNames", 126 subScenario, 127 commandLineArgs: ["--w", "--p", `${windowsStyleRoot}/${projectRootRelative}`, "--explainFiles"], 128 sys: () => { 129 const moduleA: File = { 130 path: `${windowsStyleRoot}/${projectRootRelative}/a.ts`, 131 content: ` 132export const a = 1; 133export const b = 2; 134` 135 }; 136 const moduleB: File = { 137 path: `${windowsStyleRoot}/${projectRootRelative}/b.ts`, 138 content: ` 139import { a } from "${windowsStyleRoot.toLocaleUpperCase()}/${projectRootRelative}/a" 140import { b } from "${windowsStyleRoot.toLocaleLowerCase()}/${projectRootRelative}/a" 141 142a;b; 143` 144 }; 145 const tsconfig: File = { 146 path: `${windowsStyleRoot}/${projectRootRelative}/tsconfig.json`, 147 content: JSON.stringify({ compilerOptions: { forceConsistentCasingInFileNames: true } }) 148 }; 149 return createWatchedSystem([moduleA, moduleB, libFile, tsconfig], { windowsStyleRoot, useCaseSensitiveFileNames: false }); 150 }, 151 changes: [ 152 { 153 caption: "Prepend a line to moduleA", 154 change: sys => sys.prependFile(`${windowsStyleRoot}/${projectRootRelative}/a.ts`, `// some comment 155 `), 156 timeouts: runQueuedTimeoutCallbacks, 157 } 158 ], 159 }); 160 } 161 162 verifyWindowsStyleRoot("when Windows-style drive root is lowercase", "c:/", "project"); 163 verifyWindowsStyleRoot("when Windows-style drive root is uppercase", "C:/", "project"); 164 165 function verifyFileSymlink(subScenario: string, diskPath: string, targetPath: string, importedPath: string) { 166 verifyTscWatch({ 167 scenario: "forceConsistentCasingInFileNames", 168 subScenario, 169 commandLineArgs: ["--w", "--p", ".", "--explainFiles"], 170 sys: () => { 171 const moduleA: File = { 172 173 path: diskPath, 174 content: ` 175export const a = 1; 176export const b = 2; 177` 178 }; 179 const symlinkA: SymLink = { 180 path: `${projectRoot}/link.ts`, 181 symLink: targetPath, 182 }; 183 const moduleB: File = { 184 path: `${projectRoot}/b.ts`, 185 content: ` 186import { a } from "${importedPath}"; 187import { b } from "./link"; 188 189a;b; 190` 191 }; 192 const tsconfig: File = { 193 path: `${projectRoot}/tsconfig.json`, 194 content: JSON.stringify({ compilerOptions: { forceConsistentCasingInFileNames: true } }) 195 }; 196 return createWatchedSystem([moduleA, symlinkA, moduleB, libFile, tsconfig], { currentDirectory: projectRoot }); 197 }, 198 changes: [ 199 { 200 caption: "Prepend a line to moduleA", 201 change: sys => sys.prependFile(diskPath, `// some comment 202 `), 203 timeouts: runQueuedTimeoutCallbacks, 204 } 205 ], 206 }); 207 } 208 209 verifyFileSymlink("when both file symlink target and import match disk", `${projectRoot}/XY.ts`, `${projectRoot}/XY.ts`, `./XY`); 210 verifyFileSymlink("when file symlink target matches disk but import does not", `${projectRoot}/XY.ts`, `${projectRoot}/Xy.ts`, `./XY`); 211 verifyFileSymlink("when import matches disk but file symlink target does not", `${projectRoot}/XY.ts`, `${projectRoot}/XY.ts`, `./Xy`); 212 verifyFileSymlink("when import and file symlink target agree but do not match disk", `${projectRoot}/XY.ts`, `${projectRoot}/Xy.ts`, `./Xy`); 213 verifyFileSymlink("when import, file symlink target, and disk are all different", `${projectRoot}/XY.ts`, `${projectRoot}/Xy.ts`, `./yX`); 214 215 function verifyDirSymlink(subScenario: string, diskPath: string, targetPath: string, importedPath: string) { 216 verifyTscWatch({ 217 scenario: "forceConsistentCasingInFileNames", 218 subScenario, 219 commandLineArgs: ["--w", "--p", ".", "--explainFiles"], 220 sys: () => { 221 const moduleA: File = { 222 223 path: `${diskPath}/a.ts`, 224 content: ` 225export const a = 1; 226export const b = 2; 227` 228 }; 229 const symlinkA: SymLink = { 230 path: `${projectRoot}/link`, 231 symLink: targetPath, 232 }; 233 const moduleB: File = { 234 path: `${projectRoot}/b.ts`, 235 content: ` 236import { a } from "${importedPath}/a"; 237import { b } from "./link/a"; 238 239a;b; 240` 241 }; 242 const tsconfig: File = { 243 path: `${projectRoot}/tsconfig.json`, 244 // Use outFile because otherwise the real and linked files will have the same output path 245 content: JSON.stringify({ compilerOptions: { forceConsistentCasingInFileNames: true, outFile: "out.js", module: "system" } }) 246 }; 247 return createWatchedSystem([moduleA, symlinkA, moduleB, libFile, tsconfig], { currentDirectory: projectRoot }); 248 }, 249 changes: [ 250 { 251 caption: "Prepend a line to moduleA", 252 change: sys => sys.prependFile(`${diskPath}/a.ts`, `// some comment 253 `), 254 timeouts: runQueuedTimeoutCallbacks, 255 } 256 ], 257 }); 258 } 259 260 verifyDirSymlink("when both directory symlink target and import match disk", `${projectRoot}/XY`, `${projectRoot}/XY`, `./XY`); 261 verifyDirSymlink("when directory symlink target matches disk but import does not", `${projectRoot}/XY`, `${projectRoot}/Xy`, `./XY`); 262 verifyDirSymlink("when import matches disk but directory symlink target does not", `${projectRoot}/XY`, `${projectRoot}/XY`, `./Xy`); 263 verifyDirSymlink("when import and directory symlink target agree but do not match disk", `${projectRoot}/XY`, `${projectRoot}/Xy`, `./Xy`); 264 verifyDirSymlink("when import, directory symlink target, and disk are all different", `${projectRoot}/XY`, `${projectRoot}/Xy`, `./yX`); 265 266 verifyTscWatch({ 267 scenario: "forceConsistentCasingInFileNames", 268 subScenario: "with nodeNext resolution", 269 commandLineArgs: ["--w", "--explainFiles"], 270 sys: () => createWatchedSystem({ 271 "/Users/name/projects/web/src/bin.ts": `import { foo } from "yargs";`, 272 "/Users/name/projects/web/node_modules/@types/yargs/index.d.ts": "export function foo(): void;", 273 "/Users/name/projects/web/node_modules/@types/yargs/index.d.mts": "export function foo(): void;", 274 "/Users/name/projects/web/node_modules/@types/yargs/package.json": JSON.stringify({ 275 name: "yargs", 276 version: "17.0.12", 277 exports: { 278 ".": { 279 types: { 280 import: "./index.d.mts", 281 default: "./index.d.ts" 282 } 283 }, 284 } 285 }), 286 "/Users/name/projects/web/tsconfig.json": JSON.stringify({ 287 compilerOptions: { 288 moduleResolution: "nodenext", 289 forceConsistentCasingInFileNames: true, 290 traceResolution: true, 291 } 292 }), 293 [libFile.path]: libFile.content, 294 }, { currentDirectory: "/Users/name/projects/web" }), 295 changes: emptyArray, 296 }); 297 298 verifyTscWatch({ 299 scenario: "forceConsistentCasingInFileNames", 300 subScenario: "self name package reference", 301 commandLineArgs: ["-w", "--explainFiles"], 302 sys: () => createWatchedSystem({ 303 "/Users/name/projects/web/package.json": JSON.stringify({ 304 name: "@this/package", 305 type: "module", 306 exports: { 307 ".": "./dist/index.js" 308 } 309 }), 310 "/Users/name/projects/web/index.ts": Utils.dedent` 311 import * as me from "@this/package"; 312 me.thing(); 313 export function thing(): void {} 314 `, 315 "/Users/name/projects/web/tsconfig.json": JSON.stringify({ 316 compilerOptions: { 317 module: "nodenext", 318 outDir: "./dist", 319 declarationDir: "./types", 320 composite: true, 321 forceConsistentCasingInFileNames: true, 322 traceResolution: true, 323 } 324 }), 325 "/a/lib/lib.esnext.full.d.ts": libFile.content, 326 }, { currentDirectory: "/Users/name/projects/web" }), 327 changes: emptyArray, 328 }); 329 330 331 verifyTscWatch({ 332 scenario: "forceConsistentCasingInFileNames", 333 subScenario: "package json is looked up for file", 334 commandLineArgs: ["-w", "--explainFiles"], 335 sys: () => createWatchedSystem({ 336 "/Users/name/projects/lib-boilerplate/package.json": JSON.stringify({ 337 name: "lib-boilerplate", 338 version: "0.0.2", 339 type: "module", 340 exports: "./src/index.ts", 341 }), 342 "/Users/name/projects/lib-boilerplate/src/index.ts": Utils.dedent` 343 export function thing(): void {} 344 `, 345 "/Users/name/projects/lib-boilerplate/test/basic.spec.ts": Utils.dedent` 346 import { thing } from 'lib-boilerplate' 347 `, 348 "/Users/name/projects/lib-boilerplate/tsconfig.json": JSON.stringify({ 349 compilerOptions: { 350 module: "node16", 351 target: "es2021", 352 forceConsistentCasingInFileNames: true, 353 traceResolution: true, 354 } 355 }), 356 "/a/lib/lib.es2021.full.d.ts": libFile.content, 357 }, { currentDirectory: "/Users/name/projects/lib-boilerplate" }), 358 changes: emptyArray, 359 }); 360 }); 361} 362