Skip to content

Modernize the plugin and strengthen language-service compatibility - #33

Merged
quantizor merged 5 commits into
styled-components:mainfrom
usercao:chore/modernize-project
Sep 27, 2026
Merged

quantizor merged 5 commits into
styled-components:mainfrom
usercao:chore/modernize-project

Conversation

@usercao

@usercao usercao commented Sep 16, 2026 •

Copy link
Copy Markdown
Collaborator

Hi @quantizor, I have prepared this update to bring the project up to date while preserving its existing purpose and public API.

This PR keeps the project's role as a cross-editor TypeScript Server plugin. The work is intended to make the plugin easier to maintain, test, and publish, while also addressing several compatibility and language-service issues found during the update.

What changed

  • Migrated the development workflow to Yarn 4 and replaced the previous build, lint, formatting, and test tooling with tsdown, Oxlint, Oxfmt, and Vitest.
  • Updated the language-service dependencies. The tsserver entry is a plain CommonJS file at 1.0.1's own lib/index.js path, so every supported host loads it with a synchronous require() and no require(esm) interop; ./api ships a real CommonJS build for require() and an ESM build for import().
  • Reorganized the implementation into focused modules for configuration, template substitution, virtual documents, tsserver integration, and individual CSS language-service features, while preserving the public constructor and mapping contracts.
  • Expanded unit and tsserver end-to-end coverage for completions, completion details, diagnostics, code fixes, hover, folding, Emmet, configuration updates, plugin lifecycle, script kinds, styled-components syntax, interpolation mapping, and the published package API.
  • Improved handling of dynamic CSS declaration names and values, globalCss and keyframes templates, completion edit ranges, cache invalidation, and disabled validation. Completion entries keep 1.0.1's shape: the editor inserts the entry's label.
  • Fixed CSS code actions at diagnostic start positions, retained the original three-argument public code-fix call, and ensured explicitly filtered requests only return fixes for the plugin's diagnostic code.
  • Hardened public extension boundaries by validating virtual-document mappings through the provider contract instead of assuming a fixed wrapper or trailer, and safely accepting unknown object-shaped or partial plugin configuration from tsserver. A new StyledPluginConfigurationInput type covers partial configuration, and the lint API now also exposes unknownAtRules.
  • Preserved all ECMAScript line terminators during template substitution. U+2028 and U+2029 are normalized only in the virtual SCSS document, keeping UTF-16 offsets stable while aligning TypeScript and CSS language-service line maps.
  • Reduced repeated work on interactive completion and hover paths by reusing virtual documents and parsed stylesheets. The provider now controls cache reuse explicitly, with built-in invalidation across text, wrapper, and file changes and conservative behavior for third-party providers.
  • Hardened the real-tsserver harness by resolving fixture paths with native ESM APIs, parsing protocol frames as raw UTF-8 bytes, escaping Unicode line separators in the line protocol, matching responses by request_seq, and failing tests when tsserver exits unsuccessfully or leaves requests incomplete.
  • Added focused regression coverage for completion-source merging, duplicate candidates, SCSS filtering, unknown completion details, hover and folding fallbacks, diagnostic severity and code translation, invalid code actions, cross-file cache isolation, injectGlobal, and disabled Emmet behavior.
  • Added a prepack build so a clean Git checkout cannot publish an empty shell package. Package tests remove existing build output, create and unpack the tarball, synchronously load the tsserver entry, import and require the API, and exercise the legacy code-fix API from the packed artifact.
  • Refreshed the README and split usage and maintenance guidance into dedicated documents, including the supported host contract and release workflow.
  • Added a 24-hour Yarn package-age gate so newly published dependency versions are not selected immediately during dependency resolution.

Compatibility notes

This is a minor release with no breaking changes for 1.0.1 users. The plugin supports TypeScript 5.0 and newer (6.x recommended) and Node.js 14.21.3 or later in the tsserver host. TypeScript 5.0 is the range 1.0.1 actually ran on: on older hosts 1.0.1 crashes during activation, while this version logs a clear message and leaves the host's language service untouched. The Node floor is the one 1.0.1's own runtime dependencies already require.

The tsserver entry, its types, the lib/api deep imports, and StyledPluginConfiguration keep 1.0.1's exact shapes, and a type-check gate compiles a consumer written against 1.0.1 on every supported TypeScript version. The public language-service API is also available through the new ./api export. Legacy three-argument code-fix calls remain supported, and custom virtual-document providers can opt into cache reuse. VS Code with workspace TypeScript is the primary verified host; other tsserver-compatible editors should be listed as supported only after host-specific validation.

Verification

The repository provides a single yarn verify workflow covering formatting, linting, strict type checking, 873 unit tests, 159 real-tsserver end-to-end tests, a scaling and retained-memory guardrail, package API type checks on TypeScript 5.0 through the current release (including a 1.0.1-shaped consumer and node10 and bundler resolution), packed-artifact runtime consumption, and an exact check of the files yarn pack puts in the tarball. CI builds and packs once on the Node.js version in .github/.node-version and runs the end-to-end suite on Node.js 22.12 with TypeScript 5.0.4, 5.9, and the current release, and on Node.js 24 with the current release. Separate jobs drive a real tsserver from the packed tarball on Node.js 14.21.3 with TypeScript 5.0.4, and prove graceful non-activation on TypeScript 4.9.5. Performance benchmarks are also included to track completion latency and retained heap behavior. Every test and measurement tool runs under a wall-clock deadline and a heap cap, so a regression fails loudly instead of hanging the machine.

Next steps

After this PR, I plan to address the currently open issues in this repository. Once those have been handled, I plan to update and maintain the downstream styled-components/vscode-styled-components repository.

Please merge this PR. If you have a different direction in mind or would like any part of the update adjusted, please let me know and I will update it accordingly.

Thank you for your time and review.


Maintainer follow-up

Thank you so much @usercao for this. I built on your work and compacted the branch into four commits (build, language service, tests, docs), each co-authored by you, so the release can ship as a minor with no breaking changes for 1.0.1 users:

  • Kept a CommonJS tsserver entry at 1.0.1's path with 1.0.1's declared types, restored the TypeScript 5.0 floor 1.0.1 actually ran on, and matched 1.0.1's completion entries and lib/api imports.
  • Fixed position mapping for multi-line interpolations, end-of-template errors, and files not open in the editor, and made every plugin entry point recover from an unexpected exception instead of failing tsserver's whole response.
  • Made interpolation substitution linear on long lines, cached diagnostics per template with a size bound, and added a scaling and retained-memory guardrail to CI. Diagnostics, completions, and hover on large files are faster than 1.0.1, and a request the caches already answer skips re-reading the template.
  • Adapted to styled-components v7 templates: interpolations in block position (after ;, {, }, or a comment) are read as mixins, value-shaped css fragments validate as values, a nested @layer with declarations no longer reports { expected (a workaround for SCSS: nested @layer with declarations reports '{ expected' microsoft/vscode-css-languageservice#530), and @ at the start of a statement suggests the at-rules that work in a component.
  • Removed false errors on code that works at runtime, most of which 1.0.1 reported too: interpolations inside property names (margin-${side}), at-rule conditions (@media screen and ${q}), keyframe percentages, selector lists split over lines, multi-line values, a ; or { inside a nearby comment or string, and JavaScript escapes, which are now read the way styled-components receives them. Emmet no longer suggests declarations inside values, strings, or comments, and a mistyped or misspelled plugin setting now logs what was ignored and the accepted form. yarn compare-release compares the working build against a published release.
  • Added AGENTS.md and an architecture spec, and set up Changesets with a release workflow that runs the full yarn verify before it can publish; release notes now live in .changeset/.

Copilot AI lite review requested due to automatic review settings September 16, 2026 07:20
@changeset-bot

changeset-bot Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6b6cf02

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@styled/typescript-styled-plugin Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

One or more issues must be addressed before approval.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Modernizes the TypeScript styled-components plugin with an ESM/TypeScript 6 build, modular language-service implementation, and expanded automated coverage.

Changes:

  • Replaces legacy tooling with Yarn 4, tsdown, Vitest, Oxlint, and Oxfmt.
  • Refactors configuration, substitutions, virtual documents, CSS features, and tsserver integration.
  • Adds unit, E2E, package API, performance, CI, and documentation updates.
File summaries
File Description
vitest.config.ts Updated as part of this pull request.
tsdown.config.ts Updated as part of this pull request.
tsconfig.json Updated as part of this pull request.
test/unit/tsserver-plugin.test.ts Updated as part of this pull request.
test/unit/template-substitutions.test.ts Updated as part of this pull request.
test/unit/template-language-service.test.ts Updated as part of this pull request.
test/unit/styled-virtual-document-provider.test.ts Updated as part of this pull request.
test/unit/plugin-configuration.test.ts Updated as part of this pull request.
test/performance/template-language-service.bench.ts Updated as part of this pull request.
test/performance/template-language-service-fixture.ts Updated as part of this pull request.
test/package-api/tsconfig.json Updated as part of this pull request.
test/package-api/runtime.ts Updated as part of this pull request.
test/package-api/consumer.ts Updated as part of this pull request.
test/e2e/tsserver-fixture/index.ts Updated as part of this pull request.
test/e2e/tsconfig.json Updated as part of this pull request.
test/e2e/styled-project-fixture/tsconfig.json Updated as part of this pull request.
test/e2e/styled-project-fixture/script-kind-tsx.tsx Updated as part of this pull request.
test/e2e/styled-project-fixture/script-kind-jsx.jsx Updated as part of this pull request.
test/e2e/styled-project-fixture/script-kind-js.js Updated as part of this pull request.
test/e2e/styled-project-fixture/main.ts Updated as part of this pull request.
test/e2e/styled-project-fixture/index.ts Updated as part of this pull request.
test/e2e/styled-project-fixture/.vscode/settings.json Updated as part of this pull request.
test/e2e/scenarios/tsserver-test-helpers.ts Updated as part of this pull request.
test/e2e/scenarios/styled-components-syntax.test.ts Updated as part of this pull request.
test/e2e/scenarios/script-kinds.test.ts Updated as part of this pull request.
test/e2e/scenarios/plugin-lifecycle.test.ts Updated as part of this pull request.
test/e2e/scenarios/outlining-spans.test.ts Updated as part of this pull request.
test/e2e/scenarios/hover.test.ts Updated as part of this pull request.
test/e2e/scenarios/emmet-completions.test.ts Updated as part of this pull request.
test/e2e/scenarios/completions.test.ts Updated as part of this pull request.
test/e2e/scenarios/completion-entry-details.test.ts Updated as part of this pull request.
test/e2e/scenarios/code-fixes.test.ts Updated as part of this pull request.
test/e2e/plugin-missing-project-fixture/tsconfig.json Updated as part of this pull request.
test/e2e/plugin-missing-project-fixture/main.ts Updated as part of this pull request.
test/e2e/plugin-missing-project-fixture/index.ts Updated as part of this pull request.
test/e2e/package.json Updated as part of this pull request.
test/e2e/emmet-disabled-project-fixture/tsconfig.json Updated as part of this pull request.
test/e2e/emmet-disabled-project-fixture/main.ts Updated as part of this pull request.
test/e2e/emmet-disabled-project-fixture/index.ts Updated as part of this pull request.
test/e2e/.gitignore Updated as part of this pull request.
src/virtual-document/virtual-document-session-provider.ts Updated as part of this pull request.
src/virtual-document/styled-virtual-document-provider.ts Updated as part of this pull request.
src/tsserver/tsserver-plugin.ts Updated as part of this pull request.
src/tsserver/tsserver-logger.ts Updated as part of this pull request.
src/tsserver/plugin-identity.ts Updated as part of this pull request.
src/test/substituter.test.ts Updated as part of this pull request.
src/template/template-substitutions.ts Updated as part of this pull request.
src/template-language-service.ts Updated as part of this pull request.
src/index.ts Updated as part of this pull request.
src/features/styles-language-services.ts Updated as part of this pull request.
src/features/hover.ts Updated as part of this pull request.
src/features/folding.ts Updated as part of this pull request.
src/features/diagnostics.ts Updated as part of this pull request.
src/features/css-diagnostic-code.ts Updated as part of this pull request.
src/features/completions.ts Updated as part of this pull request.
src/features/code-actions.ts Updated as part of this pull request.
src/configuration/plugin-configuration.ts Updated as part of this pull request.
src/api.ts Updated as part of this pull request.
src/_virtual-document-provider.ts Updated as part of this pull request.
src/_substituter.ts Updated as part of this pull request.
src/_plugin.ts Updated as part of this pull request.
src/_logger.ts Updated as part of this pull request.
src/_language-service.ts Updated as part of this pull request.
src/_configuration.ts Updated as part of this pull request.
README.md Updated as part of this pull request.
package.json Updated as part of this pull request.
e2e/tests/quickFix.js Updated as part of this pull request.
e2e/tests/outliningSpans.js Updated as part of this pull request.
e2e/tests/errors.js Updated as part of this pull request.
e2e/tests/emmetCompletions.js Updated as part of this pull request.
e2e/tests/completions.js Updated as part of this pull request.
e2e/tests/completionEntryDetails.js Updated as part of this pull request.
e2e/tests/_helpers.js Updated as part of this pull request.
e2e/server-fixture/index.js Updated as part of this pull request.
e2e/project-fixture/tsconfig.json Updated as part of this pull request.
e2e/project-fixture/main.ts Updated as part of this pull request.
e2e/project-fixture/index.ts Updated as part of this pull request.
e2e/project-fixture/.vscode/settings.json Updated as part of this pull request.
e2e/package.json Updated as part of this pull request.
e2e/disabled-emmet-project-fixture/tsconfig.json Updated as part of this pull request.
e2e/disabled-emmet-project-fixture/main.ts Updated as part of this pull request.
e2e/disabled-emmet-project-fixture/index.ts Updated as part of this pull request.
docs/usage.md Updated as part of this pull request.
docs/maintenance.md Updated as part of this pull request.
CHANGELOG.md Updated as part of this pull request.
.yarnrc.yml Updated as part of this pull request.
.vscode/tasks.json Updated as part of this pull request.
.vscode/settings.json Updated as part of this pull request.
.vscode/launch.json Updated as part of this pull request.
.prettierrc.json Updated as part of this pull request.
.oxlintrc.json Updated as part of this pull request.
.oxfmtrc.json Updated as part of this pull request.
.gitignore Updated as part of this pull request.
.github/workflows/ci.yml Updated as part of this pull request.
.github/needs_more_info.yml Updated as part of this pull request.
.eslintrc.js Updated as part of this pull request.
.eslintignore Updated as part of this pull request.
.editorconfig Updated as part of this pull request.
Review details

Suppressed comments (4)

src/template/template-substitutions.ts:65

  • The . pattern also matches \r, so a placeholder containing CRLF is converted to x\n instead of preserving \r\n; the added CRLF tests in test/unit/template-substitutions.test.ts will fail. Match all non-line-break code units instead.
    src/template/template-substitutions.ts:71
  • This branch has the same CRLF corruption in the mixin path: \r is replaced with x before the newline. Preserve both \r and \n here as well, otherwise multiline placeholders followed by a semicolon lose their original line endings.
    src/virtual-document/styled-virtual-document-provider.ts:14
  • This adds a context-dependent overload to the exported VirtualDocumentProvider mapping contract. Existing providers that implement the old one-argument method can still be passed here, but the new feature code will call them with a context and receive wrapper positions without the new boundary check, so wrapper edits/diagnostics can leak into source offsets. Make the context-aware mapping additive or adapt legacy providers before using it.
    test/unit/template-substitutions.test.ts:36
  • The new test name contains the typo proeprty; please correct it to property.
  • Files reviewed: 92/103 changed files
  • Comments generated: 20
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread package.json
Comment thread test/e2e/scenarios/code-fixes.test.ts Outdated
Comment thread test/e2e/scenarios/completion-entry-details.test.ts Outdated
Comment thread test/e2e/scenarios/completions.test.ts Outdated
Comment thread test/e2e/scenarios/emmet-completions.test.ts Outdated
Comment thread .vscode/launch.json Outdated
Comment thread .vscode/tasks.json Outdated
Comment thread package.json
Comment thread test/e2e/scenarios/outlining-spans.test.ts Outdated
Comment thread docs/usage.md Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Completion translation and malformed top-level configuration handling contain unresolved runtime bugs.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

src/configuration/plugin-configuration.ts:83

  • This method is a runtime tsserver boundary, but a null or missing configuration value causes an immediate property-access exception before the malformed configuration can be ignored. Normalize the top-level value as well as its fields so configurePlugin cannot break the plugin with valid JSON such as configuration: null.
    src/features/completions.ts:240
  • Completion items are allowed to omit textEdit; in that case the editor should insert at the requested position. Returning a zero-length replacementSpan at offset 0 instead makes accepting such an item insert at the beginning of the template. Omit replacementSpan when no range is available (and handle InsertReplaceEdit separately if the providers can return it).
  • Files reviewed: 97/105 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Critical CI and supply-chain findings, plus a moderate completion issue, remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (1)

src/features/completions.ts:181

  • VirtualDocumentProvider is exported as a public API, but its contract does not require a virtual document to end with \n}. Hard-coding that trailer here makes valid completion edits at the end of a custom provider's document get rejected (or allows edits into a different trailer). Derive the template end from the TemplateContext/provider mapping instead of assuming StyledVirtualDocumentProvider's suffix.
  • Files reviewed: 97/105 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread .github/workflows/ci.yml Outdated
Comment thread .yarnrc.yml Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Package API verification fails on the supported Node 22.12.0 minimum because it runs a TypeScript harness with plain Node.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (3)

package.json:82

  • The PR description says the package requires Node.js 24.11 or newer, but this published engine range—and the CI/docs—sets the minimum to 22.12.0. Because this is a release-critical constraint for synchronous require(ESM), please reconcile the implementation and compatibility notes before publishing rather than advertising conflicting host requirements.
    test/unit/template-substitutions.test.ts:14
  • The test title uses “a entire”; use “an entire” so the newly added test description is grammatically correct.
    test/unit/template-substitutions.test.ts:21
  • The test title uses “a entire”; use “an entire” so the newly added test description is grammatically correct.
  • Files reviewed: 99/107 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread package.json Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The VS Code SDK setting is invalid, and code fixes fail for a cursor at a diagnostic’s first character.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

.vscode/settings.json:22

  • This is not the VS Code TypeScript SDK setting, so the workspace will continue using VS Code's bundled TypeScript despite the surrounding documentation requiring the workspace SDK. Use the built-in typescript.tsdk setting instead (the deleted fixture setting used that key as well).
    src/features/code-actions.ts:97
  • A zero-length code-fix request at the first character of a diagnostic is rejected here because isAfter(right.end, left.start) treats equality as non-overlap. Editors can request fixes with a cursor range, so placing the cursor on the first character of boarder yields no rename action even though a cursor one character later works; keep the diagnostic end exclusive, but include equality at its start.
  • Files reviewed: 99/107 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Empty whole-declaration interpolations currently shift all subsequent source mappings, and the new globalStyle integration lacks real-tsserver coverage.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

src/configuration/plugin-configuration.ts:52

  • The new default globalStyle tag is only checked as configuration data. The real-tsserver fixture overrides tags without globalStyle, and the styled-components syntax matrix does not exercise it, so the advertised host behavior can regress unnoticed. Add globalStyle to the fixture configuration and syntax scenario.
    CHANGELOG.md:173
  • Correct the misspelling in this changelog entry.
  • Files reviewed: 99/107 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread src/template/template-substitutions.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The broad modernization and an unresolved E2E completion-response contract issue require human review.

Review details

Suppressed comments (3)

src/api.ts:4

  • The public API comment is missing “be”, so it currently reads ungrammatically. Please change it to “allows the language service to be consumed by other libraries.”
    test/e2e/tsserver-fixture/index.ts:40
  • translateCompletionEntry intentionally omits replacementSpan when an LSP completion has no textEdit (see test/unit/template-language-service.test.ts:108-117), but this protocol model requires the field for every completion. Any E2E assertion that reads item.replacementSpan for such a valid response will be unsound or throw; make the field optional to match tsserver responses.
    test/unit/tsserver-message-reader.test.ts:29
  • This test sends both complete frames in one chunk, so it verifies UTF-8 byte length but not the advertised case where a response is split inside a multibyte character. Split the first frame at a byte within 颜色 and assert that no message is emitted until the remaining bytes arrive, so regressions in raw-byte buffering are caught.
  • Files reviewed: 99/107 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The E2E harness has unresolved request-matching and bounded-shutdown issues.

Review details

Suppressed comments (2)

test/e2e/scenarios/tsserver-test-helpers.ts:27

  • The harness tracks request_seq only to clear the pending set, but this helper still selects the first response for a command by arrival order. Scenarios such as the lifecycle and code-fix tests issue multiple requests of the same command, so an out-of-order tsserver response can be asserted against the wrong request; retain each returned sequence number and select responses by request_seq (or expose a request-specific helper) to make the claimed response matching reliable.
    test/e2e/tsserver-fixture/index.ts:200
  • When any request is still pending, close() marks the server closed but deliberately does not call shutdown(). If tsserver drops a response or remains alive without answering, the close event can never occur, so the pending-request check below is never reached and the test hangs until Vitest's timeout instead of reporting the incomplete request. Add a bounded close timeout/kill path (or otherwise force an exit and reject with the pending sequence numbers).
  • Files reviewed: 100/108 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

src/features/diagnostics.ts must honor validate: false before approval.

Review details

Suppressed comments (1)

src/features/diagnostics.ts:25

  • validate is normalized and updated by the configuration manager, but this feature never consults it before calling doValidation. As a result, validate: false still produces CSS diagnostics (the new plugin-lifecycle scenario at test/e2e/scenarios/plugin-lifecycle.test.ts:69 will fail), and configuration changes cannot disable validation. Pass the configuration manager or a validation predicate into this feature and return an empty list when validation is disabled.
  • Files reviewed: 100/108 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No blockers in the release tooling itself — four suggestions inline, all in comments, docs, or dependency config.

Reviewed changes

Reviewed the two commits pushed since the last review (29 files): the changesets setup and release automation, the custom changelog generator with its credits script and unit tests, the new Dependabot npm entry, and the four test tightenings from the previous round. I reproduced the release path locally (yarn changeset status, yarn changeset add --empty, yarn changeset version, which produced the 1.1.0 notes), the 177-test unit suite, and npm pack --dry-run, and checked the third-party claims the workflow comments rest on against primary sources.

  • Changesets plus a release workflow — seven changesets under .changeset/, a config wiring a custom generator, and .github/workflows/release.yml publishing on main through changesets/action pinned to v2.1.2 with npm trusted publishing and no NPM_TOKEN. package.json gains workspaces: ["."] so changesets sees the root package.
  • Custom changelog generator — scripts/changelog.cjs cites every commit that added or modified a changeset file (--diff-filter=AM) and thanks every non-maintainer, non-agent contributor; scripts/changeset-credits.ts reports the same attribution for whoever writes a credit line by hand; test/unit/changelog.test.ts covers attribution, agent and maintainer filtering, the shallow-clone fallback, and both release-line shapes. The end-to-end path works: changeset version produced correct notes from real history.
  • Dependabot npm entry — covers the root yarn.lock and, through the Yarn workspace, test/e2e/package.json, with a 1-day cooldown, minor/patch grouping, and five typescript-* alias pins listed under ignore.
  • Test tightenings from the last round — the template-end diagnostic test now asserts an exact anchor, the missing-plugin test is renamed and asserts its premise from the tsserver log, the scaling check hoists createTemplateContext out of the timed region, and the message-reader test pins its multibyte split point. All four prior findings are resolved.
  • Doc and comment follow-ups — CHANGELOG.md's hand-written Unreleased section is replaced by the changesets, the maskSubstitutions comment no longer over-claims, and StyledPluginEmmetConfiguration is alphabetized.

ℹ️ Nitpicks

  • .github/dependabot.yml:15 — "Rounds .yarnrc.yml's npmMinimalAgeGate (minutes) up to whole days" describes a rounding that does not happen: 1440 minutes is exactly 1 day. The comment also reads as if the cooldown only tightens things, while default-days: 1 lowers Dependabot's built-in 3-day default for version updates.
  • src/template/template-substitutions.ts:303 — the rewritten maskSubstitutions comment wraps mid-sentence ("Spans need / not arrive sorted or"), and oxfmt does not reflow comments for you.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using Space Bunny (free) | 𝕏

Comment thread .github/workflows/release.yml Outdated
Comment thread .github/workflows/release.yml Outdated
Comment thread docs/maintenance.md Outdated
Comment thread .github/dependabot.yml Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No correctness blockers in the new commit — two informational notes below. This one cannot be an approval while the four threads from the last round are still open.

Reviewed changes

Reviewed the one commit pushed since the last review (a4104e2, 4 files): the scaling guardrail, the node10 self-link script, and the two doc paragraphs describing both. I ran yarn test:scaling end to end, forced its two new failure paths with a throwaway copy of the script, exercised every branch of the self-link script by hand, and ran yarn test:package-api:node10 on top of it.

  • Every timed scaling result is now checked against its fixture — defineCheck takes a verify beside run, stops the clock before verifying, and reports a mismatch separately from the ratio, skipping the ratio entirely once a result is wrong, so a feature that throws (caught into an empty result, fast at every size) can no longer pass as a healthy operation. The expectations are derived from the fixtures rather than copied from a run: exact substituted text for all four placeholder shapes, the colr offsets as diagnostic starts, one outlining span per rule, the hover span at offset 0 with the selector in the documentation, the red value entry, the rename fix's span, and one diagnostic per template in both many-template checks.
  • A plausibility floor on the ratio — MIN_PLAUSIBLE_RATIO = 1.5 fails a check whose 4N run is not meaningfully slower than its N run, with a message naming the two ways that happens. Measured here, all eleven checks land at 3.35-4.74: 2x above the floor, 1.5x below SCALING_THRESHOLD = 7. Forcing the floor and a wrong expectation in a scratch copy produced the expected FAIL lines and exit code 1.
  • The node10 self-link is repaired instead of assumed — ensure-node10-self-link.mjs realpaths both sides, re-points a link that resolves elsewhere or dangles, and refuses to replace a real directory or file. All four cases in its header comment behave as written, including both failure exits.
  • Both doc paragraphs match the code — docs/maintenance.md's scaling description and docs/architecture.md's "Public surface" bullet now say what the gate and the script actually do.

ℹ️ The retained-heap guardrail reads bimodally, and the negative tolerance is nearly spent

RETAINED_HEAP_NEGATIVE_TOLERANCE_BYTES is half of RETAINED_HEAP_THRESHOLD_BYTES, so 7.00MB at the current budgets, and across four runs of yarn test:scaling the retained delta was +3.38MB, -6.55MB, +3.40MB, -6.55MB. It lands in one of two spots, and the negative one clears the tolerance by 0.45MB, so a different Node version or a differently loaded runner can push it past and fail yarn verify with a message blaming the probe. This did not come from this commit (the constant arrived in 3c53611, and stubbing out this commit's verify reproduces the same two readings), but it is the one remaining knife-edge in a file this commit just made trustworthy.

Technical details
# The retained-heap guardrail's negative tolerance has ~0.45MB of headroom on a bimodal reading

## Affected sites
- `test/performance/scaling-check.ts:517` — `RETAINED_HEAP_NEGATIVE_TOLERANCE_BYTES =
  RETAINED_HEAP_THRESHOLD_BYTES / 2`, i.e. 7.00MB at `MAX_VALIDATION_CACHE_BYTES * 3 + 2MB`.
  Introduced in `3c53611`, not in this commit.
- `test/performance/scaling-check.ts:577` and `:585` — the "before" and "after" readings, both taken
  in the process that has just run eleven timed checks plus their verifications.
- `test/performance/scaling-check.ts:604` — the throw whose message concludes "the probe is broken".

## Evidence (Node 24.19.0, `a4104e2`, `yarn test:scaling`)
- Four clean runs: `+3.38MB`, `-6.55MB`, `+3.40MB`, `-6.55MB`, against a `14.00MB` threshold.
- Re-running a copy whose `verify` is stubbed to `mismatch: undefined`, so none of this commit's
  added allocation runs: `-6.55MB`, `-6.56MB`, `+3.39MB`. The delta is not the cause.
- The guardrail is not measuring a smaller cache: every one of its `RETAINED_HEAP_EDIT_COUNT` (6000)
  templates is distinct by its leading `/* edit N */` comment, so each is a cold
  `RawValidationCache` miss, and the service is read again after the "after" gc specifically to
  keep it reachable. The bimodality is heap state inherited from the timed checks.

## Required outcome
- The guardrail's readings stop depending on heap state inherited from the timed checks, or the
  tolerance stops being a coin flip against a bimodal measurement.

## Suggested approach
- Run the guardrail in a child process, so its "before" reading is a fresh heap. A before/after delta
  inside a process that has just done a megabyte-scale allocation storm is only meaningful when the
  two readings are taken from the same starting state.
- Failing that, raise `RETAINED_HEAP_NEGATIVE_TOLERANCE_BYTES` to `RETAINED_HEAP_THRESHOLD_BYTES`
  (still far below the positive threshold) and record in its comment that the reading is bimodal, so
  a future reader does not read a 0.45MB margin as a measured bound.

ℹ️ Nitpicks

  • Two one-line descriptions of the gate still predate this commit: AGENTS.md:78 says yarn test:scaling "gates superlinear time and retained memory in CI", and the docs/maintenance.md:52 commands table still reads "Guard against superlinear regressions in a hot operation". Since this commit it also fails a wrong result and an implausible ratio, and the docs/maintenance.md prose just below was updated for exactly that.

Pullfrog  | Fix it ➔ | View workflow run | Using Space Bunny (free) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found. All four threads from the last round are genuinely addressed, and the new prose in this commit is the accurate version of the claims it replaced.

Reviewed changes

Reviewed the one commit pushed since the last review (208d029, 4 files, +32/−27): the release workflow, the Dependabot npm entry, the npmjs.com trusted-publisher instructions, and a new publishConfig.registry. Nothing runs in CI here, so I checked every load-bearing claim the new comments rest on against primary sources instead of taking the prose on faith: Yarn 4.18.1's publish.ts / npmConfigUtils.ts / npmHttpUtils.ts, @changesets/cli 3.0.3's yarn publish tool, dependabot-core's npm and Yarn file parsers, and GitHub's Dependabot options reference.

  • Dependabot now protects the requirement it can actually see — the five typescript-* entries are replaced by one dependency-name: typescript. The aliases really are invisible to Dependabot (dropped in ignore_requirement? on the manifest path, skipped in YarnLock#dependencies while dealias_packages? is off on the lockfile path), while plain typescript is the name the live ~6.0.3 range resolves to in yarn.lock, so this entry is the one that bites.
  • The 1-day cooldown gives way to the platform default — dropping cooldown is safe because Dependabot's documented default for version updates is 3 days (version updates only, so security updates are unaffected), which still clears npmMinimalAgeGate: 1440.
  • Two unused steps leave the release job — the npm install -g npm@12 pin and setup-node's registry-url. @changesets/cli 3.0.3 selects its yarn tool, whose publish runs yarn npm publish --access … --tag … and whose info runs yarn npm info … --json, so the job never invokes the npm binary and the .npmrc that registry-url writes was never read.
  • Publishing now names its registry explicitly — publishConfig.registry is honored: getPublishRegistry returns manifest.publishConfig.registry ahead of any scope or fetch setting, and the OIDC exchange posts to ${registry}/-/npm/v1/oidc/token/exchange/package/… built from that same registry, which is exactly what the new docs/maintenance.md paragraph describes. No flag overrides it, since the yarn publish tool passes no --registry.
  • The Yarn OIDC floor is attributed to the versions that introduced it — 4.10.0 for the exchange, 4.10.3 for the scoped-package fix that this package needs, so packageManager: yarn@4.18.1 reads as a pin with slack rather than as the feature boundary.
  • The setup list tells the maintainer to permit direct publishing — step 3 names the npm stage publish default for trusted publishers configured after 2026-09-03, so the first release no longer depends on an npmjs.com UI default this workflow does not match.

Also confirmed while reading around the delta: the PR description's compatibility section now matches the branch (CommonJS tsserver entry, TypeScript 5.0+, Node 14.21.3+, 177 unit and 131 e2e tests), which closes the one body-level item left over from the first review.

Pullfrog  | View workflow run | Using Space Bunny (free) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No correctness blockers in the new behavior — the substitution, wrapper, and completion work all hold up against the real CSS language service. Three inline notes are the spec and its comments describing that work inaccurately, plus one completeness question.

Reviewed changes

Reviewed the one commit pushed since the last review (765ab1d, "feat: adapt to styled-components v7 templates and modern nested at-rules", 16 files, +1029/−112): the substitution statement rule, the @layer prelude rewrite, the css value wrapper, nested at-rule completions, the spec sections describing all three, four changesets, and the new unit, e2e, and scaling coverage. I ran yarn verify end to end on this branch (format, lint, typecheck, 260 unit, 133 e2e, all five package-API type gates, npm pack --dry-run) plus yarn test:scaling (thirteen checks, ratios 3.51–5.07 against a threshold of 7, the two new ones at 3.71 and 4.49), then probed vscode-css-languageservice@6.3.10 and @vscode/emmet-helper@2.11.0 directly to confirm what each workaround is actually working around.

  • A statement rule for block-position interpolations — getTemplateSubstitutions now also reads a placeholder as a mixin when the last significant character before it, across lines, is ;, {, }, or the template start, chaining that classification to a following placeholder, so color: red; ${mixin}, &:hover { ${mixin} }, and ${a} ${b} stop reporting false colon expected / semi-colon expected. The excluded-next set keeps selector continuations, property names, and rule bodies x-filled; I ran 28 real-world template shapes through the real validator and saw no false diagnostics, with output length still equal to input length everywhere.
  • A nested block @layer rewrite in the virtual document — a block @layer prelude in a nested position becomes a same-length & prelude, so the SCSS parser stops misparsing declarations in a layer body. The linked upstream issue is real, still open, and reproduces here; I also confirmed @layer is the only nested at-rule that misbehaves (declarations directly in @media, @supports, @container, @scope, and @starting-style bodies already validate clean), so the new docs/usage.md claim holds.
  • A value wrapper for value-shaped css fragments — :root{all:\n when the tag is css and the substituted text reads as one value, memoized per TemplateContext in a WeakMap and folded into the validation cache key and canReuseVirtualDocument, both of which stay sound because the substituted text is a pure function of the raw text. An unclosed value still reports at the template end, and any fragment with a ;, {, or : keeps the declaration wrapper, so property completions survive mid-typing.
  • Nested at-rule completions — an @ or @me at a statement position adds nine at-rules carrying the CSS service's own documentation, cached once per service, skipping labels the CSS and SCSS lists already have, and replacing the whole at-keyword at the caret. I confirmed the catalog holds all nine names, that both language services really do return nothing usable at a nested @ (CSS mode returns properties, SCSS mode only Sass directives), and that Emmet's @media snippet survives beside the new keyword entry rather than being duplicated by name.
  • Coverage for all of it — the new tests assert exact values derived from their own fixtures (substituted text, diagnostic start/offset/text, wrapper text and length, entry shapes and replacement spans) rather than counts or truthiness, and the two new scaling checks are sized so an injected quadratic would fail with margin, which the measured 3.71 and 4.49 confirm.

ℹ️ Nitpicks

  • The PR description's "Verification" counts have drifted again: it says 177 unit and 131 e2e tests, and the branch now runs 260 and 133.
  • .changeset/nested-at-rule-completions.md:5 groups all nine at-rules under "the at-rules that work there", but @keyframes, @property, and @font-face are not valid nested in a style rule at all — they work only because v7 relocates them. docs/architecture.md:41 already draws that split correctly, so the release note could mirror it.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using Space Bunny (free) | 𝕏

Comment thread docs/architecture.md Outdated
Comment thread src/template/template-substitutions.ts Outdated
Comment thread src/features/completions.ts

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Two things to address before merging: the new shared CSS boundary scanner still diverges from the CSS language service in two of the places the new spec says it matches (one produces a false diagnostic, the other blanks a whole completion list), and the new createCodeLookback makes getTemplateSubstitutions quadratic for exactly the out-of-order spans its own contract accepts.

Reviewed changes

Reviewed the three commits pushed since the last review (090abd5, 1d90bc6, 5e3bc0a): the consolidated nonCodeEnd / createCodeLookback scanner and the two look-backs that consume it, the three added at-rules, scripts/check-pack-contents.ts plus its test:pack wiring, the retained-heap guardrail's child process, and the yarn pack and provenance changes in both workflows. I installed the branch and ran yarn test:pack (ok 14 files, exactly the expected set), yarn test:scaling (16 checks, ratios 3.31–4.69 against a threshold of 7, retained heap +3.87MB) and yarn test:unit (295 passing, matching the description), packed into a directory that did not exist, and drove the plugin's own diagnostics and completion paths against the pinned vscode-css-languageservice@6.3.10. Two specialists verified the YARN_NPM_PUBLISH_PROVENANCE chain through Yarn 4.18.1 and @changesets/cli 3.0.3, and hunted for scanner divergences.

  • One boundary scanner for four call sites — nonCodeEnd with commentEnd / stringEnd / escapeEnd / lineBreakEnd replaces four ad-hoc scanners, and createCodeLookback serves the statement rule and the at-keyword look-back. isLineBreak now includes \f and escapeEnd implements the CSS hex-escape rule; both are real fixes, and I confirmed 6.3.10 agrees (scssScanner.js:92-98, cssScanner.js:443-483). The look-back's runStart/runEnd memo survives a shuffled query order (637k queries, zero mismatches), and every sorted-input shape measures linear. Two gaps survive, in the inline below.
  • The statement rule looks back past comments — the mixin branch asks the look-back instead of a regex over the text before the placeholder, so color: red; /* note */ ${mixin}; now becomes $a:0;, and a placeholder inside a comment or string stays x-filled whatever that string contains. followsDeclarationBoundary folds in the previous-placeholder case, which is what keeps ${a} ${b} two mixins.
  • At-keyword completions skip comments, and the hoisted set is complete — the list is now suppressed inside a comment or string and offered after one (color: red; /* note */ @), and @page, @counter-style, and @font-palette-values join the set. The spec's inclusion rule does justify all three, so last round's completeness question is answered on the merits.
  • An exact pack-contents gate — scripts/check-pack-contents.ts runs the same packer yarn npm publish uses, derives the expected set from package.json plus the two build-hook files, and fails listing every missing and unexpected path; it replaces npm pack --dry-run in verify and CI ahead of a real yarn pack. I confirmed yarn pack --out creates missing parent directories, so dropping CI's mkdir -p package-artifact is fine, and that yarn pack still runs prepack, so the gate never reads a stale lib/.
  • The retained-heap guardrail gets a fresh process — the child-process split with a distinct exit code for "over budget" closes the bimodal reading I flagged two rounds ago, and the tolerance comment now explains the negative mode that survives even in the child rather than pretending it does not.
  • CI and release move to Yarn's packer — npm pack is gone from both workflows, provenance is on, and the two stale one-line gate descriptions in AGENTS.md and docs/maintenance.md now say what the gate actually fails on.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using Space Bunny (free) | 𝕏

Comment thread docs/architecture.md Outdated
Comment thread docs/architecture.md Outdated
Comment thread src/template/template-substitutions.ts
Comment thread .github/workflows/release.yml

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Three places where the new escape reading, the new caret classifier, and the new interpolated-name fill leave a false diagnostic or a bad completion in a shape one token away from the ones their own tests pin. None is a regression against 1.0.1, but all three sit in code these commits wrote, and two contradict claims the new changesets make.

Reviewed changes

Reviewed the three commits pushed since the last review (1b090c2, 8be45b4, 50de41e): the completed CSS-tokenizer escape handling, the new src/virtual-document/javascript-escapes.ts and its wiring into every structural decision, the new substitution branches for joined property names and at-rule conditions, the findCaretPlacement classifier, the configuration normalizer that reports rejected values, the scaling retry plus five new timed checks, and the compile:unless-fresh build-once plumbing. I installed the branch and ran yarn test:unit (529 passing), yarn test:e2e (143 passing), yarn test:scaling (21 checks, ratios 3.15–4.60 against a threshold of 7, retained heap +3.86MB), yarn test:pack (ok 14 files), format:check, lint, and typecheck, then drove the real plugin pipeline against the pinned vscode-css-languageservice@6.3.10 to reproduce each inline finding. Two specialists independently audited the escape stand-ins and the caret classifier; their two substantive findings are verified inline below.

  • Escapes at code positions are now followed, and the spec says so — nonCodeEnd returns the whole identifier run a backslash starts or continues (startsCodeEscape, identifierEnd), the look-back counts an escape as one significant character at its backslash, and opensUrlArgument decodes escapes to recognise url and url-prefix the way the parser does. This is the backslash half of the last round's scanner finding; the /*-inside-url() half of that thread is unchanged, and the spec bullet now describes it more precisely rather than differently. The new unterminated-run rule (isUnterminatedRun) is what lets a caret at the end of an unclosed string or comment keep typing into it, and the new unit block exercises that end-of-run case rather than only in-string escapes.
  • Templates are validated as the CSS styled-components receives — replaceJavaScriptEscapes turns each run of adjacent escape sequences into a same-length stand-in for its cooked text, and the masked text in substitution, the virtual document, the value-shape test, and the stray-brace search all read that. The arithmetic closes: 210k fuzzed inputs preserved length exactly, and the cooked values agree with a real eval cook across every escape shape I checked, invalid ones included. Spending the padding inside a CSS escape where CSS allows it ("\\f101" → "\0f101", u\\rl( → u\72l() is a nice touch; the one place it is not token-safe is inline below.
  • Interpolated names, at-rule conditions, and values split over lines — a placeholder joined to name characters becomes the Sass interpolation #{x} so the linter never reports an unknown property, one in an @media/@supports/@container prelude after a combinator becomes (x…) or x(…), and a value written one interpolation per line keeps the second a value instead of a dummy declaration. The one-parameter look-back (one gap, one word) and the per-query-kind run scanners keep all of this linear: the three new substitution checks measure 4.20–4.34, and the new diagnostics-with-escapes check measures 4.41.
  • Completions and Emmet land where they fit — findCaretPlacement returns NonCode inside a comment or string, NoDeclaration in a value, a pseudo-class selector, a media feature, or an at-rule prelude, and Other elsewhere; an empty template now offers property suggestions. The spec's cost claim holds: I could not build a shape that needs more than the two documented passes over the text before the caret.
  • Rejected configuration is reported instead of ignored — per-field normalization logs one line naming the setting, what it accepts, and what it received, accepts a handful of unambiguous aliases, and suggests the nearest setting for a misspelled key by edit distance. A logger that throws cannot stop the configuration from applying, and the name/global keys tsserver adds log nothing.
  • Contributor plumbing — a scaling ratio outside [1.5, 7] is re-measured up to twice so machine load alone cannot fail verify, while a wrong result still fails at once; compile:unless-fresh plus STYLED_PLUGIN_LIB_FRESH means verify and CI's compile job build lib/ once and every later script reuses it, with the lib/index.js-exists guard keeping the package-API runtime test honest after it deletes lib/; .gitattributes forces LF, which is what the exact-offset e2e fixtures need; changelog.cjs pipes git's stderr so a failure message reaches the caller instead of the terminal, and changeset-credits now throws with git's error rather than reporting "no history".

ℹ️ The description's test counts drifted again

"Verification" still says 295 unit tests and 133 real-tsserver end-to-end tests. On this head the suites are 529 and 143, so the number a reader uses to gauge the change is off by more than half.


Note: 4 inline comment(s) dropped because they did not anchor to lines inside the PR diff:

  • src/features/completions.ts:6876 (RIGHT) — line 6876 (RIGHT) is not inside a diff hunk
  • src/virtual-document/javascript-escapes.ts:9213 (RIGHT) — line 9213 (RIGHT) is not inside a diff hunk
  • src/template/template-substitutions.ts:8335 (RIGHT) — line 8335 (RIGHT) is not inside a diff hunk
  • docs/architecture.md:2291 (RIGHT) — line 2291 (RIGHT) is not inside a diff hunk

Pullfrog  | Fix it ➔ | View workflow run | Using Space Bunny (free) | 𝕏

@pullfrog

pullfrog Bot commented Sep 27, 2026

Copy link
Copy Markdown

The review above covers 1b090c2, 8be45b4, and 50de41e. Its four inline comments were dropped in transit (they were anchored to the rendered diff's line numbers rather than source line numbers), and a second review could not be posted in the same run, so the findings are restated here with source anchors. Everything below was reproduced by hand against the real plugin pipeline and the pinned vscode-css-languageservice@6.3.10; two specialists audited the new escape stand-ins and the caret classifier independently, and both findings were confirmed before posting.

⚠️ A ; inside an unquoted url() argument ends the statement, so Emmet offers a declaration inside a data URI

src/features/completions.ts:299, with isStructuralCharacter (:353) carrying no inUnquotedUrl state, so the ; of a data URI is read as the last structural code character and the : rule never fires.

template: background: url(data:image/svg+xml;utf8,<svg/>) no-repeat m10
caret at template end            -> Other,     `margin: 10px;` offered
caret at 36 (just after the `;`)  -> Other,     `user-select: none;` offered
control  url(icon.svg) no-repeat m10, caret at end -> NoDeclaration (correct)

Accepting at the caret end inserts margin: 10px; after no-repeat; accepting at offset 36 turns the data URI's utf8 into tf8. The CSS language service offers 265 items at both carets and none is a property name, so no declaration can start there, which is what .changeset/emmet-and-completion-placement.md promises.

Required outcome: a ;, { or } inside an unquoted url() argument does not end the statement, so a caret anywhere in the argument classifies as NoDeclaration. createCodeLookback already tracks inUnquotedUrl on CssCodeScanState; threading that state into the structural search is enough, and the findCaretPlacement doc comment should then say the search consults it.

⚠️ Escape-run padding is not token-safe inside an unquoted url()

src/virtual-document/javascript-escapes.ts:62. The space padding is only safe where whitespace is insignificant, and inside an unquoted url() argument whitespace ends the url token, so the rest of the declaration is re-read as a selector.

background: url(x\u0041y.png);    raw: 0 diagnostics   stand-in url(x     Ay.png): 4
background: url(foo\<newline>bar.png);   raw: 4      stand-in url(foo  bar.png): 4

url(x\u0041y.png) cooks to url(xAy.png), which the CSS language service validates clean, so the four errors (at-rule or selector expected, semi-colon expected, ) expected, identifier or variable expected) are new. The second row is the same defect in a shape that was already broken. joinEscapedCharacter exists precisely to avoid this, and its isSafe list (:111) constrains only the escaped character and the one after it, with no notion of the run's position; both doc comments (:32, :81) claim the join keeps a url() intact, and docs/architecture.md:52's "both parse without a diagnostic" holds for neither shape.

Required outcome: an escape run inside an unquoted url() argument keeps the url token intact, so the cooked CSS and the validated CSS agree. The shared scanner already exposes the state needed to classify the run. Both shapes belong in the unit block that already pins "a one-digit hex escape in a url()".

⚠️ A later & on the same line turns an interpolated property name into a selector

src/template/template-substitutions.ts:222. nextSpecial accepts the first { or & it reaches wherever it is on the line, so an & belonging to a later rule is read as this placeholder's own pseudo-class.

padding-${s}: 0; &:hover { color: red; }         -> padding-xxxx: 0; ...  Unknown property: 'padding-xxxx'
${s}-top: 0; &:hover { color: red; }             -> xxxx-top: 0; ...      Unknown property: 'xxxx-top'
border-${s}-color: red; &:hover { color: red; }  -> border-xxxx-color:…  Unknown property: 'border-xxxx-color'
${p}: 0; &:hover { color: red; }                 -> &   : 0; ...          at-rule or selector expected + } expected
the same four with the `&` on the next line      -> padding-#{x}: 0; ...  no diagnostics

The classifier behaved this way before these commits, so this is an instance the new #{x} branch does not reach rather than a regression, but docs/architecture.md:27 now states the joined-name rule unconditionally and the new test matrix pins color: red; ${side}-top: 0; without the & variant.

Required outcome: a joined property name is classified as a property name whenever a ;, { or } stands between its colon and any { or & the scan would otherwise accept. The regex this scan reproduces, /^\s*[:].+?[{&]/, could never cross a line terminator but could cross a ;; adding ; (and }) to the run scanner's stop set and treating either as "not a selector" leaves ${B}:hover & { working and makes the scan agree with the statement boundary the statement rule already tracks.

ℹ️ Two smaller items

  • docs/architecture.md:91 — "the key dropped, so the CSS language service's own default for that rule applies" is wrong for emptyRules, the one key defaultConfiguration.lint holds: a rejected value falls back to the plugin's 'ignore', while the CSS language service's own default for that rule is a warning (lintRules.js:44). So lint: { emptyRules: "Error" } leaves the rule silent, which is not what the log line "The default applies" reads as.
  • The PR description's "Verification" still says 295 unit tests and 133 real-tsserver end-to-end tests. On this head the suites are 529 and 143.

Pullfrog  | View workflow run | via Pullfrog | Using Space Bunny (free) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Two things to address before merging: the new single-identifier validation filter breaks the soundness of the per-file validation cache key, and a property name followed by a rule on the same line is still read as a selector.

Reviewed changes

Reviewed the two commits pushed since the last review (f23fc0e, 4c5e8aa, 25 files): the escape-run bookkeeping that widens diagnostics, hover, folding, and completion spans, the name-aware and url-aware padding in replaceJavaScriptEscapes, the single-identifier css reading with its intersection filter, the nextSpecialInList selector-list continuation, the keyframe % and @media … or placeholder shapes, normalizeSpans for ./api callers, the compare-release sensor, and the spec and test changes describing all of it. I ran the branch green end to end (format:check, lint, typecheck, 649 unit, 147 e2e, test:scaling at 28 checks with ratios 3.00-4.71 against a threshold of 7 and retained heap 3.88MB, test:pack at 14 files, and compare-release itself at 32 cases with 20 differing from 1.0.1), and drove the real plugin pipeline against the pinned vscode-css-languageservice@6.3.10 to reproduce each finding below.

  • Escape runs, and spans widened to whole runs — getTemplateEscapeRuns records the raw span each stand-in covers in the same pass as the replaced text and rides on TemplateLineMap as escapeRuns; fromVirtualDocRange (diagnostics, hover, folding), fromVirtualDocRangeStrict (code-fix edits), and widenToEscapeRuns (completion replacement spans) then resolve each end and widen it, and a fix whose edit overlaps a run is dropped. findFirstRunEndingAfter makes each lookup a binary search, and collapsing four fromVirtualDocPosition call sites into one fromVirtualDocRange per feature is a real readability win. I fuzzed 400k generated escape, name, and url inputs: no throw, length always equal to the input, runs always ascending and disjoint.
  • Padding that no longer splits a name or a url token — the new openName state machine moves a run's padding in front of the whole name (co\x6Cr reads as colr, .md\\:flex as .md\:flex, @me\x64ia as @media), falls back to a CSS hex escape where whitespace would split a larger token (&.bt\x6E as &.bt\06e, a:ho\x76er as a:ho\76 er), and writes _ inside an unquoted url() where a space would end the token. createUnquotedUrlLookback reuses the previous run's answer so each stretch of text is read once. This closes the prior round's escape-inside-url() finding: url(a\\(b\\).png) no longer reports ) expected, and the caret no longer mistakes a ; inside a data URI for a statement boundary.
  • A single-identifier css fragment reads as a value too — getCssFragmentShape returns a third shape, the provider keeps the declaration wrapper for completions and hands createValueReadingDocument a second document under :root{all:, and diagnostics keep only what both readings report, so css`none`, css`${x}px`, and css`colr` are silent while css`colr: red;` still reports. The fake-language-service test proving a diagnostic both readings share survives, and the cache-hit count test still holds.
  • The url() boundary scanner now matches the parser's trivia, token, trivia reading — urlArgumentRunEnd opens a block comment only where a token could start, keeps // off, steps over an escape, and makes every character inside the argument url content for findStrayClosingBraceOffset, getCssFragmentShape, and the look-back, which answers the argument's (. This closes the remaining half of the docs/architecture.md:37 thread, and the three prior findings that were still open on 50de41e are now genuinely fixed.
  • More placeholder shapes — ${step}% fills with zeros, a selector list split over lines ending in , is read as selectors, @media ${a} or ${b} and preludes behind comments are conditions, and normalizeSpans sorts, clamps, and merges an ./api caller's spans so the output depends only on positions. Seven new scaling checks cover the list scanner, the unterminated-comment skipper, reversed spans, and the two escape paths; all measure linear (3.47-4.29).
  • compare-release, a sensor against a published release — scripts/compare-release.ts and its default cases run each template through lib/ and 1.0.1 the way tsserver loads a plugin and print what differs. It works, and its output is the most legible account of this PR's behavior change anywhere in the repo.

ℹ️ The escape-replacement invariants have no property test, and this delta added the branching that could break them

replaceEscapes grew three new interacting pieces of state (openName, hexEscapeLastCharacter, createUnquotedUrlLookback), and every table case asserts result.length === raw.length individually, so nothing checks the invariant the whole plugin's offset mapping rests on. I fuzzed 400k generated inputs and it holds today, which is why this is informational rather than a blocker, but the check is cheap and the failure mode is silent: a length that drifts by one shifts every diagnostic, hover, fold, and edit in that template.

Technical details
# The escape-replacement length and run-span invariants are only asserted per case

## Affected sites
- `src/virtual-document/javascript-escapes.ts:92` (`replaceEscapes`) — the new `openName`
  lifecycle, the `hexEscapeLastCharacter` fallback, and the `URL_PADDING` branch; the
  stand-in a run produces and the `[start, end)` pushed to `escapeRuns` come out of three
  different paths that must agree.
- `test/unit/javascript-escapes.test.ts:16` — the tables assert length per row; nothing
  iterates.

## Required outcome
- One test that generates inputs from the pieces the new branches branch on (a name
  character, a `NAME_LEADER`, a `\` run, `url(` or `url-prefix(`, a string, a comment, a
  digit, an astral character, a line break) and asserts, for each, that the output length
  equals the input length and that the recorded runs are ascending, disjoint, and inside the
  input.

## Open questions for the human
- Whether that belongs in `test/unit` or in the scaling check's `verify` hook, which already
  asserts exact expected text per size.

ℹ️ Nitpicks

  • docs/architecture.md:107 — "another value is rejected and the key dropped, so the CSS language service's own default for that rule applies" is false for the one key the plugin defaults: updateFromPluginConfig spreads ...defaults.lint before the normalized lint (src/configuration/plugin-configuration.ts:116), defaultConfiguration.lint is { emptyRules: 'ignore' }, and vscode-css-languageservice's own default for emptyRules is Warning (node_modules/vscode-css-languageservice/lib/umd/services/lintRules.js:44). A mistyped emptyRules therefore lands on ignore, not warning.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using Space Bunny (free) | 𝕏

Comment thread src/features/diagnostics.ts Outdated
Comment thread src/template/template-substitutions.ts Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The ; / } stop that fixes the one-line property-name case also fires inside comments and strings, so a selector interpolation with a comment holding a ; is now read as a property name and reports false diagnostics. 33 such templates where 1.0.1 is silent, inline below.

Reviewed changes

Reviewed the one commit pushed since the last review (ebe3558, 16 files): the ; / } stop added to the placeholder position classifier, the rewritten unquoted-url() branch of replaceJavaScriptEscapes with its carried insideUrl state, case-insensitive lint-level acceptance with a log message that names the default in force, and the docs, changesets, and tests for all of it. The branch is green locally (format:check, lint, typecheck, 676 unit, 149 e2e), and I drove the plugin's own compare-release sensor against 1.0.1 to judge each change in the direction that matters.

  • A statement boundary ends the selector search — isSelectorScanStop is now shared by nextSpecial and createListContinuationScanner, so a ; or } between a placeholder's colon and the rule's { / & ends the declaration. padding-${s}: 0; &:hover { … }, ${p}: 0; &:hover { … }, and the }-then-& shape all keep the property-name fill, each with unit, e2e, and compare-release coverage; I confirmed 1.0.1 reported colon expected / semi-colon expected / } expected on every one of them. The one ;-then-{ and ;-then-& rows pin the new behavior rather than a tautology, and the new } stop is what makes &:focus { ${p}: 0 } &:hover { … } come out right.
  • A url token stays whole across an escape — the url branch no longer requires a trailing escaping backslash, so it now outranks the name branches, and the padding moved after the cooked text: url(a/\x41y.png) reads as url(a/A___y.png), url(x\u0041y.png) as url(xA____.png), and a line continuation as url(foo__bar.png). The look-back takes the previous run's answer across its cooked text unless that text holds whitespace, a quote, or a paren, which keeps the read linear. A specialist fuzzed 400k generated inputs: output length always equals input length, run spans always ascend and stay disjoint, and the new stand-in reproduces the cooked text's diagnostics in all 14 shapes it could construct where 4c5e8aa did not. The line continuation inside a url, which made 1.0.1 throw Debug Failure. False expression., is now clean on both LF and CRLF, and the new unit test asserts the following colr diagnostic's exact offset across both.
  • Lint levels in any letter case, and a log line that names the default — toLintLevel lowercases before the hasOwn lookup, so "Error", "OFF", and "Warn" are accepted silently, and a rejected value now says which default actually applies (The plugin default, "ignore", applies. for a key in defaultConfiguration.lint, The CSS language service's own default for this rule applies. otherwise). normalizeLint takes defaults as a parameter instead of re-deriving the answer, which also retires the docs/architecture.md:107 nitpick from the last round: the spec, docs/usage.md, and both changesets now say the accurate thing.

ℹ️ Nitpicks

  • src/template/template-substitutions.ts:118 — the reworded SyntaxBoundaryScanner doc now runs the clause "where the next {, &, ;, }, or line terminator after it is (isSelectorScanStop). Each query scans lazily…" past the comment's right margin, so oxfmt leaves a mid-sentence wrap. Reflowing that one line is enough.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using Space Bunny (free) | 𝕏

Comment thread src/template/template-substitutions.ts
Comment thread src/virtual-document/javascript-escapes.ts Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No correctness blockers in this delta — three suggestions inline, in a release note, a comment trim, and a build script.

Reviewed changes

Reviewed the three commits pushed since the last pullfrog review (77bd1fa, 99bbdf9, 218f0d4; 16 files, +575/−471): the package-API gate scripts consolidated into two TypeScript entry points, the line-map module split out of the virtual-document provider, the top-level nesting mode added to the reuse rule and the validation cache key, and the hover span clamp. I ran the unit suite (681 passing) and the real-tsserver e2e suite (149 passing) on the branch, and proved the new tests can fail by stubbing isTemplateTopLevelNested to a constant: the provider test's two isFalse assertions and both @layer diagnostic orders go red.

  • Cache modes are isolated end to end — isTemplateTopLevelNested is a new public method on StyledVirtualDocumentProvider, canReuseVirtualDocument now requires the same mode, and buildValidationCacheKey writes it as one character after the length-prefixed wrapper, so a same-text component template and global-style template no longer share a validation entry, a parsed document, a line map, or a completion result. Every other consumer of the reuse rule (the completions cache, CachedVirtualDocumentSessionProvider, and the new getReusableLineMap fast path) inherits the fix through canReuseVirtualDocument rather than re-deriving it, and the key's length prefix still keeps (wrapper, mode, rawText) tuples from concatenating together.
  • The line map moves into its own module — src/virtual-document/template-line-map.ts takes the position-only helpers, positionsEqual, and the line-terminator constants; the provider keeps virtual-document construction and CSS scanning and re-exports the seven names its consumers import. The move also drops the provider's private positionsEqual, leaving completions.ts as the only second copy. VirtualPositionMapper keeps the split one-directional, so the new module depends on nothing that imports it.
  • A hover with no range no longer overruns the template — the fallback length is Math.min(1, context.text.length - start), which is 0 at the template end and 1 everywhere else; the anchored position is already clamped to the template end by fromVirtualDocPosition, so the length can never go negative.
  • Package-API gates consolidated — the inline node -e freshness check plus check-typescript-alias.mjs and ensure-node10-self-link.mjs become scripts/compile-unless-fresh.ts and scripts/test-package-api-types.ts, with one CHECKS entry per gate. I compared each deleted script against the new path: the alias check, the node10 self-link (including all four of its link states and both failure exits), the per-version tsc invocation, and the compile-first ordering are all still performed, with the same failure messages, and spawning process.execPath instead of shelling out to yarn compile is what makes the gate work on Windows.

One thread from the last round is half addressed; I replied on it with what I measured at 218f0d4 and left it open, since the value-reading collision is unchanged.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using Space Bunny (free) | 𝕏

Comment thread .changeset/diagnostics-and-mapping-fixes.md Outdated
Comment thread src/virtual-document/template-line-map.ts Outdated
Comment thread scripts/compile-unless-fresh.ts Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No correctness blockers in the refactor — two small coverage gaps inline.

Reviewed changes

Reviewed the one commit pushed since the last review (0abc58c, 88 files, +6661/−7501): a whole-branch "simplify" pass that extracts the shared CSS boundary scanner into src/virtual-document/css-code-scanner.ts, folds the mapping predicates into one span-based pair, routes code fixes through the diagnostics list, moves getTemplateSettings out of the tsserver entry, and rebuilds the CI, packaging, and test harnesses around one composite setup action.

  • The validation cache key now determines the stored result — getReadingKey returns ${isSingleIdentifier}${topLevelIsNested}${wrapper}, so a css single-identifier fragment and a component template with byte-identical text no longer share an entry. Re-measured with one StyledTemplateLanguageService on the text red: isolated css → 0 and styled.div → 2, and cached in both orders the pairs come back [0, 2] and [2, 0]. This closes the thread from the last round.
  • One shared boundary scanner for every structural reader — nonCodeEnd, createCodeLookback, createScanCandidatePattern, and the character predicates move into css-code-scanner.ts, and the four modules that had each grown their own copy now import from there. Two specialists fuzzed the result against 218f0d4: replaceJavaScriptEscapes is byte-identical over roughly 8M inputs apart from one documented change, and getTemplateSubstitutions is identical over 920k pairs apart from the four documented fill changes, with output length equal to input length and run spans ascending and disjoint throughout. It also removes two pre-existing ./api bugs, a length violation and a RangeError from an empty span.
  • Name characters now match vscode-css-languageservice's _identChar — every code unit ≥ U+0080 except U+2028/U+2029, so a non-ASCII space joins a name where it used to split one. I checked the spec's claim against node_modules/vscode-css-languageservice/lib/esm/parser/cssScanner.js:571; the spec states the exception and the reason for it.
  • Mapping is span-based end to end — fromVirtualDocSpan / fromVirtualDocSpanStrict replace the range-returning pair, and folding, hover, diagnostics, and code fixes all take template offsets from them instead of round-tripping through TemplateContext.toOffset, which retires the last per-result toOffset calls on the interactive paths.
  • Code fixes read the shown diagnostics — CodeActionsFeature now takes DiagnosticsFeature, so a request is answered from the validation cache, covers only diagnostics the editor can see, and honors validate: false.
  • Tooling, CI, and harnesses rebuilt — prepack is yarn compile and scripts/compile-unless-fresh.ts is gone; the seven test:package-api:* scripts collapse into test:package-api:consumers; one .github/actions/setup action replaces five copies of the setup-node/Corepack/Yarn preamble; the retained-heap guardrail runs in process instead of in an untimed spawnSync child; and the e2e suite shares one tsserver per suite file, which takes a full run from about two minutes to thirteen seconds. The rewritten scenarios kept exact-value assertions and a positive control in every "returns nothing" test, so the 2266 deleted lines of test code bought 10 more tests rather than looser ones.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using Space Bunny (free) | 𝕏

Comment thread test/unit/javascript-escapes.test.ts
Comment thread src/features/code-actions.ts

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The release workflow can publish without full verification, and two resilience paths still allow exceptions to escape.

Review effort: Lite
Findings: 1 High severity · 1 Medium severity

Open (2)

Comment thread .github/workflows/release.yml Outdated
Comment thread src/tsserver/tsserver-plugin.ts Outdated
@pullfrog

pullfrog Bot commented Sep 27, 2026

Copy link
Copy Markdown

Run failed. View the logs →

Pullfrog  | Rerun failed job ➔ | View workflow run | via Pullfrog | Using Space Bunny (free) | 𝕏

quantizor and others added 4 commits September 27, 2026 14:55
…ed CI and release pipeline

Package builds as a CommonJS tsserver entry at 1.0.1's path plus a dual CommonJS and ESM ./api.
CI builds and packs once, then runs the e2e matrix, the Node floor smoke test, and the scaling
guardrail; releases run the full verify before Changesets can publish. Every test and measurement
tool runs under a wall-clock deadline and a heap cap.

Co-authored-by: usercao <cao858646882@qq.com>
…working styled-components code

Reorganizes the plugin into configuration, template substitution, virtual document, tsserver,
and one-file-per-feature modules while keeping 1.0.1's public constructor, mapping contracts,
and TypeScript 5.0 floor. Interpolations in property names, at-rule conditions, selectors, and
keyframe percentages are read by their role, comments and strings no longer mislead that
reading, and JavaScript escapes are checked as styled-components receives them. Adapts to
styled-components v7 templates, maps positions exactly, caches validation within a size bound,
and keeps the common path free of edge-case work.

Co-authored-by: usercao <cao858646882@qq.com>
…d scaling behavior

A shared tsserver per suite file runs every scenario across the supported TypeScript range;
package tests consume the packed tarball from CommonJS and ESM and type-check consumers on
TypeScript 5.0 through the current release; the scaling check guards superlinear time and
retained memory.

Co-authored-by: usercao <cao858646882@qq.com>
…add AGENTS.md

Co-authored-by: usercao <cao858646882@qq.com>
@quantizor
quantizor force-pushed the chore/modernize-project branch from b33fb9f to d8fc730 Compare September 27, 2026 18:58
@quantizor

Copy link
Copy Markdown
Collaborator

Alright, I think this is ready to roll. Waiting for the final review.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

One or more issues must be addressed before approval.

Review effort: Lite
Findings: 1 Medium severity

Open (1)
Resolved since last review (2)

Comment thread test/e2e/tsserver-fixture/timeouts.ts Outdated
A delay above 2147483647 ms makes Node fire the timer after 1 ms, so an oversized
TSSERVER_RESPONSE_TIMEOUT_MS or WITH_DEADLINE_MS failed the run at once instead of extending it.
@quantizor
quantizor merged commit f499414 into styled-components:main Sep 27, 2026
11 checks passed
@quantizor

Copy link
Copy Markdown
Collaborator

Thank you so much @usercao :) a prerelease is available for you test: https://github.com/styled-components/typescript-styled-plugin/releases/tag/%40styled%2Ftypescript-styled-plugin%401.1.0-prerelease-20260927193731

@pullfrog

pullfrog Bot commented Sep 27, 2026

Copy link
Copy Markdown

Run failed. View the logs →

Pullfrog  | Rerun failed job ➔ | View workflow run | via Pullfrog | Using Space Bunny (free) | 𝕏

@usercao

usercao commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator Author

@quantizor Thank you very much for all your hard work on this!

With this preview release available, I can now start working on the bugs in vscode-styled-components.

Thanks again for your time and effort!

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.

3 participants