diff --git a/.claude/rules/extensions/install.md b/.claude/rules/extensions/install.md new file mode 100644 index 00000000000..940d747d0f5 --- /dev/null +++ b/.claude/rules/extensions/install.md @@ -0,0 +1,16 @@ +--- +paths: + - "src/extension/**" + - "src/command/add/**" + - "src/command/update/**" + - "src/command/remove/**" + - "src/command/use/**" + - "tests/smoke/extensions/install.test.ts" + - "tests/smoke/use/**" +--- + +# Extension installation + +For how extensions are staged, namespaced and installed under `_extensions/`, how ids are looked up, and which behaviors the tests pin, see `llm-docs/extension-install-architecture.md`. + +The namespace of an installed extension comes from where it is installed from (the GitHub owner, or none for local paths and archives), not from an owner folder in the source. This is deliberate: it keeps a fork or copy from overwriting another owner's extension. Read the doc's "The namespace rule" section before changing install layout or lookup. diff --git a/llm-docs/extension-install-architecture.md b/llm-docs/extension-install-architecture.md new file mode 100644 index 00000000000..af9da1b58f9 --- /dev/null +++ b/llm-docs/extension-install-architecture.md @@ -0,0 +1,68 @@ +--- +main_commit: 4c7b5d25d +analyzed_date: 2026-10-19 +key_files: + - src/extension/install.ts + - src/extension/extension.ts + - src/extension/extension-host.ts + - src/extension/remove.ts + - src/command/add/cmd.ts + - src/command/update/cmd.ts + - src/command/remove/cmd.ts + - src/command/use/commands/template.ts + - tests/smoke/extensions/install.test.ts + - tests/smoke/use/template.test.ts +--- + +# Extension installation and namespaces + +How `quarto add`, `quarto update`, `quarto remove` and `quarto use template` place extensions under `_extensions/`, and how a document finds them again. Read this before changing the install layout or the extension lookup. + +## The namespace rule + +An installed extension lives at `_extensions///` or `_extensions//`. + +- `` comes from **where the extension is installed from**, never from the source tree. For GitHub sources (`org/repo`, `org/repo/subdir`, `org/repo@ref`, and github.com archive URLs) it is the GitHub owner (`ExtensionSource.owner`, set from `host.organization` in `extension-host.ts`). +- Local directories, local archives and non-GitHub URLs have no owner: their extensions go directly under `_extensions/`. The user docs say archives "share a single namespace" (quarto-web `docs/extensions/distributing.qmd`, "Archive Distribution"). +- `` is the extension's folder name in the source. +- An owner folder found in the source (`_extensions///`) is **dropped**. Since #15111 the installer prints an INFO note when this happens, naming the id to use. + +The documented purpose of the namespace is avoiding name conflicts ("the use of organizations as a 'namespace' for managing name conflicts", `distributing.qmd`). Deriving it from the install origin means a fork or copy installed from another owner lands under that owner and cannot overwrite the original. Do not change install to keep the source owner folder: a fork shipping `_extensions/quarto-ext/x/` would then install into `quarto-ext/`. + +### History + +- Before June 2022 the install copied the source tree as is, so the namespace was whatever folder the source shipped (`name@org`, later `org/name`). +- June 2022 (`959f25f85`, `dc94a9673`, `e72966d30`): ids became `org/name`, the owner comes from GitHub, local sources are flattened. +- July 2022: author guidance changed to an unowned source layout `_extensions//` (quarto-web `903cc7ca7`, and `9fafe706a` whose message says sources "used to have owner name space"). quarto-journals repos were moved out of their owner folders. `quarto-journals/acm` got its owner folder back by an accidental merge and still has it. +- `92458565d` (v1.5.15) passed the template root to `copyExtensions`, so templates with an owner folder installed nothing (#14008). Fixed by #14010 (1.11). + +## Source layout (what extension authors ship) + +`_extensions//_extension.yml` at the root of the repo or archive, optionally inside a single wrapper directory (GitHub archive tarballs). `quarto create extension` scaffolds this. An extension that needs another owner's extension embeds it with `quarto add --embed `, which places it at `_extensions//_extensions///` inside the host extension and keeps its owner folder there. + +## Install pipeline (`src/extension/install.ts`) + +1. `installExtension` resolves the source (`extensionSource` in `extension-host.ts`): `{type: "local"}` for an existing path, `{type: "remote", owner, targetSubdir}` for a resolved URL. +2. `stageExtension` builds a staged `_extensions` tree in a temp dir: + - remote or local archive → `unzipAndStage` → `copyExtensions(source, , staged)`; + - local directory → `readAndCopyExtensions(/_extensions, staged/_extensions)` with no owner. +3. `copyExtensions` targets `_extensions//` (or the root) and calls `readAndCopyExtensions`, which copies each extension to `/` and prints the owner note when the source owner folder differs. +4. `confirmInstallation` compares staged extensions with installed ones by **name and organization** and prints the `[Install]`/`[Update]`/`[Revert]`/`[No Change]` table. A flattened `acm` and an owned `quarto-journals/acm` count as different extensions, so a reinstall that changes the owner installs a second copy instead of updating. +5. `completeInstallation` moves the staged tree into the install directory chosen by `determineInstallDir`: the current directory (inside a project, a prompt offers the project root instead; `--no-prompt` keeps the current directory), or, with `--embed `, the folder of that extension, which must contribute a format. + +`quarto use template` (`src/command/use/commands/template.ts`) stages the template, reads `