- Notifications
You must be signed in to change notification settings - Fork 0
Assemble the tecode API object and wire the "tecode" module alias#52
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| name: CI | ||
| on: | ||
| push: | ||
| branches: [main] | ||
| pull_request: | ||
| permissions: | ||
| contents: read | ||
| jobs: | ||
| test: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| # CI only reads the tree — never persist the token into the local | ||
| # git config for later steps. | ||
| persist-credentials: false | ||
| - uses: oven-sh/setup-bun@v2 | ||
| with: | ||
| bun-version: latest | ||
| - run: bun install --frozen-lockfile | ||
| - name: contract-tests | ||
| run: bun test | ||
| - name: lint | ||
| run: bun run lint | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -8,6 +8,7 @@ | ||
| "tecode": "src/main.ts" | ||
| }, | ||
| "dependencies": { | ||
| "@tecode/api": "workspace:*", | ||
| "@tecode/core": "workspace:*" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,110 @@ | ||
| import pkg from "../package.json"; | ||
| import type { FileSystem, Tecode } from "@tecode/api"; | ||
| import { | ||
| createCommandRegistry, | ||
| createConfigService, | ||
| createContextService, | ||
| createDocumentManager, | ||
| createFileSystem, | ||
| createHostLog, | ||
| createNoopStatusSink, | ||
| createTecodeApi, | ||
| pathToUri, | ||
| registerTecodeAlias, | ||
| type CommandRegistry, | ||
| type ConfigService, | ||
| type ContextService, | ||
| type DocumentManager, | ||
| type HostLog, | ||
| type StatusSink, | ||
| } from "@tecode/core"; | ||
| /** | ||
| * Every core service {@link buildAssemblyRoot} wires together, plus the | ||
| * assembled `tecode` object itself — returned so a caller (currently just | ||
| * this module's own `main`; Task 1.15's startup sequence next) can hold | ||
| * onto `config` for `ready`/`dispose()` without reaching back into the | ||
| * module's internals. | ||
| */ | ||
| export interface AssemblyRoot { | ||
| log: HostLog; | ||
| sink: StatusSink; | ||
| commands: CommandRegistry; | ||
| documents: DocumentManager; | ||
| fs: FileSystem; | ||
| config: ConfigService; | ||
| context: ContextService; | ||
| api: Tecode; | ||
| } | ||
| /** | ||
| * Build the `tecode` composition root and register the `"tecode"` module | ||
| * alias (Req 10.1, 10.2; design.md §12, §17; Task 1.13's "Bun module alias | ||
| * registration" note). `packages/cli` is the one place allowed to import | ||
| * `@tecode/core` directly (`eslint.config.mjs`'s layering rule) — this | ||
| * function is that wiring. | ||
| * | ||
| * **This is deliberately a small slice of design.md §17's full startup | ||
| * sequence**, not that sequence itself: argv parsing (file vs. directory), | ||
| * the sync-before-first-frame phase, rendering the UI shell, deferred | ||
| * extension discovery/activation, the initial file open, and startup-timing | ||
| * instrumentation are all Task 1.15's job. That task should *call* this | ||
| * function (or extend it) rather than duplicate its ordering — the one | ||
| * invariant it establishes and Task 1.15 must preserve is | ||
| * {@link registerTecodeAlias} running immediately after | ||
| * {@link createTecodeApi} and strictly before any extension module is | ||
| * imported (Req 1.4, design.md §2): an extension's `import ... from | ||
| * "tecode"` resolves only once the alias is registered. | ||
| * | ||
| * `workspaceRoot` defaults to `process.cwd()` as a placeholder for Task | ||
| * 1.15's real argv-driven file/directory resolution (design.md §17's | ||
| * "Argv parsing (file/directory)" step) — nothing here interprets `argv` | ||
| * yet. | ||
| */ | ||
| export function buildAssemblyRoot(workspaceRoot: string = process.cwd()): AssemblyRoot { | ||
| const log = createHostLog(); | ||
| // No UI shell exists yet (Task 1.14) to back a real StatusSink — matches | ||
| // every other core composition point that hasn't reached its UI task. | ||
| const sink = createNoopStatusSink(); | ||
| const commands = createCommandRegistry({ log, sink }); | ||
| const documents = createDocumentManager({ log, sink }); | ||
| const fs = createFileSystem({ log }); | ||
| const config = createConfigService({ log, sink, workspaceRoot }); | ||
| const context = createContextService(); | ||
| const api = createTecodeApi({ | ||
| commands, | ||
| documents, | ||
| fs, | ||
| rootUri: pathToUri(workspaceRoot), | ||
| config, | ||
| context, | ||
| sink, | ||
| }); | ||
| // Must run before any extension module is imported (see this function's | ||
| // TSDoc) — no extension loading exists yet (Task 1.15/2.x), so this is | ||
| // simply the last step here today. | ||
| registerTecodeAlias(api); | ||
| return { log, sink, commands, documents, fs, config, context, api }; | ||
| } | ||
| function main(argv: string[]): void { | ||
| if (argv.includes("--version")) { | ||
| console.log(pkg.version); | ||
| process.exit(0); | ||
| } | ||
| buildAssemblyRoot(); | ||
| } | ||
| main(process.argv.slice(2)); | ||
| // `import.meta.main` is Bun's "am I the entry point" check (true only when | ||
| // this file itself was executed, e.g. `bun run main.ts`; false when another | ||
| // module — such as this file's own test — imports it). Without this guard, | ||
| // importing `main.ts` for testing `buildAssemblyRoot` would also run | ||
| // `main(process.argv.slice(2))` as an unwanted side effect, against the | ||
| // *importing* process's real argv and real `HOME`. | ||
| if (import.meta.main) { | ||
| main(process.argv.slice(2)); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| /** | ||
| * `registerTecodeAlias`: makes `import ... from "tecode"` resolve at | ||
| * runtime (Req 10.1, design.md §2, §12; Task 1.13) using `Bun.plugin`'s | ||
| * virtual-module hook. Every extension is written against `@tecode/api`'s | ||
| * *types* but reaches the live implementation through the `"tecode"` | ||
| * module specifier (design.md §2) — this is the one place that binding is | ||
| * actually wired up, and it must run once, after {@link createTecodeApi} | ||
| * has built the object and *before* any extension module is imported | ||
| * (`cli/main.ts`'s startup wiring, Task 1.15, is the intended call site; | ||
| * `discovery.ts`'s manifest-only dynamic import runs before this and never | ||
| * touches `index.ts`, so ordering there is unaffected). | ||
| * | ||
| * **Static typing for `"tecode"`**: `Bun.plugin`'s `builder.module(...)` is | ||
| * a runtime-only hook — TypeScript has no way to see that the specifier | ||
| * `"tecode"` will resolve to anything without help. `api/tecode-module.d.ts` | ||
| * supplies that help with an ambient `declare module "tecode"` re-exporting | ||
| * each namespace's type from `@tecode/api`; that file's own TSDoc explains | ||
| * why it works across every package in one `bunx tsc --noEmit` run despite | ||
| * living in `core`. | ||
| * | ||
| * **Compiled-mode (`bun build --compile`) note**: `Bun.plugin` registration | ||
| * must still run before any extension module import inside the compiled | ||
| * binary's own entry point — nothing about this changes for a compiled | ||
| * build (`Bun.plugin` is a runtime call, not a bundler transform), but the | ||
| * *build entry file* (design.md §17's `scripts/release.ts`-driven build, | ||
| * not yet written) must be the one that calls | ||
| * {@link createTecodeApi}/{@link registerTecodeAlias}, exactly like | ||
| * `cli/main.ts` does in dev. No build script changes are needed for this | ||
| * task; this note exists so Task whichever-wires-`--compile` doesn't have | ||
| * to rediscover the constraint. | ||
| */ | ||
| import type { Tecode } from "@tecode/api"; | ||
| /** The `api` object most recently registered via {@link registerTecodeAlias} | ||
| * — tracked so a repeat call with the exact same object is a cheap no-op | ||
| * (idempotent) while a call with a genuinely different object (e.g. a test | ||
| * building a fresh composition root) still takes effect: `Bun.plugin` | ||
| * itself is fine with re-registering the same module specifier (last | ||
| * registration wins, verified empirically — it does not throw or warn), so | ||
| * there is no correctness reason to refuse that case, only a cheap | ||
| * optimization for the common one. */ | ||
| let registeredApi: Tecode | undefined; | ||
| /** | ||
| * Register the `"tecode"` virtual module so `import ... from "tecode"` | ||
| * resolves to `api`'s namespaces as named exports (`commands`, `workspace`, | ||
| * `window`, `editor`, `ui`, `config`, `context`, `languages`, `themes` — | ||
| * matching `Tecode`'s own shape, since `Bun.plugin`'s `loader: "object"` | ||
| * projects an object's own enumerable properties onto the module's named | ||
| * exports). Call this exactly once per `api` object, after | ||
| * {@link createTecodeApi} and before any extension module loads. | ||
| */ | ||
| export function registerTecodeAlias(api: Tecode): void { | ||
| if (registeredApi === api) return; | ||
| registeredApi = api; | ||
| Bun.plugin({ | ||
| name: "tecode-module-alias", | ||
| setup(builder) { | ||
| builder.module("tecode", () => ({ | ||
| // `OnLoadResultObject.exports` is typed `Record<string, unknown>` | ||
| // (an index signature `Tecode` deliberately does not declare — its | ||
| // nine namespaces are named, not open-ended). The cast is safe: | ||
| // `api`'s own enumerable properties genuinely are exactly what | ||
| // `tecode-module.d.ts`'s ambient declaration promises callers of | ||
| // `import ... from "tecode"`. | ||
| exports: api as unknown as Record<string, unknown>, | ||
| loader: "object", | ||
| })); | ||
| }, | ||
| }); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.