From b786714b98641ff630967edbce5699f87ab85014 Mon Sep 17 00:00:00 2001 From: Edward Faulkner Date: Tue, 30 Apr 2024 10:47:56 -0400 Subject: [PATCH] adding new http-audit test support --- packages/compat/src/audit/babel-visitor.ts | 5 +- packages/compat/src/http-audit.ts | 51 ++++++++++++++++++ test-packages/support/audit-assertions.ts | 62 +++++++++++++++------- 3 files changed, 99 insertions(+), 19 deletions(-) create mode 100644 packages/compat/src/http-audit.ts diff --git a/packages/compat/src/audit/babel-visitor.ts b/packages/compat/src/audit/babel-visitor.ts index 43a72449e..f953ada7d 100644 --- a/packages/compat/src/audit/babel-visitor.ts +++ b/packages/compat/src/audit/babel-visitor.ts @@ -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(); let problems = [] as { message: string; detail: string; codeFrameIndex: number | undefined }[]; diff --git a/packages/compat/src/http-audit.ts b/packages/compat/src/http-audit.ts new file mode 100644 index 000000000..a16d25452 --- /dev/null +++ b/packages/compat/src/http-audit.ts @@ -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 { + 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, + }; +} diff --git a/test-packages/support/audit-assertions.ts b/test-packages/support/audit-assertions.ts index af421e07f..f79fb825d 100644 --- a/test-packages/support/audit-assertions.ts +++ b/test-packages/support/audit-assertions.ts @@ -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'; @@ -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 { @@ -40,7 +54,7 @@ export function setupAuditTest(hooks: NestedHooks, opts: () => AuditBuildOptions } async function audit(this: Assert, opts: AuditBuildOptions): Promise { - 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) { @@ -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 { return new ExpectModule(this, inputName); @@ -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; @@ -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, @@ -160,7 +174,7 @@ export class ExpectModule { this.expectAudit.assert.codeContains(this.module.content, expectedSource); } - resolves(specifier: string): PublicAPI { + resolves(specifier: string | RegExp): PublicAPI { if (!this.module) { this.emitMissingModule(); return new EmptyExpectResolution(); @@ -171,7 +185,19 @@ 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}`, @@ -179,8 +205,8 @@ export class ExpectModule { }); 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}`,