diff --git a/CHANGELOG.md b/CHANGELOG.md index aeb6b97b..d8800c62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -302,6 +302,45 @@ Semver applies from 1.0.0. A breaking change to a documented API needs a major the throws that caused it reached the job retry classifier as bare `Error`s with no code; and restored `localStorage` was written on `about:blank` where it can never reach the site's origin. +- **An apostrophe in JSX text silently disabled the `errors` gate for a whole file, and it was not + hypothetical.** `maskLiterals` treated `'` as a literal opener and blanked everything up to the + next one, so `scanFixes` returned nothing for the rest of the file — while `scanCodes` kept + passing, masking the hole. `packages/http/src/errors.ts` contains + `…already route "${input.otherRoute}"'s`, so **eight real `fix:` lines in that file had never been + checked**. All eight pass now. One test asserted "nothing in the installed framework raises + `X_DRAINING`" — disproved by `draining()` in the very file the gate had stopped reading. + +- **A page with zero executable JavaScript could fail its JS budget**, with a `fix:` line naming an + import that does not exist. Every inline `', dir)).toBe(14); }); + + // The bug this guards: a document that makes the browser execute NOTHING failed its JS budget, + // with a `fix:` telling the author to move an import that does not exist. `@ultimat3/seo` emits + // `application/ld+json` for structured data and `@ultimat3/render` emits `application/json` for + // island props — both are data the parser never runs. + test('a JSON-typed script is data, so a page of only data ships zero JS', async () => { + const document = + '' + + ''; + expect(await measureJsBytes(document, out)).toBe(0); + }); + + // A real document writes the charset: `'; + expect(await measureJsBytes(withCharset, out)).toBe(0); + expect( + await measureJsBytes('', out), + ).toBe(0); + // And the parameter cannot make code data: the suffix test still decides, on the type alone. + expect( + await measureJsBytes('', out), + ).toBe(7); + }); + + test('the rule is the type ending in json, not the one literal type', async () => { + expect(await measureJsBytes('', out)).toBe(0); + expect(await measureJsBytes('', out)).toBe( + 0, + ); + // A module is code, and so is a type nobody declared. + expect(await measureJsBytes('', out)).toBe(7); + expect(await measureJsBytes('', out)).toBe(7); + }); }); describe('unit · writeBuildStats is what makes X_BUDGET_UNMEASURED reachable', () => { diff --git a/packages/cli/src/budgets.ts b/packages/cli/src/budgets.ts index 6a571c93..f0abbac2 100644 --- a/packages/cli/src/budgets.ts +++ b/packages/cli/src/budgets.ts @@ -123,6 +123,23 @@ export async function readBuildStats(root: string): Promise[^>]*)>(?[\s\S]*?)<\/script>/g; const SRC_ATTR = /\ssrc="(?[^"]*)"/; +const TYPE_ATTR = /\stype="(?[^"]*)"/; + +/** + * `application/ld+json`, `application/json`, any `…+json`: the body is data, not code — the rule + * `@ultimat3/render`'s `head.ts` already states, restated because its `carriesJson` reads a + * `HeadTag` and is not exported, and this side has an attribute string off the emitted document. + * Without it a page shipping only `meta.ld` structured data and island props measured 8kb of JS + * and failed a 2kb budget with a `fix:` naming an import chain that does not exist. + */ +const carriesJson = (attrs: string): boolean => { + // Everything from the first `;` is a MIME PARAMETER and not the type: a real document writes + // `type="application/ld+json; charset=utf-8"`, which does not END with `json`, so the suffix + // test alone charged an SEO structured-data block as executable JavaScript all over again. + const [type = ''] = (TYPE_ATTR.exec(attrs)?.groups?.['type'] ?? '').split(';'); + return type.trim().toLowerCase().endsWith('json'); +}; + /** * An island's chunk is reached by `import()` from inside the hydration runtime, so it never appears * as a `