Skip to content

feat: parse only the frontmatter and <script> bodies of .astro files - #1

Merged
matej21 merged 3 commits into
mainfrom
feat/astro-code-sections
Nov 12, 2026
Merged

matej21 merged 3 commits into
mainfrom
feat/astro-code-sections

Conversation

@matej21

@matej21 matej21 commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

.astro files matched by sourcePatterns are now scanned by parsing only their code: the frontmatter and the body of each <script> that Astro bundles. Other files are handled as before, and the default sourcePatterns don't change.

Why

The collector parsed a whole Astro component with the TypeScript parser. The markup is not TypeScript, and error recovery gave wrong results:

  • A <script> import that follows JSX-like markup such as {items.map((i) => <li>{i}</li>)} was missed (a false negative, so a missing dependency went unreported).
  • Import-like text in the markup, such as <code>import D from 'pkg'</code> or call import('pkg'), was reported as an import (a false positive).

Change

src/imports/collector.ts, for a file ending in .astro:

  • Frontmatter: the block between a first non-blank line --- and the next --- line. Without a closing fence, it runs to the end of the file, as in Astro.
  • Scripts: the body of each lowercase <script> in the rest of the file. Comments and scripts are found in one scan, so a <!-- inside a script stays script text, and a script inside a comment is skipped. A self-closing <script /> has no body, <script-foo> is another element, and the closing tag matches in any case.
  • Attributes: only a body of a <script> without attributes is parsed. With src, Astro bundles the file and drops the body; any other attribute (type="module", is:inline, define:vars, an uppercase SRC, …) makes the script inline, so its imports are not package dependencies.
  • Components: <Script> is a component in Astro, not a script.
  • Parsing: every section is parsed on its own as TypeScript. The same visitor collects static, type-only and dynamic imports and require calls.

README.md: the sourcePatterns row says how to include .astro files and what gets parsed.

Tests

New tests in src/__tests__/imports.test.ts, under collectImports in .astro files:

  • frontmatter static, type-only and dynamic imports
  • a <script> import after JSX-like markup, a backtick and /* in text
  • every bundled <script>, including <script > and a src-only script
  • inline scripts (type="module", is:inline, define:vars) are ignored
  • the body of a src script and of an uppercase SRC script is ignored
  • a self-closing <script /> does not swallow the markup after it
  • <script-foo> is another element; </SCRIPT> closes a script
  • a <!-- inside a script does not hide the scripts after it
  • a <script> inside an HTML comment is ignored
  • <Script> is a component
  • frontmatter without a closing fence
  • a BOM, leading blank lines and CRLF line ends
  • import-like text in the markup is ignored
  • markup without frontmatter or scripts has no imports

Against 0.3.0's collector, 9 of these tests fail. Against bd54b62, 7 fail: the four cases from review round 1 (inline scripts, <!-- in a script, <Script>, open frontmatter) and the three from round 2. Against be576cc, the three from round 2 fail. All pass with the change.

Checks

  • bun run typecheck: passes
  • bun test: 171 pass, 0 fail
  • Run against a monorepo that scans its published .astro files (sourcePatterns: ['src/**/*.{ts,tsx,astro}']): no issues, the same result as with 0.3.0

Not covered

The scan is line- and tag-based, not a real Astro parser, which would mean depending on @astrojs/compiler (WebAssembly). Cases it gets wrong, all rare:

  • A --- line inside a template literal in the frontmatter closes the frontmatter early.
  • A fence line that carries code (--- // comment), or text before the opening fence: the frontmatter is not found.
  • <script> text inside <textarea> or <style> content, or inside a JS string in the markup, is read as a script.
  • A script inside <svg>, <template>, <noscript> or an is:raw element is collected; Astro does not bundle it.
  • A <!-- in an attribute value or an expression string hides the scripts up to the next -->.

Decisions

decision where status
"a PR in our deptective repo (public): parse only the frontmatter and <script> contents of .astro files" bd54b62, be576cc, 6af5b90, src/imports/collector.ts done; scripts Astro does not bundle (inline) are left out too
"with tests for a <script> import after JSX-like markup and for import-like text in markup" bd54b62, be576cc, 6af5b90, src/__tests__/imports.test.ts done, plus the cases listed above

Status

  • Review: round 1 changes, fixed in be576cc; round 2 changes, fixed in 6af5b90; round 3 approve on 6af5b90
  • CI: green on 6af5b90
  • Merged in ebb53ea. Release is a separate step (tag push)

🤖 Generated with Claude Code

matej21 and others added 2 commits November 12, 2026 13:19
Parsing a whole Astro component as TypeScript misreads its markup: a
<script> import after JSX-like markup such as {items.map((i) => <li>{i}</li>)}
was missed, and import-like text in the markup was reported as an import.
Each code section is now parsed on its own, and HTML comments are skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Script>

- A <script> with any attribute besides src is inline in Astro and is not
  bundled, so its imports are not dependencies.
- Comments and scripts are found in one scan, so a <!-- inside a script no
  longer hides the scripts after it.
- Frontmatter without a closing fence runs to the end of the file.
- Only lowercase <script> counts; <Script> is a component.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@matej21

matej21 commented Nov 12, 2026

Copy link
Copy Markdown
Member Author

Review round 1

Reviewer: Claude Opus 5.5, an independent general-purpose agent that checked each case against @astrojs/compiler 4.0.0 parse()
Commit: bd54b62
Verdict: changes

# severity finding
1 medium Comments were stripped from the whole template before scripts were matched, so a <!-- inside a script (const s = '<!--') swallowed the following scripts: their imports went unreported.
2 minor Astro treats a <script> with any attribute besides src as inline and does not bundle it. The PR collected its imports, and a test asserted that.
3 minor A --- line inside a template literal in the frontmatter closes it early. Not listed under "Not covered".
4 nit Frontmatter variants: no closing fence (Astro reads to the end of the file), fence lines with code, text before the fence.
5 nit <script> text in <textarea>/<style> content, and uppercase <SCRIPT> (a component in Astro), were collected.
6 nit No test for BOM, CRLF or leading blank lines (createPkg trims the content).
7 nit A local filesystem path in the description.

Checked and fine: BOM, CRLF, trailing spaces on fences, empty frontmatter, <script> text in the frontmatter, </script >, a script inside <svg>, <style> and JSON-LD not leaking, non-.astro files unchanged; exactly the four claimed tests fail on 0.3.0.

@matej21

matej21 commented Nov 12, 2026

Copy link
Copy Markdown
Member Author

Response to review round 1

# outcome
1 fixed in be576cc: comments and scripts are found in one scan. Regression test added.
2 fixed in be576cc: scripts with any attribute besides src are skipped; the test now expects that.
3 won't fix: listed under "Not covered". It needs a string-aware scan or the Astro compiler.
4 partly fixed in be576cc: frontmatter without a closing fence now runs to the end of the file. Fence lines with code and text before the fence are listed under "Not covered".
5 fixed in be576cc for <Script>/<SCRIPT> (lowercase <script> only). <textarea>/<style> content is listed under "Not covered".
6 fixed in be576cc: a test writes a file with a BOM, leading blank lines and CRLF line ends.
7 fixed in the description.

… script tags

A <script src> bundles its file and drops the body, and an uppercase SRC is
inline, so a body counts only when the tag has no attributes. A self-closing
<script /> no longer runs to the next </script>, <script-foo> is no longer a
script, and the closing tag matches in any case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@matej21

matej21 commented Nov 12, 2026

Copy link
Copy Markdown
Member Author

Review round 2 — Claude Opus 5.5 (independent general-purpose agent), commit be576cc. Verdict: changes.

Ground truth this round: @astrojs/compiler 4.0.0 transform().scripts (the scripts Astro actually bundles), compared against the PR's output on 84 cases.

  1. medium — src/imports/collector.ts:96: a self-closing <script src="./a.ts" /> ran to the next </script>, so the markup between was read as a script body (missed imports, and import-like text reported). Already in bd54b62.
  2. minor — collector.ts:98,104-107: a <script src> body was collected, but Astro bundles the file and drops the body; an uppercase SRC was treated as src, but Astro matches attribute names case-sensitively and treats it as inline.
  3. nit — collector.ts:96: \b matched <script-foo> and <script:foo>.
  4. nit — collector.ts:96: the closing tag was case-sensitive (</SCRIPT>), a regression from bd54b62.
  5. minor — description, "Not covered": missing scripts inside <svg>/<template>/<noscript>/is:raw, and a <!-- in an attribute value or expression string.
  6. nit — description, Tests: the count of tests failing on 0.3.0 was stale.
  7. nit — README.md:158: "<script> bodies" should say which scripts.

Checked and fine: inline detection for is:inline, type, define:vars, lang, async, defer, data-*, spread and shorthand; quoted and unquoted src values; one-scan comments; open frontmatter; <Script>/<SCRIPT>; the bd54b62 failure claim; 168 tests and typecheck.

@matej21

matej21 commented Nov 12, 2026

Copy link
Copy Markdown
Member Author

Response to review round 2

  1. Self-closing <script />: fixed in 6af5b90 (the open tag is quote-aware and accepts />); test "a self-closing <script /> does not swallow the markup after it".
  2. src body and uppercase SRC: fixed in 6af5b90 — isBundledScript is gone; a body is parsed only when the tag has no attributes. Test "a src script bundles its file, not its body, and an uppercase SRC is inline".
  3. <script-foo>: fixed in 6af5b90 ((?=[\s/>]) instead of \b); test " is another element, and a closing tag matches in any case".
  4. </SCRIPT>: fixed in 6af5b90; same test.
  5. Not covered: fixed in the description (both cases added).
  6. Stale count: fixed in the description (9 of the tests fail on 0.3.0; the three new ones fail on be576cc).
  7. README: fixed in 6af5b90.

bun test: 171 pass, 0 fail. bun run typecheck: passes. Findings were medium and below, all fixed, so no round 3.

@matej21

matej21 commented Nov 12, 2026

Copy link
Copy Markdown
Member Author

Review round 3 — Claude Opus 5.5 (the round 2 reviewer, resumed), commit 6af5b90. Verdict: approve.

  • Findings 1–4 from round 2 are fixed. Every round 2 case for them now matches @astrojs/compiler 4.0.0 transform(): self-closing <script /> with or without a space, a src, SRC, src="" or bare src with a body, <script-foo>, <script:foo>, and </SCRIPT>.
  • Everything that matched in round 2 still matches. The remaining differences are exactly the "Not covered" items, plus an unterminated <!--, which Astro rejects as an error.
  • The 3 new tests each catch their fix: they fail on be576cc. 9 tests fail on v0.3.0 and 7 on bd54b62.
  • bun test: 171 pass. bun run typecheck: clean. The README row and the description match the code.
  • nit: the description's "Against bd54b62" sentence should give the full count of 7.

@matej21

matej21 commented Nov 12, 2026

Copy link
Copy Markdown
Member Author

Response to review round 3

  • nit (bd54b62 count): fixed in the description. It now says 7 fail on bd54b62.

Correction to the round 2 response: it said "no round 3". Finding 1 was medium, so round 3 was run, and it approved 6af5b90.

@matej21
matej21 merged commit ebb53ea into main Nov 12, 2026
1 check passed
@matej21
matej21 deleted the feat/astro-code-sections branch November 12, 2026 12:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant