LanguageRunner (libraries/tools/src/components/language-runner/language-runner.ts) currently carries ~120 lines of .prettierrc-specific machinery for the Gitea #36 binding:
prettierStore injection + lastPrettierRcContent field (~10 lines)
Two effect() blocks for store↔file binding (~70 lines including comments)
All of this is dead weight for non-JavaScript playgrounds (C#, Python, future languages) — the inject still runs, the predicate is still in scope, and any future config-file UI (.eslintrc, tsconfig.json, etc.) would balloon LanguageRunner again.
The JS playground config provides [{ provide: SYNTHETIC_FILE_BINDINGS, multi: true, useClass: PrettierRcSyntheticFileBinding }]; other languages provide nothing and pay zero runtime cost.
The isSyntheticFile predicate stays in LanguageRunner (still used by template + guards), but the dotfile convention is now the only contract syntheticbindings need to honor — keeps the predicate generic.
Why now
Three triggers:
Future config files: .eslintrc is the next obvious extension; tsconfig.json for TypeScript playgrounds eventually; learner-editable manifest files for advanced lesson tracks.
Per-language opt-in cleanliness: Today's flag-based injectPrettierRc is a code smell — a feature owned by one language is wired through the universal config interface. DI multi-providers express the relationship more honestly.
Test isolation: The current .prettierrc binding is hard to unit-test in isolation because it's tangled with LanguageRunner's mode/sandbox-level orchestration. A SyntheticFileBinding interface lets us write a prettier-rc-binding.spec.ts that mounts only the binding against a stub host.
Severity
Yellow — refactor, not a bug fix. The current code works correctly (modulo #-canonicalize-on-parse). This is about keeping LanguageRunner from becoming a god-object as more synthetic files arrive.
Acceptance
LanguageRunner has no .prettierrc-specific code (the injectPrettierRc flag also goes away).
A SyntheticFileBinding interface + DI multi-provider seam exists.
The JS playground provides PrettierRcSyntheticFileBinding; existing tests in language-runner-prettierrc.spec.ts continue to pass against the new shape.
A new unit test mounts the binding against a stub host without booting LanguageRunner.
## Surface
`LanguageRunner` (`libraries/tools/src/components/language-runner/language-runner.ts`) currently carries ~120 lines of `.prettierrc`-specific machinery for the Gitea #36 binding:
- `prettierStore` injection + `lastPrettierRcContent` field (~10 lines)
- Two `effect()` blocks for store↔file binding (~70 lines including comments)
- `isSyntheticFile()` predicate (~3 lines + JSDoc)
- `upsertPrettierRcFile()` private method (~25 lines + JSDoc)
- Synthetic-file guards in `startRename`, `deleteFile`, `setEntryFile` (~3 sites × ~5 lines)
- Dotfile filter in `snapshotFiles()` (~2 lines)
All of this is dead weight for non-JavaScript playgrounds (C#, Python, future languages) — the inject still runs, the predicate is still in scope, and any future config-file UI (`.eslintrc`, `tsconfig.json`, etc.) would balloon `LanguageRunner` again.
## Suggested shape
```ts
@Injectable()
export class PrettierRcSyntheticFileBinding implements SyntheticFileBinding {
readonly path = '.prettierrc';
constructor(private readonly store: PrettierConfigStore) {}
install(host: SyntheticFileHost): Disposable { /* effects + upsert */ }
}
export interface SyntheticFileHost {
readonly mode: Signal<'sandbox' | 'lesson'>;
readonly effectiveSandboxLevel: Signal<'beginner' | 'advanced'>;
readonly files: WritableSignal<VirtualFile[]>;
persistFilesProject(): void;
}
```
`LanguageRunner` becomes:
```ts
private readonly syntheticBindings = inject(SYNTHETIC_FILE_BINDINGS, { optional: true }) ?? [];
ngOnInit() {
for (const binding of this.syntheticBindings) {
binding.install(this).pipe(takeUntilDestroyed()).subscribe();
}
}
```
The JS playground config provides `[{ provide: SYNTHETIC_FILE_BINDINGS, multi: true, useClass: PrettierRcSyntheticFileBinding }]`; other languages provide nothing and pay zero runtime cost.
The `isSyntheticFile` predicate stays in `LanguageRunner` (still used by template + guards), but the dotfile convention is now the only contract syntheticbindings need to honor — keeps the predicate generic.
## Why now
Three triggers:
1. **Future config files**: `.eslintrc` is the next obvious extension; `tsconfig.json` for TypeScript playgrounds eventually; learner-editable manifest files for advanced lesson tracks.
2. **Per-language opt-in cleanliness**: Today's flag-based `injectPrettierRc` is a code smell — a feature owned by one language is wired through the universal config interface. DI multi-providers express the relationship more honestly.
3. **Test isolation**: The current `.prettierrc` binding is hard to unit-test in isolation because it's tangled with `LanguageRunner`'s mode/sandbox-level orchestration. A `SyntheticFileBinding` interface lets us write a `prettier-rc-binding.spec.ts` that mounts only the binding against a stub host.
## Severity
Yellow — refactor, not a bug fix. The current code works correctly (modulo #-canonicalize-on-parse). This is about keeping `LanguageRunner` from becoming a god-object as more synthetic files arrive.
## Acceptance
- `LanguageRunner` has no `.prettierrc`-specific code (the `injectPrettierRc` flag also goes away).
- A `SyntheticFileBinding` interface + DI multi-provider seam exists.
- The JS playground provides `PrettierRcSyntheticFileBinding`; existing tests in `language-runner-prettierrc.spec.ts` continue to pass against the new shape.
- A new unit test mounts the binding against a stub host without booting `LanguageRunner`.
Done — shipped in spikersoft-angular PR #59 (merged to master as beaba4b).
All acceptance criteria met:
LanguageRunner now carries zero .prettierrc-specific code; the injectPrettierRc config flag is removed.
New SyntheticFileBinding / SyntheticFileHost interfaces + SYNTHETIC_FILE_BINDINGS multi-provider token in platform/language-runner.
PrettierRcSyntheticFileBinding holds the bidirectional store↔file binding (including the #80 dynamic-dependency note and the #81 canonicalize-under-cursor fix); the JS shell opts in via { provide: SYNTHETIC_FILE_BINDINGS, useClass: PrettierRcSyntheticFileBinding, multi: true }. Other playgrounds provide nothing.
The generic dotfile isSyntheticFile predicate, the startRename/deleteFile/setEntryFile guards, and the snapshotFiles() filter stay in LanguageRunner.
language-runner-prettierrc.spec.ts updated to the new shape and stays green; a new prettier-rc-synthetic-file-binding.spec.ts mounts the binding in isolation against a stub host.
Deviation from the sketch:install(host) returns a { destroy() } disposer rather than an Observable/takeUntilDestroyed() flow — teardown maps directly onto Angular EffectRefs without an rxjs round-trip.
Verified: platform-language-runner 153 tests passing (incl. the #81 cases ported into the binding), feature-dev-tools-javascript-runner green, full workspace lint green.
Done — shipped in spikersoft-angular PR #59 (merged to `master` as `beaba4b`).
**All acceptance criteria met:**
- `LanguageRunner` now carries zero `.prettierrc`-specific code; the `injectPrettierRc` config flag is removed.
- New `SyntheticFileBinding` / `SyntheticFileHost` interfaces + `SYNTHETIC_FILE_BINDINGS` multi-provider token in `platform/language-runner`.
- `PrettierRcSyntheticFileBinding` holds the bidirectional store↔file binding (including the #80 dynamic-dependency note and the #81 canonicalize-under-cursor fix); the JS shell opts in via `{ provide: SYNTHETIC_FILE_BINDINGS, useClass: PrettierRcSyntheticFileBinding, multi: true }`. Other playgrounds provide nothing.
- The generic dotfile `isSyntheticFile` predicate, the `startRename`/`deleteFile`/`setEntryFile` guards, and the `snapshotFiles()` filter stay in `LanguageRunner`.
- `language-runner-prettierrc.spec.ts` updated to the new shape and stays green; a new `prettier-rc-synthetic-file-binding.spec.ts` mounts the binding in isolation against a stub host.
**Deviation from the sketch:** `install(host)` returns a `{ destroy() }` disposer rather than an `Observable`/`takeUntilDestroyed()` flow — teardown maps directly onto Angular `EffectRef`s without an rxjs round-trip.
Verified: `platform-language-runner` 153 tests passing (incl. the #81 cases ported into the binding), `feature-dev-tools-javascript-runner` green, full workspace lint green.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Surface
LanguageRunner(libraries/tools/src/components/language-runner/language-runner.ts) currently carries ~120 lines of.prettierrc-specific machinery for the Gitea #36 binding:prettierStoreinjection +lastPrettierRcContentfield (~10 lines)effect()blocks for store↔file binding (~70 lines including comments)isSyntheticFile()predicate (~3 lines + JSDoc)upsertPrettierRcFile()private method (~25 lines + JSDoc)startRename,deleteFile,setEntryFile(~3 sites × ~5 lines)snapshotFiles()(~2 lines)All of this is dead weight for non-JavaScript playgrounds (C#, Python, future languages) — the inject still runs, the predicate is still in scope, and any future config-file UI (
.eslintrc,tsconfig.json, etc.) would balloonLanguageRunneragain.Suggested shape
LanguageRunnerbecomes:The JS playground config provides
[{ provide: SYNTHETIC_FILE_BINDINGS, multi: true, useClass: PrettierRcSyntheticFileBinding }]; other languages provide nothing and pay zero runtime cost.The
isSyntheticFilepredicate stays inLanguageRunner(still used by template + guards), but the dotfile convention is now the only contract syntheticbindings need to honor — keeps the predicate generic.Why now
Three triggers:
.eslintrcis the next obvious extension;tsconfig.jsonfor TypeScript playgrounds eventually; learner-editable manifest files for advanced lesson tracks.injectPrettierRcis a code smell — a feature owned by one language is wired through the universal config interface. DI multi-providers express the relationship more honestly..prettierrcbinding is hard to unit-test in isolation because it's tangled withLanguageRunner's mode/sandbox-level orchestration. ASyntheticFileBindinginterface lets us write aprettier-rc-binding.spec.tsthat mounts only the binding against a stub host.Severity
Yellow — refactor, not a bug fix. The current code works correctly (modulo #-canonicalize-on-parse). This is about keeping
LanguageRunnerfrom becoming a god-object as more synthetic files arrive.Acceptance
LanguageRunnerhas no.prettierrc-specific code (theinjectPrettierRcflag also goes away).SyntheticFileBindinginterface + DI multi-provider seam exists.PrettierRcSyntheticFileBinding; existing tests inlanguage-runner-prettierrc.spec.tscontinue to pass against the new shape.LanguageRunner.Done — shipped in spikersoft-angular PR #59 (merged to
masterasbeaba4b).All acceptance criteria met:
LanguageRunnernow carries zero.prettierrc-specific code; theinjectPrettierRcconfig flag is removed.SyntheticFileBinding/SyntheticFileHostinterfaces +SYNTHETIC_FILE_BINDINGSmulti-provider token inplatform/language-runner.PrettierRcSyntheticFileBindingholds the bidirectional store↔file binding (including the #80 dynamic-dependency note and the #81 canonicalize-under-cursor fix); the JS shell opts in via{ provide: SYNTHETIC_FILE_BINDINGS, useClass: PrettierRcSyntheticFileBinding, multi: true }. Other playgrounds provide nothing.isSyntheticFilepredicate, thestartRename/deleteFile/setEntryFileguards, and thesnapshotFiles()filter stay inLanguageRunner.language-runner-prettierrc.spec.tsupdated to the new shape and stays green; a newprettier-rc-synthetic-file-binding.spec.tsmounts the binding in isolation against a stub host.Deviation from the sketch:
install(host)returns a{ destroy() }disposer rather than anObservable/takeUntilDestroyed()flow — teardown maps directly onto AngularEffectRefs without an rxjs round-trip.Verified:
platform-language-runner153 tests passing (incl. the #81 cases ported into the binding),feature-dev-tools-javascript-runnergreen, full workspace lint green.