• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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