Skip to content

Commit

Permalink
Merge pull request #1895 from embroider-build/http-audit
Browse files Browse the repository at this point in the history
adding new http-audit test support
  • Loading branch information
ef4 authored May 2, 2024
2 parents 9b46cf4 + c90b00d commit beba8d9
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 19 deletions.
5 changes: 4 additions & 1 deletion packages/compat/src/audit/babel-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ export interface ExportAll {
all: string;
}

// babelConfig must include { ast: true }
export function auditJS(rawSource: string, filename: string, babelConfig: TransformOptions, frames: CodeFrameStorage) {
if (!babelConfig.ast) {
throw new Error(`module auditing requires a babel config with ast: true`);
}

let imports = [] as InternalImport[];
let exports = new Set<string | ExportAll>();
let problems = [] as { message: string; detail: string; codeFrameIndex: number | undefined }[];
Expand Down
51 changes: 51 additions & 0 deletions packages/compat/src/http-audit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { Finding } from './audit';
import { CodeFrameStorage } from './audit/babel-visitor';
import { type Module, visitModules, type ContentType } from './module-visitor';

export interface HTTPAuditOptions {
appURL: string;
startingFrom: string[];
fetch?: typeof fetch;
}

export async function httpAudit(
options: HTTPAuditOptions
): Promise<{ modules: { [file: string]: Module }; findings: Finding[] }> {
let findings: Finding[] = [];

async function resolveId(specifier: string, fromFile: string): Promise<string | undefined> {
return new URL(specifier, fromFile).href;
}

async function load(id: string): Promise<{ content: string | Buffer; type: ContentType }> {
let response = await (options.fetch ?? globalThis.fetch)(id);
let content = await response.text();
let type: ContentType;
switch (response.headers.get('content-type')) {
case 'text/javascript':
type = 'javascript';
break;
case 'text/html':
type = 'html';
break;
default:
throw new Error(`oops content type ${response.headers.get('content-type')}`);
}
return { content, type };
}

let modules = await visitModules({
base: options.appURL,
entrypoints: options.startingFrom.map(s => new URL(s, options.appURL).href),
babelConfig: { ast: true },
frames: new CodeFrameStorage(),
findings,
resolveId,
load,
});

return {
modules,
findings,
};
}
62 changes: 44 additions & 18 deletions test-packages/support/audit-assertions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { AuditBuildOptions, AuditResults, Module } from '../../packages/compat/src/audit';
import type { AuditBuildOptions, Finding, Module } from '../../packages/compat/src/audit';
import { httpAudit, type HTTPAuditOptions } from '../../packages/compat/src/http-audit';
import type { Import } from '../../packages/compat/src/module-visitor';
import { Audit } from '../../packages/compat/src/audit';
import { explicitRelative } from '../../packages/shared-internals';
import { install as installCodeEqualityAssertions } from 'code-equality-assertions/qunit';
Expand All @@ -13,17 +15,29 @@ import { getRewrittenLocation } from './rewritten-path';
take advantage of the audit tool within our test suite to help us analyze
Embroider's output.
*/
export function setupAuditTest(hooks: NestedHooks, opts: () => AuditBuildOptions) {
let result: AuditResults;
export function setupAuditTest(hooks: NestedHooks, opts: () => AuditBuildOptions | HTTPAuditOptions) {
let result: { modules: { [file: string]: Module }; findings: Finding[] };
let expectAudit: ExpectAuditResults;

hooks.before(async () => {
result = await Audit.run(opts());
let o = opts();
if ('appURL' in o) {
result = await httpAudit(o);
} else {
result = await Audit.run(o);
}
});

hooks.beforeEach(assert => {
installAuditAssertions(assert);
expectAudit = new ExpectAuditResults(result, assert, opts().app);
let o = opts();
let pathRewriter: (p: string) => string;
if ('appURL' in o) {
pathRewriter = p => p;
} else {
pathRewriter = p => getRewrittenLocation(o.app, p);
}
expectAudit = new ExpectAuditResults(result, assert, pathRewriter);
});

return {
Expand All @@ -40,7 +54,7 @@ export function setupAuditTest(hooks: NestedHooks, opts: () => AuditBuildOptions
}

async function audit(this: Assert, opts: AuditBuildOptions): Promise<ExpectAuditResults> {
return new ExpectAuditResults(await Audit.run(opts), this, opts.app);
return new ExpectAuditResults(await Audit.run(opts), this, p => getRewrittenLocation(opts.app, p));
}

export function installAuditAssertions(assert: Assert) {
Expand All @@ -55,12 +69,12 @@ declare global {
}

export class ExpectAuditResults {
constructor(readonly result: AuditResults, readonly assert: Assert, private appDir: string) {}

// input and output paths are relative to getAppDir()
toRewrittenPath = (path: string) => {
return getRewrittenLocation(this.appDir, path);
};
constructor(
readonly result: { modules: { [file: string]: Module }; findings: Finding[] },
readonly assert: Assert,
// input and output paths are relative to getAppDir()
readonly toRewrittenPath: (path: string) => string
) {}

module(inputName: string): PublicAPI<ExpectModule> {
return new ExpectModule(this, inputName);
Expand Down Expand Up @@ -109,7 +123,7 @@ export class ExpectModule {
});
}

withContents(fn: (src: string) => boolean, message?: string) {
withContents(fn: (src: string, imports: Import[]) => boolean, message?: string) {
if (!this.module) {
this.emitMissingModule();
return;
Expand All @@ -118,7 +132,7 @@ export class ExpectModule {
this.emitUnparsableModule(message);
return;
}
const result = fn(this.module.content);
const result = fn(this.module.content, this.module.imports);
this.expectAudit.assert.pushResult({
result,
actual: result,
Expand Down Expand Up @@ -160,7 +174,7 @@ export class ExpectModule {
this.expectAudit.assert.codeContains(this.module.content, expectedSource);
}

resolves(specifier: string): PublicAPI<ExpectResolution> {
resolves(specifier: string | RegExp): PublicAPI<ExpectResolution> {
if (!this.module) {
this.emitMissingModule();
return new EmptyExpectResolution();
Expand All @@ -171,16 +185,28 @@ export class ExpectModule {
return new EmptyExpectResolution();
}

if (!(specifier in this.module.resolutions)) {
let resolution: string | undefined | null;
if (typeof specifier === 'string') {
resolution = this.module.resolutions[specifier];
} else {
for (let [source, module] of Object.entries(this.module.resolutions)) {
if (specifier.test(source)) {
resolution = module;
break;
}
}
}

if (resolution === undefined) {
this.expectAudit.assert.pushResult({
result: false,
expected: `${this.module.appRelativePath} does not refer to ${specifier}`,
actual: Object.keys(this.module.resolutions),
});
return new EmptyExpectResolution();
}
let resolution = this.module.resolutions[specifier];
if (!resolution) {

if (resolution === null) {
this.expectAudit.assert.pushResult({
result: false,
expected: `${specifier} fails to resolve in ${this.module.appRelativePath}`,
Expand Down

0 comments on commit beba8d9

Please sign in to comment.