main
ts 188 lines 5.41 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 import fs from 'fs/promises';
9 import * as glob from 'glob';
10 import path from 'path';
11 import {FIXTURES_PATH, SNAPSHOT_EXTENSION} from './constants';
12
13 const INPUT_EXTENSIONS = [
14 '.js',
15 '.cjs',
16 '.mjs',
17 '.ts',
18 '.cts',
19 '.mts',
20 '.jsx',
21 '.tsx',
22 ];
23
24 export type TestFilter = {
25 paths: Array<string>;
26 };
27
28 function stripExtension(filename: string, extensions: Array<string>): string {
29 for (const ext of extensions) {
30 if (filename.endsWith(ext)) {
31 return filename.slice(0, -ext.length);
32 }
33 }
34 return filename;
35 }
36
37 export function getBasename(fixture: TestFixture): string {
38 return stripExtension(path.basename(fixture.inputPath), INPUT_EXTENSIONS);
39 }
40 export function isExpectError(fixture: TestFixture | string): boolean {
41 const basename = typeof fixture === 'string' ? fixture : getBasename(fixture);
42 return basename.startsWith('error.') || basename.startsWith('todo.error');
43 }
44
45 export type TestFixture =
46 | {
47 fixturePath: string;
48 input: string | null;
49 inputPath: string;
50 snapshot: string | null;
51 snapshotPath: string;
52 }
53 | {
54 fixturePath: string;
55 input: null;
56 inputPath: string;
57 snapshot: string;
58 snapshotPath: string;
59 };
60
61 async function readInputFixtures(
62 rootDir: string,
63 filter: TestFilter | null,
64 ): Promise<Map<string, {value: string; filepath: string}>> {
65 let inputFiles: Array<string>;
66 if (filter == null) {
67 inputFiles = glob.sync(`**/*{${INPUT_EXTENSIONS.join(',')}}`, {
68 cwd: rootDir,
69 });
70 } else {
71 inputFiles = (
72 await Promise.all(
73 filter.paths.map(pattern => {
74 // If the pattern already has an extension other than .expect.md,
75 // search for the pattern directly. Otherwise, search for the
76 // pattern with the expected input extensions added.
77 // Eg
78 // `alias-while` => search for `alias-while{.js,.jsx,.ts,.tsx}`
79 // `alias-while.js` => search as-is
80 // `alias-while.expect.md` => search for `alias-while{.js,.jsx,.ts,.tsx}`
81 const patternWithoutExt = stripExtension(pattern, [
82 ...INPUT_EXTENSIONS,
83 SNAPSHOT_EXTENSION,
84 ]);
85 const hasExtension = pattern !== patternWithoutExt;
86 const globPattern =
87 hasExtension && !pattern.endsWith(SNAPSHOT_EXTENSION)
88 ? pattern
89 : `${patternWithoutExt}{${INPUT_EXTENSIONS.join(',')}}`;
90 return glob.glob(globPattern, {
91 cwd: rootDir,
92 });
93 }),
94 )
95 ).flat();
96 }
97 const inputs: Array<Promise<[string, {value: string; filepath: string}]>> =
98 [];
99 for (const filePath of inputFiles) {
100 // Do not include extensions in unique identifier for fixture
101 const partialPath = stripExtension(filePath, INPUT_EXTENSIONS);
102 inputs.push(
103 fs.readFile(path.join(rootDir, filePath), 'utf8').then(input => {
104 return [
105 partialPath,
106 {
107 value: input,
108 filepath: filePath,
109 },
110 ];
111 }),
112 );
113 }
114 return new Map(await Promise.all(inputs));
115 }
116 async function readOutputFixtures(
117 rootDir: string,
118 filter: TestFilter | null,
119 ): Promise<Map<string, string>> {
120 let outputFiles: Array<string>;
121 if (filter == null) {
122 outputFiles = glob.sync(`**/*${SNAPSHOT_EXTENSION}`, {
123 cwd: rootDir,
124 });
125 } else {
126 outputFiles = (
127 await Promise.all(
128 filter.paths.map(pattern => {
129 // Strip all extensions and find matching .expect.md files
130 const basenameWithoutExt = stripExtension(pattern, [
131 ...INPUT_EXTENSIONS,
132 SNAPSHOT_EXTENSION,
133 ]);
134 return glob.glob(`${basenameWithoutExt}${SNAPSHOT_EXTENSION}`, {
135 cwd: rootDir,
136 });
137 }),
138 )
139 ).flat();
140 }
141 const outputs: Array<Promise<[string, string]>> = [];
142 for (const filePath of outputFiles) {
143 // Do not include extensions in unique identifier for fixture
144 const partialPath = stripExtension(filePath, [SNAPSHOT_EXTENSION]);
145
146 const outputPath = path.join(rootDir, filePath);
147 const output: Promise<[string, string]> = fs
148 .readFile(outputPath, 'utf8')
149 .then(output => {
150 return [partialPath, output];
151 });
152 outputs.push(output);
153 }
154 return new Map(await Promise.all(outputs));
155 }
156
157 export async function getFixtures(
158 filter: TestFilter | null,
159 ): Promise<Map<string, TestFixture>> {
160 const inputs = await readInputFixtures(FIXTURES_PATH, filter);
161 const outputs = await readOutputFixtures(FIXTURES_PATH, filter);
162
163 const fixtures: Map<string, TestFixture> = new Map();
164 for (const [partialPath, {value, filepath}] of inputs) {
165 const output = outputs.get(partialPath) ?? null;
166 fixtures.set(partialPath, {
167 fixturePath: partialPath,
168 input: value,
169 inputPath: filepath,
170 snapshot: output,
171 snapshotPath: path.join(FIXTURES_PATH, partialPath) + SNAPSHOT_EXTENSION,
172 });
173 }
174
175 for (const [partialPath, output] of outputs) {
176 if (!fixtures.has(partialPath)) {
177 fixtures.set(partialPath, {
178 fixturePath: partialPath,
179 input: null,
180 inputPath: 'none',
181 snapshot: output,
182 snapshotPath:
183 path.join(FIXTURES_PATH, partialPath) + SNAPSHOT_EXTENSION,
184 });
185 }
186 }
187 return fixtures;
188 }