Skip to content

Commit

Permalink
Use Babel
Browse files Browse the repository at this point in the history
  • Loading branch information
rmarescu committed Feb 28, 2025
1 parent 6f5a3ff commit 1fbc71f
Show file tree
Hide file tree
Showing 8 changed files with 369 additions and 98 deletions.
12 changes: 7 additions & 5 deletions packages/shortest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,20 @@
"prepublishOnly": "pnpm build",
"postinstall": "node -e \"if (process.platform !== 'win32') { try { require('child_process').execSync('chmod +x dist/cli/bin.js') } catch (_) {} }\"",
"build:types": "tsup src/index.ts --dts-only --format esm --outDir dist",
"build:js": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:esbuild --external:punycode --external:playwright --external:expect --external:dotenv --external:ai --external:@ai-sdk/*",
"build:cjs": "esbuild src/index.ts --bundle --platform=node --format=cjs --outfile=dist/index.cjs --external:esbuild --external:punycode --external:playwright --external:expect --external:dotenv --external:ai --external:@ai-sdk/*",
"build:cli": "esbuild src/cli/bin.ts --bundle --platform=node --format=esm --outdir=dist/cli --metafile=dist/meta-cli.json --external:fsevents --external:chokidar --external:glob --external:esbuild --external:events --external:path --external:fs --external:util --external:stream --external:os --external:assert --external:url --external:playwright --external:expect --external:dotenv --external:otplib --external:picocolors --external:punycode --external:https --external:http --external:net --external:tls --external:crypto --external:mailosaur --external:ai --external:@ai-sdk/*",
"build:js": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:esbuild --external:punycode --external:playwright --external:expect --external:dotenv --external:ai --external:@ai-sdk/* --external:@babel/* --external:tty",
"build:cjs": "esbuild src/index.ts --bundle --platform=node --format=cjs --outfile=dist/index.cjs --external:esbuild --external:punycode --external:playwright --external:expect --external:dotenv --external:ai --external:@ai-sdk/* --external:@babel/* --external:tty",
"build:cli": "esbuild src/cli/bin.ts --bundle --platform=node --format=esm --outdir=dist/cli --metafile=dist/meta-cli.json --external:fsevents --external:chokidar --external:glob --external:esbuild --external:events --external:path --external:fs --external:util --external:stream --external:os --external:assert --external:url --external:playwright --external:expect --external:dotenv --external:otplib --external:picocolors --external:punycode --external:https --external:http --external:net --external:tls --external:crypto --external:mailosaur --external:ai --external:@ai-sdk/* --external:@babel/* --external:tty --external:debug",
"dev": "pnpm build --watch",
"test:unit": "npx vitest run",
"test:unit:watch": "npx vitest --watch",
"test:e2e": "node --import tsx --test tests/e2e/index.ts",
"cache:clear": "pnpm build && shortest cache clear --force-purge"
},
"dependencies": {
"acorn": "^8.14.0",
"acorn-walk": "^8.3.4",
"@babel/parser": "^7.26.9",
"@babel/traverse": "^7.26.9",
"@babel/types": "^7.26.9",
"@types/babel__traverse": "^7.20.6",
"chromium-bidi": "^0.5.24",
"glob": "^10.4.5",
"otplib": "^12.0.1",
Expand Down
105 changes: 34 additions & 71 deletions packages/shortest/src/core/runner/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import { readFileSync } from "fs";
import { pathToFileURL } from "url";
import { parse } from "acorn";
import { simple as walkSimple } from "acorn-walk";
import { glob } from "glob";
import { APIRequest, BrowserContext } from "playwright";
import * as playwright from "playwright";
Expand All @@ -12,6 +9,10 @@ import { BrowserTool } from "@/browser/core/browser-tool";
import { BrowserManager } from "@/browser/manager";
import { TestCache } from "@/cache";
import { TestCompiler } from "@/core/compiler";
import {
EXPRESSION_PLACEHOLDER,
parseShortestTestFile,
} from "@/core/runner/test-file-parser";
import { TestReporter } from "@/core/runner/test-reporter";
import { getLogger, Log } from "@/log";
import {
Expand Down Expand Up @@ -284,83 +285,44 @@ export class TestRunner {
};
}

private filterTestsByLineNumber(
private async filterTestsByLineNumber(
tests: TestFunction[],
file: string,
lineNumber: number,
): TestFunction[] {
const fileContent = readFileSync(file, "utf8");
const ast = parse(fileContent, {
sourceType: "module",
ecmaVersion: "latest",
locations: true,
});
): Promise<TestFunction[]> {
const testLocations = parseShortestTestFile(file);
const escapeRegex = (str: string) =>
str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");

const testLocations: {
[testName: string]: { start: number; end: number };
} = {};
const filteredTests = tests.filter((test) => {
const testNameNormalized = test.name.trim();
let testLocation = testLocations.find(
(location) => location.testName === testNameNormalized,
);

walkSimple(ast, {
CallExpression(node: any) {
if (
node.callee.type === "Identifier" &&
node.callee.name === "shortest"
) {
const testNameArg = node.arguments[0];
if (
!testNameArg ||
testNameArg.type !== "Literal" ||
typeof testNameArg.value !== "string"
) {
return;
}
if (!testLocation) {
testLocation = testLocations.find((location) => {
const TEMP_TOKEN = "##PLACEHOLDER##";
let pattern = location.testName.replace(
new RegExp(escapeRegex(EXPRESSION_PLACEHOLDER), "g"),
TEMP_TOKEN,
);

const testName = testNameArg.value;

// Find the largest chain containing this shortest() call
let largestChain = {
start: node.loc?.start.line,
end: node.loc?.end.line,
};

walkSimple(ast, {
CallExpression(chainNode: any) {
if (
chainNode.loc.start.line === node.loc.start.line && // Same starting line as shortest()
chainNode.callee.type === "MemberExpression" &&
chainNode.callee.property.name === "expect"
) {
// Update end line if this chain node ends later
if (chainNode.loc.end.line > largestChain.end!) {
largestChain.end = chainNode.loc.end.line;
}
}
},
});

if (
largestChain.start !== undefined &&
largestChain.end !== undefined
) {
testLocations[testName] = largestChain;
}
}
},
});
pattern = escapeRegex(pattern);
pattern = pattern.replace(new RegExp(TEMP_TOKEN, "g"), ".*");
const regex = new RegExp(`^${pattern}$`);

const filteredTests = tests.filter((test) => {
const location = testLocations[test.name];
if (!location) {
return false;
return regex.test(testNameNormalized);
});
}
const isInRange =
lineNumber >= location.start && lineNumber <= location.end;
if (isInRange) {
this.log.trace(
`Test found for line number ${lineNumber}: ${test.name}`,
);

if (!testLocation) {
return false;
}

const isInRange =
lineNumber >= testLocation.startLine &&
lineNumber <= testLocation.endLine;
return isInRange;
});

Expand All @@ -376,12 +338,13 @@ export class TestRunner {

const filePathWithoutCwd = file.replace(this.cwd + "/", "");
const compiledPath = await this.compiler.compileFile(file);

this.log.trace("Importing compiled file", { compiledPath });
await import(pathToFileURL(compiledPath).href);
let testsToRun = registry.currentFileTests;

if (lineNumber) {
testsToRun = this.filterTestsByLineNumber(
testsToRun = await this.filterTestsByLineNumber(
registry.currentFileTests,
file,
lineNumber,
Expand Down
167 changes: 167 additions & 0 deletions packages/shortest/src/core/runner/test-file-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { readFileSync } from "fs";
import { createRequire } from "module";
import type { NodePath } from "@babel/traverse";
import type * as t from "@babel/types";
import { z } from "zod";
import { getLogger } from "@/log";

export const EXPRESSION_PLACEHOLDER = "${...}";

export const TestLocationSchema = z.object({
testName: z.string(),
startLine: z.number().int().positive(),
endLine: z.number().int().positive(),
});
export type TestLocation = z.infer<typeof TestLocationSchema>;

const TestLocationsSchema = z.array(TestLocationSchema);

export const parseShortestTestFile = (filePath: string): TestLocation[] => {
const log = getLogger();
try {
log.setGroup("File Parser");
const require = createRequire(import.meta.url);

const TemplateElementSchema = z.object({
value: z.object({
cooked: z.string().optional(),
raw: z.string().optional(),
}),
});
type TemplateElement = z.infer<typeof TemplateElementSchema>;

const StringLiteralSchema = z.object({
type: z.literal("StringLiteral"),
value: z.string(),
});

const TemplateLiteralSchema = z.object({
type: z.literal("TemplateLiteral"),
quasis: z.array(TemplateElementSchema),
});

const babelTypes = require("@babel/types");
const babelParser = require("@babel/parser");
const traverseModule = require("@babel/traverse");
const babelTraverse = traverseModule.default || traverseModule;

const fileContent = readFileSync(filePath, "utf8");
const ast = babelParser.parse(fileContent, {
sourceType: "module",
plugins: [
"typescript",
"objectRestSpread",
"optionalChaining",
"nullishCoalescingOperator",
],
});

const testLocations: TestLocation[] = [];

const testCallsByLine = new Map<
number,
{ name: string; node: NodePath<t.CallExpression> }
>();

babelTraverse(ast, {
CallExpression(path: NodePath<t.CallExpression>) {
const node = path.node;

if (
!node.type ||
node.type !== "CallExpression" ||
!node.callee ||
node.callee.type !== "Identifier" ||
node.callee.name !== "shortest"
) {
return;
}

const args = node.arguments || [];
if (args.length === 0) return;

const firstArg = args[0];
let testName = "";

if (babelTypes.isStringLiteral(firstArg)) {
const parsed = StringLiteralSchema.parse(firstArg);
testName = parsed.value;
} else if (babelTypes.isTemplateLiteral(firstArg)) {
const parsed = TemplateLiteralSchema.parse(firstArg);
testName = parsed.quasis
.map(
(quasi: TemplateElement, i: number, arr: TemplateElement[]) => {
const str = quasi.value.cooked || quasi.value.raw || "";
return i < arr.length - 1 ? str + EXPRESSION_PLACEHOLDER : str;
},
)
.join("")
.replace(/\s+/g, " ")
.trim();
} else {
return;
}

const startLine = node.loc?.start?.line || 0;
testCallsByLine.set(startLine, {
name: testName,
node: path,
});
},
});

const sortedStartLines = Array.from(testCallsByLine.keys()).sort(
(a, b) => a - b,
);

for (let i = 0; i < sortedStartLines.length; i++) {
const currentLine = sortedStartLines[i];
const nextLine = sortedStartLines[i + 1] || Number.MAX_SAFE_INTEGER;
const { name, node } = testCallsByLine.get(currentLine)!;

let path = node;
let endLine = path.node.loc?.end?.line || 0;

let currentPath: NodePath<t.Node> = path;
while (
currentPath.parentPath &&
(currentPath.parentPath.node.loc?.end?.line ?? 0) < nextLine
) {
const parentType = currentPath.parentPath.node.type;

if (parentType === "ExpressionStatement") {
endLine = currentPath.parentPath.node.loc?.end?.line || endLine;
break;
}

if (
parentType === "CallExpression" ||
parentType === "MemberExpression"
) {
currentPath = currentPath.parentPath;
endLine = Math.max(endLine, currentPath.node.loc?.end?.line || 0);
} else {
endLine = Math.max(
endLine,
currentPath.parentPath.node.loc?.end?.line || endLine,
);
break;
}
}
endLine = Math.min(endLine, nextLine - 1);

const testLocation = TestLocationSchema.parse({
testName: name,
startLine: currentLine,
endLine,
});
testLocations.push(testLocation);
}

log.trace("Test locations", { filePath, testLocations });

return TestLocationsSchema.parse(testLocations);
} finally {
log.resetGroup();
}
};
5 changes: 3 additions & 2 deletions packages/shortest/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,12 @@ const createTestChain = (
): TestChain => {
const registry = global.__shortest__.registry;

const normalizeName = (name: string) => name.replace(/\s+/g, " ").trim();
// Handle array of test names
if (Array.isArray(nameOrFn)) {
const tests = nameOrFn.map((name) => {
const test: TestFunction = {
name,
name: normalizeName(name),
filePath: "",
expectations: [],
};
Expand Down Expand Up @@ -179,7 +180,7 @@ const createTestChain = (

// Rest of existing createTestChain implementation...
const test: TestFunction = {
name: nameOrFn,
name: normalizeName(nameOrFn),
filePath: "",
payload: typeof payloadOrFn === "function" ? undefined : payloadOrFn,
fn: typeof payloadOrFn === "function" ? payloadOrFn : fn,
Expand Down
Empty file.
Loading

0 comments on commit 1fbc71f

Please sign in to comment.