- Notifications
You must be signed in to change notification settings - Fork 0
Define the @tecode/api type surface and layering lint rule (Task 1.2)#41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Uh oh!
There was an error while loading. Please reload this page.
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| /** | ||
| * The document/text-buffer surface (Req 5, design.md §7). | ||
| */ | ||
| import type { Event, TextEdit, Uri } from "./primitives"; | ||
| /** Line-ending style. Detected on load (first occurrence wins, default | ||
| * `"\n"`) and preserved on save (Req 5.1). */ | ||
| export type Eol = "\n" | "\r\n"; | ||
| /** | ||
| * Fired on `Document.onDidChange` after a call to `applyEdits` completes. | ||
| * Carries the edits that were applied (in the form actually committed, i.e. | ||
| * already validated) and the document's new version, so listeners such as | ||
| * the syntax-highlight service can apply incremental updates instead of | ||
| * re-scanning the whole buffer (design.md §10). | ||
| */ | ||
| export interface DocumentChangeEvent { | ||
| document: Document; | ||
| edits: TextEdit[]; | ||
| version: number; | ||
| } | ||
| /** | ||
| * A single open text document. Every open file (and unsaved/untitled | ||
| * buffer) is represented as one `Document`, synchronized to the renderer by | ||
| * the core (Req 5.1). | ||
| * | ||
| * `applyEdits` is the *only* mutation path (Req 5.2) — there is no | ||
| * `setText`, no direct buffer access, and no other way to change a | ||
| * document's content. This keeps undo/redo and change notification | ||
| * centralized in the core. | ||
| */ | ||
| export interface Document { | ||
| /** The document's resource identifier. */ | ||
| uri: Uri; | ||
| /** The language ID resolved for this document (e.g. `"typescript"`, | ||
| * `"plaintext"` when no language matches — Req 8.3). */ | ||
| languageId: string; | ||
| /** Monotonically increasing version number, bumped on every applied | ||
| * edit. */ | ||
| version: number; | ||
| /** Whether the document has unsaved changes. */ | ||
| dirty: boolean; | ||
| /** Whether the document rejects edits (e.g. files over 10 MB are opened | ||
| * read-only — Req 5.5). */ | ||
| readonly: boolean; | ||
| /** Line-ending style used when saving this document. */ | ||
| eol: Eol; | ||
| /** | ||
| * Apply one or more edits atomically. This is the only way to modify a | ||
| * document's content (Req 5.2). Edits are validated, applied bottom-up, | ||
| * and recorded on the undo stack; the call bumps `version` and fires | ||
| * exactly one `onDidChange` event. On a `readonly` document this | ||
| * surfaces a status-bar error and does nothing (design.md §14). | ||
| */ | ||
| applyEdits(edits: TextEdit[]): void; | ||
| /** | ||
| * Group every `applyEdits` call made inside `fn` into a single undo step | ||
| * (Req 5.4), so extensions can perform multi-step edits (e.g. "toggle | ||
| * comment on N lines") that undo/redo as one operation. | ||
| */ | ||
| transaction(fn: () => void): void; | ||
| /** Fired after each `applyEdits` call completes (Req 5.3). */ | ||
| onDidChange: Event<DocumentChangeEvent>; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,10 @@ | ||
| import { expect, test } from "bun:test"; | ||
| import { API_PLACEHOLDER } from "./index"; | ||
| import { API_VERSION } from "./index"; | ||
| test("placeholder", () => { | ||
| expect(API_PLACEHOLDER).toBe(true); | ||
| test("API_VERSION is the current major.minor version", () => { | ||
| expect(API_VERSION).toBe("1.0"); | ||
| }); | ||
| test("API_VERSION uses <major>.<minor> form", () => { | ||
| expect(API_VERSION).toMatch(/^\d+\.\d+$/); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,91 @@ | ||
| // Placeholder public API surface for @tecode/api. | ||
| // The real type declarations (Manifest, ExtensionContext, namespace | ||
| // interfaces, API_VERSION, etc.) are added in a later task. | ||
| export const API_PLACEHOLDER = true; | ||
| /** | ||
| * `@tecode/api` — the complete public type surface extensions are written | ||
| * against. This package has no dependencies and contains no runtime | ||
| * behavior beyond {@link API_VERSION} (Req 1.3, design.md §2): `core` | ||
| * implements these types, `builtin` and third-party extensions import only | ||
| * from here, and neither imports from `core` directly. | ||
| */ | ||
| export type { | ||
| Position, | ||
| Range, | ||
| Selection, | ||
| TextEdit, | ||
| Uri, | ||
| Disposable, | ||
| Listener, | ||
| Event, | ||
| } from "./primitives"; | ||
| export type { Eol, Document, DocumentChangeEvent } from "./document"; | ||
| export type { | ||
| UiColorKey, | ||
| BaseCaptureName, | ||
| CaptureName, | ||
| RGB, | ||
| Style, | ||
| ResolvedTheme, | ||
| ThemeContribution, | ||
| } from "./theme"; | ||
| export type { | ||
| ActivationEvent, | ||
| CommandMeta, | ||
| CommandContribution, | ||
| KeybindingContribution, | ||
| ViewSlot, | ||
| ViewContribution, | ||
| LanguageComments, | ||
| BracketPair, | ||
| LanguageContribution, | ||
| ConfigurationPropertySchema, | ||
| ConfigurationContribution, | ||
| Contributes, | ||
| Manifest, | ||
| ExtensionContext, | ||
| } from "./manifest"; | ||
| export type { | ||
| CommandHandler, | ||
| CommandDescriptor, | ||
| CommandsNamespace, | ||
| FileType, | ||
| FileStat, | ||
| DirEntry, | ||
| FileChangeType, | ||
| FileChangeEvent, | ||
| FileSystem, | ||
| WorkspaceNamespace, | ||
| MessageKind, | ||
| QuickPickItem, | ||
| QuickPickOptions, | ||
| InputBoxOptions, | ||
| StatusBarItem, | ||
| Editor, | ||
| WindowNamespace, | ||
| EditorNamespace, | ||
| SlotId, | ||
| ComponentType, | ||
| UiNamespace, | ||
| ConfigChangeEvent, | ||
| ConfigNamespace, | ||
| ContextNamespace, | ||
| LanguagesNamespace, | ||
| ThemesNamespace, | ||
| Tecode, | ||
| } from "./namespaces"; | ||
| /** | ||
| * The `@tecode/api` version, as `"<major>.<minor>"` (design.md §4.3). This | ||
| * is the package's only runtime code — everything else is type-only. | ||
| * | ||
| * **Compatibility rule** (Req 2.7): a manifest declares the API version it | ||
| * targets as `apiVersion: "<major>"` or `"<major>.<minor>"`. An extension | ||
| * is compatible with the running host when the major versions match and | ||
| * the host's minor version is greater than or equal to the extension's | ||
| * requested minor version (an omitted minor is treated as `0`). An | ||
| * incompatible extension is skipped at registration with a surfaced error | ||
| * rather than crashing the host. | ||
| */ | ||
| export const API_VERSION = "1.0"; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| /** | ||
| * Verifies the `no-restricted-imports` layering rule in the root | ||
| * `eslint.config.mjs` (Req 1.3, design.md §2): `packages/builtin/**` may | ||
| * not import `@tecode/core` directly, while `packages/cli/**` — the one | ||
| * place wiring core together is legitimate — is exempt. | ||
| * | ||
| * These fixtures are written to temp locations under the real packages | ||
| * (ESLint's flat-config `files`/`ignores` globs match on path, so the | ||
| * fixture has to actually live under `packages/builtin` or | ||
| * `packages/cli/src`) and are always removed again in a `finally`, so a | ||
| * failed assertion never leaves a permanently-lint-breaking file behind. | ||
| */ | ||
| import { afterEach, expect, setDefaultTimeout, test } from "bun:test"; | ||
| import { mkdir, rm, writeFile } from "node:fs/promises"; | ||
| import path from "node:path"; | ||
| // Each test spawns a full `bunx eslint` run, which can exceed bun's 5 s | ||
| // default test timeout on a cold cache. | ||
| setDefaultTimeout(60_000); | ||
| const REPO_ROOT = path.resolve(import.meta.dir, "../../.."); | ||
| const BAD_DIR = path.join(REPO_ROOT, "packages/builtin/__lint-fixture__"); | ||
| const BAD_FILE = path.join(BAD_DIR, "bad.ts"); | ||
| const OK_FILE = path.join(REPO_ROOT, "packages/cli/src/__lint-fixture__.ts"); | ||
| async function cleanupFixtures(): Promise<void> { | ||
| await rm(BAD_DIR, { recursive: true, force: true }); | ||
| await rm(OK_FILE, { force: true }); | ||
| } | ||
| // Belt-and-suspenders: also clean up after the suite in case a test is | ||
| // interrupted between its own try/finally and completion. | ||
| afterEach(cleanupFixtures); | ||
goofmint marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| interface EslintMessage { | ||
| ruleId: string | null; | ||
| } | ||
| interface EslintFileResult { | ||
| messages: EslintMessage[]; | ||
| } | ||
| async function runEslint( | ||
| relFile: string, | ||
| ): Promise<{ exitCode: number; results: EslintFileResult[] }> { | ||
| const proc = Bun.spawn({ | ||
| cmd: ["bunx", "eslint", "--no-ignore", "-f", "json", relFile], | ||
| cwd: REPO_ROOT, | ||
| stdout: "pipe", | ||
| stderr: "pipe", | ||
| }); | ||
| // Consume both pipes: an unread stderr pipe can fill up and stall the | ||
| // child process. | ||
| const [stdout, stderr, exitCode] = await Promise.all([ | ||
| new Response(proc.stdout).text(), | ||
| new Response(proc.stderr).text(), | ||
| proc.exited, | ||
| ]); | ||
| if (stdout.trim().length === 0) { | ||
| // ESLint always emits a JSON array on a successful run (even with no | ||
| // findings), so an empty stdout means the process itself failed — | ||
| // e.g. a broken config (exit code 2). Fail loudly instead of letting | ||
| // callers mistake it for "no lint errors". | ||
| throw new Error( | ||
| `eslint produced no output for ${relFile} (exit ${exitCode}): ${stderr.trim()}`, | ||
| ); | ||
| } | ||
| const results = JSON.parse(stdout) as EslintFileResult[]; | ||
| return { exitCode, results }; | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| test("blocks @tecode/core imports from packages/builtin", async () => { | ||
| await cleanupFixtures(); | ||
| try { | ||
| await mkdir(BAD_DIR, { recursive: true }); | ||
| await writeFile(BAD_FILE, 'import "@tecode/core";\n'); | ||
| const { exitCode, results } = await runEslint( | ||
| "packages/builtin/__lint-fixture__/bad.ts", | ||
| ); | ||
| const ruleIds = results.flatMap((r) => r.messages.map((m) => m.ruleId)); | ||
| expect(exitCode).not.toBe(0); | ||
| expect(ruleIds).toContain("no-restricted-imports"); | ||
| } finally { | ||
| await cleanupFixtures(); | ||
| } | ||
| }); | ||
| test("allows @tecode/core imports from packages/cli", async () => { | ||
| await cleanupFixtures(); | ||
| try { | ||
| await mkdir(path.dirname(OK_FILE), { recursive: true }); | ||
| await writeFile(OK_FILE, 'import "@tecode/core";\n'); | ||
| const { exitCode, results } = await runEslint( | ||
| "packages/cli/src/__lint-fixture__.ts", | ||
| ); | ||
| const ruleIds = results.flatMap((r) => r.messages.map((m) => m.ruleId)); | ||
| // Exit 0 also guards against a broken ESLint config (exit 2) being | ||
| // mistaken for "no findings". | ||
| expect(exitCode).toBe(0); | ||
| expect(ruleIds).not.toContain("no-restricted-imports"); | ||
| } finally { | ||
| await cleanupFixtures(); | ||
| } | ||
| }); | ||
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.