Skip to content
7 changes: 7 additions & 0 deletions .changeset/quiet-tags-overlay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@styled/typescript-styled-plugin': minor
---

Add an editor configuration protocol so editor integrations can append tagged-template names without replacing project tags, validation, lint, or Emmet settings.

Keep dynamic configuration consistent across project reloads, remove configuration listeners when projects close, refresh diagnostics once per project service when effective settings change, and avoid clearing caches or refreshing diagnostics for no-op updates.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ The plugin can be loaded in two ways:

The available loading modes depend on the editor integration below.

Adapter authors can use the [editor integration protocol](docs/editor-integration.md)
to append tag names at runtime while preserving project settings. The guide
covers payload validation, configuration replay, and packaged-adapter checks.

### With VS Code

**Loading mode: editor-global, with a project-local alternative.**
Expand Down
9 changes: 7 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,14 +143,19 @@ Spec for how the plugin turns a tsserver request inside a tagged template into a

## Configuration

- Source: `compilerOptions.plugins[]` entry at startup, then `configurePlugin` requests. `PluginConfigurationManager` (`src/configuration/plugin-configuration.ts`) normalizes any input shape and never throws, including when its logger throws.
- Source: `compilerOptions.plugins[]` entry at startup, then `configurePlugin` requests. Each project has an internal `ProjectConfigurationManager`, while the package-root factory shares one internal dynamic configuration store across project plugin instances in the same tsserver `ProjectService` (`src/tsserver/editor-configuration.ts`). The store records `none`, the last valid legacy replacement, or the last valid editor overlay, so a project created later in the same session inherits the same dynamic state as existing projects. A `WeakMap` isolates separate embedded hosts in one process. The managers build on the public `PluginConfigurationManager` normalization and never let an exception escape into tsserver.
- Normalization per field:
- `tags`: a list of strings, or one string as a one-item list. Anything else is rejected.
- `validate`: a boolean, or the string `"true"` or `"false"`. Anything else is rejected.
- `lint`, `emmet`: an object; anything else is rejected. Inside `lint`, a level setting (every key of `StyledPluginLintConfiguration` except `validProperties`) takes `"ignore"`, `"warning"`, or `"error"`, plus the aliases `"off"` (as `"ignore"`) and `"warn"` (as `"warning"`), each in any letter case (`"Error"` as `"error"`); another value is rejected and the key dropped, so a key with a plugin default (`defaultConfiguration.lint`, such as `emptyRules`) keeps that default, and any other key gets the CSS language service's own default for that rule. The log message names the plugin default where one applies. `validProperties` takes a list, keeping its string entries and rejecting the rest, or one string as a one-item list. Unknown `lint` keys and every `emmet` key pass through unchanged.
- A rejected value falls back to its default and logs one message to the tsserver log naming the setting, the accepted form, and the value received. An accepted alias or letter-case variant logs nothing, and neither does an absent field or a `null` or `undefined` top-level configuration. A field set to `null` is rejected.
- A top-level key that is not one of the fields above is ignored and logs one message naming it and those fields, plus a "Did you mean" suggestion: the setting nearest by edit distance with letter case ignored, when that distance is at most 40% of the key's length (at least 1), so `tag` suggests `tags` and `validation` suggests `validate`. The keys tsserver adds to the configuration it passes (`name` and `global`, docs/tsserver-host.md) log nothing.
- Each update replaces the whole configuration (fields absent from an update return to defaults), so a `configurePlugin` request replaces the `tsconfig.json` settings rather than merging into them, as in 1.0.1.
- A normal `configurePlugin` update keeps the 1.0.1 behavior: it becomes the complete effective runtime configuration, fields absent from the update return to defaults, and any editor overlay is deactivated. The shared store broadcasts the normalized legacy snapshot to every live manager, including managers belonging to older decorators retained after a project reload.
- A plain object with its own `editorConfiguration` key is instead an editor overlay envelope. Its `editorConfiguration.extraTags` field must be an array. String entries are trimmed, empty strings and non-string entries are dropped, and the rest are stably deduplicated with no count or length limit. A non-empty input array from which every entry is dropped is invalid and preserves the current dynamic state; only the literal empty array clears the overlay. A valid normalized list replaces the previous overlay and is appended after each project's base tags with stable deduplication. It cannot change `validate`, `lint`, or `emmet`. An invalid envelope preserves the last valid dynamic state for the current tsserver session and never falls back to normal replacement behavior. A host that persists configuration must keep and replay its last valid payload, since a new process cannot recover an invalid payload's predecessor from old process memory.
- Legacy runtime replacement and editor overlay are mutually exclusive dynamic states. A valid editor envelope, including `extraTags: []`, deactivates the legacy replacement, restores each project's original base, and appends the overlay. This last-kind-wins rule makes the effective state reproducible when a host restarts tsserver and replays its last valid plugin configuration.
- `TsServerStyledPlugin.create` reads the project's base from the matching `compilerOptions.plugins[]` entry. This preserves it when tsserver puts a dynamic override in `PluginCreateInfo.config` after a pre-project `configurePlugin` request. A separately constructed `PluginCreateInfo.config` with the same effective plugin settings as that project entry is still the project base; tsserver's injected `name` and `global` fields do not make it a legacy replacement. Only different effective settings become shared legacy state. Each created language service has a separate base manager. The shared store keeps live managers in a `Set` and wraps each project's `close()` once to remove all its managers, including those retained after reloads, before calling the original close method.
- Effective configuration changes notify the language service through the existing `onUpdatedConfig` path. After all live managers receive the configuration, including older decorators retained after a reload, the shared store calls `refreshDiagnostics()` on one live project. This sends one event for all open files in that `ProjectService`. A failure while applying configuration to one manager is logged and does not stop the others from updating. Repeating the same normalized overlay, sending an initial overlay that leaves the project configuration unchanged, an invalid envelope, or an equivalent legacy snapshot does not notify, clear caches, or refresh diagnostics.
- Editor configuration parsing and tag merging are linear in the number of base and additional tags and run only when configuration changes.
- Defaults (tags, lint levels) live in `PluginConfigurationManager.defaultConfiguration`; user-facing documentation in `docs/usage.md`.

## Public surface
Expand Down
Loading