diff --git a/.claude/skills/imgproxy-docs-sync/SKILL.md b/.claude/skills/imgproxy-docs-sync/SKILL.md new file mode 100644 index 0000000..751d19d --- /dev/null +++ b/.claude/skills/imgproxy-docs-sync/SKILL.md @@ -0,0 +1,224 @@ +--- +name: imgproxy-docs-sync +description: Sync this package with an upstream imgproxy docs update. Takes a GitHub issue ("Usage docs of imgproxy have been updated"), reads the linked imgproxy-docs diff, works out what must change in this repo, prints a plan for approval, then implements it and opens a PR. Use when the user references such an issue by number or URL, or asks to catch up with imgproxy docs / usage changes. +--- + +# imgproxy docs sync + +Turn an upstream docs-update issue into a reviewed code change and a PR. + +The `imgproxy-docs` repo fires a `repository_dispatch` at this repo whenever the **Usage** +section of the docs changes. `.github/workflows/imgproxy-usage-updated.yml` opens an issue +from `.github/templates/ISSUE.md`, always titled _"Usage docs of imgproxy have been updated"_ +and always shaped like this: + +``` +@DarthSim has just updated the [Usage](https://docs.imgproxy.net/category/usage) part of the documentation. +Please, check [the difference](https://github.com/imgproxy/imgproxy-docs/compare/451581c87fd8...6c7c0312f7f0) and make the necessary changes. +``` + +The whole job is: read that diff, decide what it means for this package, get sign-off, ship it. + +## Input + +The skill argument is an issue number (`83`), an issue URL, or nothing. + +- **Nothing given** — list the candidates and ask which one: + `gh issue list --state open --search "Usage docs of imgproxy have been updated" --json number,title,createdAt` + If several are open, note that they are **sequential**: issue N's `base` sha is usually issue N-1's + `head` sha. Offer to process the oldest first, or to collapse the whole run into one range + (oldest `base` … newest `head`) and one PR. Don't decide this silently — ask. +- **Issue given** — go straight to step 1. + +## Step 1 — Read the issue and extract the compare range + +```bash +gh issue view --json number,title,body,state,url +``` + +Pull the two shas out of the `the difference` link: +`https://github.com/imgproxy/imgproxy-docs/compare/...`. + +Bail out early and tell the user if: the issue is already closed, its body has no compare link +(a hand-written issue — this skill doesn't apply, handle it normally), or a PR already references +it (`gh pr list --state all --search ""`). + +## Step 2 — Get the upstream diff + +```bash +gh api repos/imgproxy/imgproxy-docs/compare/... \ + --jq '.files[] | {filename, status, additions, deletions}' +``` + +Then read the patches for **`docs/usage/**` only\*\*: + +```bash +gh api repos/imgproxy/imgproxy-docs/compare/... \ + --jq '.files[] | select(.filename | startswith("docs/usage/")) | "=== \(.filename) ===\n\(.patch)"' +``` + +Notes: + +- `docs/configuration/**`, `docs/image_sources/**`, `docs/installation/**` etc. are **out of scope** — + this package builds URLs, it doesn't configure a server. List them in the report as "ignored, and why", + never drop them in silence. +- A very large compare gets truncated by the API. If `.files` looks clipped or patches are missing, + fall back to fetching the raw diff: + `curl -sL https://github.com/imgproxy/imgproxy-docs/compare/....diff` +- A diff hunk carries little context. For every option you are going to touch, **also read the + rendered doc page** (`WebFetch https://docs.imgproxy.net/usage/processing#`) to get the + full argument list, defaults, and value ranges. The hunk tells you _what changed_; the page tells + you _what the option is_. + +## Step 3 — Classify each usage change + +Put every hunk in exactly one bucket: + +| Bucket | Meaning | Action | +| ------------------------ | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| **New option** | A new `### Some option` block with an `imgproxy_url_option` code fence | Add option module + types + wiring + tests | +| **Changed option** | New/renamed/reordered arguments, widened or narrowed value range, new enum value, changed default | Amend the existing module + types + tests | +| **Removed / deprecated** | Option deleted or marked deprecated | Propose deprecation in JSDoc; **do not** delete public API without asking — it's a breaking change | +| **Prose only** | Rewording, typo, clarified explanation, example change with no semantic effect | No code change; JSDoc/`@see` refresh only if the wording is quoted in our types | +| **Out of scope** | Signing, presets server config, source URL formats, anything not an option this package emits | No change, with a one-line reason | + +Watch for these markers in the docs: + +- `((pro))` in a heading → PRO feature. Say so in the JSDoc (`**PRO feature**`) — see `src/types/blurAreas.ts`. +- `%arg` placeholders in the `imgproxy_url_option` fence → the positional argument order, and the + short alias on the second line (`progressive_blur:...` / `pbl:...`) → both keys go in the + `*OptionsPartial` interface. +- "_(optional)_" on an argument → optional in TS **and** subject to the trailing-colon rule + (see `references/repo-conventions.md`; getting this wrong caused the `gradient` bug fixed in c8eb5b7). +- An option under _Getting the image info_ → `src/optionsImageInfo/` + `src/typesImageInfo/`, + not `src/options/`. An option valid for both URL kinds → `src/optionsShared/` + `src/typesShared/`. + +Before proposing anything, **check whether it already exists**: + +```bash +rg -n "|" src/ +git log --oneline -15 +``` + +Earlier issues in the backlog may already be covered by merged PRs. + +## Step 4 — Print the plan and get approval + +**This step is mandatory. Never write code before the user approves.** + +Print a report in this shape: + +``` +## Issue #83 — imgproxy-docs 451581c…6c7c031 + +### Upstream usage changes +1. processing.mdx: NEW option `progressive_blur` / `pbl` ((pro)) + args: sigma:direction:start:stop — direction is angle|down|up|right|left (default down), + start/stop are 0..1 floats (defaults 0.0 / 1.0) +2. processing.mdx: prose only — reworded the `blur` intro + +### Ignored +- configuration/options.mdx, image_sources/amazon_s3.mdx — server config, not URL options + +### Proposed changes in this repo +| File | Action | +| --- | --- | +| src/types/progressiveBlur.ts | new — ProgressiveBlur + ProgressiveBlurOptionsPartial | +| src/types/index.ts | import + add to the Options intersection (alphabetical) | +| src/options/progressiveBlur.ts | new — test/build, guards, trailing-colon trim | +| src/options/index.ts | re-export (alphabetical) | +| tests/optionsBasic/progressiveBlur.test.ts | new — test() truthiness, build() happy paths, every guard error | +| .changeset/progressive-blur.md | new — minor | + +### Emitted URL parts +{ progressive_blur: { sigma: 5 } } -> pbl:5 +{ pbl: { sigma: 5, direction: "up", start: 0, stop: 1 } } -> pbl:5:up:0:1 + +### Open questions +- none +``` + +Include the **Emitted URL parts** section always — it is the fastest way for the user to spot a +misreading of the docs. Include **Open questions** whenever the docs are ambiguous about defaults, +ranges, or whether an argument is optional; ask rather than guess. + +Then call `AskUserQuestion`: _Proceed as planned_ / _Proceed with changes (describe them)_ / +_Report only, don't implement_ / _Cancel_. Honour "report only" — stop there and leave the tree clean. + +## Step 5 — Implement + +Only after approval. + +```bash +git checkout main && git pull && git checkout -b +``` + +Branch names describe the change: `add-progressive-blur`, `fix-gradient-explicit-optional-args`. + +Follow `references/repo-conventions.md` exactly — file layout, guard usage, JSDoc shape, test +coverage, and the optional-argument rules live there. Then: + +```bash +npm run lint && npm run check-types && npm run test -- --run && npm run build +``` + +All four must pass — they are the CI job (`.github/workflows/ci.yml`). Never edit `dist/`; `npm run build` +regenerates it, and it must not be part of the commit (it is checked in, so `git status` it and leave +it out unless it was already tracked-dirty before you started). + +Add a changeset by hand at `.changeset/.md` (don't run the interactive CLI): + +```md +--- +"@imgproxy/imgproxy-js-core": minor +--- + +Add support for the `progressive_blur` (`pbl`) processing option. … +``` + +`minor` for a new option or a new argument, `patch` for a fix or a doc/JSDoc correction. Write the +body for a changelog reader: what the option does, and the shape users pass. See +`.changeset/gradient-explicit-optional-args.md` for the tone. + +## Step 6 — Commit and open the PR + +```bash +git add -A && git commit +``` + +Commit subject matches the repo's history style: `Add progressive_blur support`, +`Add support for exif canonical_names option`, `Fix gradient dropping optional arguments`. +Use the trailers the environment requires. + +```bash +git push -u origin +gh pr create --base main --title "" --body "" +``` + +PR body: + +````md +Closes #83 + +Upstream: https://github.com/imgproxy/imgproxy-docs/compare/451581c87fd8...6c7c0312f7f0 + +Adds the `progressive_blur` / `pbl` processing option (PRO). + +- `sigma` — required, >= 0 +- `direction` — optional, angle in degrees or `down` | `up` | `right` | `left` (default `down`) +- `start` / `stop` — optional, 0..1 (defaults `0.0` / `1.0`) + +```ts +generateUrl(url, { progressive_blur: { sigma: 5, direction: "up" } }); +// -> /pbl:5:up/plain/... +``` +```` + +`Closes #` is what links the PR back to the auto-generated issue, so it closes on merge — don't +omit it. Report the PR URL to the user. Do **not** merge. + +## When the range spans several issues + +If the user asked to collapse a run of issues into one pass, use the oldest `base` and the newest +`head` in a single compare, and put `Closes #A`, `Closes #B`, `Closes #C` on separate lines in the +PR body. Still produce one report covering everything before implementing. diff --git a/.claude/skills/imgproxy-docs-sync/references/repo-conventions.md b/.claude/skills/imgproxy-docs-sync/references/repo-conventions.md new file mode 100644 index 0000000..e702dd0 --- /dev/null +++ b/.claude/skills/imgproxy-docs-sync/references/repo-conventions.md @@ -0,0 +1,179 @@ +# Repo conventions for option modules + +Everything here is derived from the existing code. When in doubt, copy the nearest neighbour: +`src/options/blurAreas.ts` (nested object), `src/options/gradient.ts` (optional positional args), +`src/optionsImageInfo/exif.ts` (image-info option with sub-flags). + +## Where a new option goes + +| Docs section | Option module | Types | Test | +| ------------------------------ | ------------------------------------- | ----------------------------------- | -------------------------------------------- | +| Usage → Processing | `src/options/.ts` | `src/types/.ts` | `tests/optionsBasic/.test.ts` | +| Usage → Getting the image info | `src/optionsImageInfo/.ts` | `src/typesImageInfo/.ts` | `tests/optionsImageInfo/.test.ts` | +| Valid for both URL kinds | `src/optionsShared/.ts` | `src/typesShared/.ts` | `tests/optionsShared/.test.ts` | + +File name is the camelCase form of the docs' snake_case option name (`blur_areas` → `blurAreas`, +`max_src_resolution` → `maxSrcResolution`). Method-level behaviour (`generateUrl` / `generateImageInfoUrl` +themselves, e.g. the `filename` URL segment) is tested next to the source in `src/methods/*.test.ts`, +not under `tests/`. + +## Wiring (four edits, all alphabetical) + +1. `src/options/index.ts` — `export * as progressiveBlur from "./progressiveBlur";` + Shared modules are re-exported here too, with a `../optionsShared/` path. + **Order matters at runtime**: `generateUrl` iterates `Object.values(optionModules)`, so this file's + order is the order the options appear in the URL. Insert alphabetically like everything else. +2. `src/types/index.ts` — `import type { ProgressiveBlurOptionsPartial } from "./progressiveBlur";` +3. `src/types/index.ts` — add `ProgressiveBlurOptionsPartial &` to the `Options` intersection, in the + same alphabetical position. **Missing this is the classic bug**: the option builds fine but is a + type error for consumers. +4. The image-info equivalents are `src/optionsImageInfo/index.ts` and `src/typesImageInfo/index.ts`. + +`src/index.ts` exports only `generateUrl`, `generateImageInfoUrl`, `INFO_PREFIX` — it never needs +touching for a new option. + +## Option module shape + +Every module exports exactly `test` and `build`. + +```ts +import type { + ProgressiveBlur, + ProgressiveBlurOptionsPartial, +} from "../types/progressiveBlur"; +import { + guardIsUndef, + guardIsNotNum, + guardIsNotStr, + guardIsValidVal, +} from "../utils"; + +const correctDirection = { down: true, up: true, right: true, left: true }; + +const getOpt = ( + options: ProgressiveBlurOptionsPartial +): ProgressiveBlur | undefined => options.progressive_blur ?? options.pbl; + +const test = (options: ProgressiveBlurOptionsPartial): boolean => + Boolean(getOpt(options)); + +const build = (options: ProgressiveBlurOptionsPartial): string => { + const opts = getOpt(options); + guardIsUndef(opts, "progressive_blur"); + const { sigma, direction, start, stop } = opts; + + guardIsNotNum(sigma, "progressive_blur.sigma", { addParam: { min: 0 } }); + // …validate each optional arg only when it is not undefined + + return `pbl:${sigma}:${dir}:${from}:${to}`.replace(/:+$/, ""); +}; + +export { test, build }; +``` + +- `build` takes `(options, settings?)` — only add the second parameter if the option actually reads + `Settings` (see `src/optionsShared/preset.ts` usage in `generateUrl`). +- The returned string is the URL segment **without** a leading slash; `generateUrl` adds it. +- Use the short alias as the emitted prefix (`pbl:`), never the long name. + +## Optional positional arguments — the rule that bit us + +Fixed in c8eb5b7 (`gradient` dropped `start: 0` / `stop: 0` / `direction: 0`). Follow it exactly: + +- Gate validation on `!== undefined`, **never** on truthiness. `0` and `""` are meaningful values. +- Default with `??`, never `||`. Same reason. +- Fill omitted trailing args with `""` and strip the trailing colons once, at the end: + `` `gr:${op}:${c}:${dir}:${from}:${to}`.replace(/:+$/, "") `` + That collapses _trailing_ empties only, so an explicit `0` in the middle survives. +- `getOpt` may use `??` between the long and short key (`blurAreas.ts`); older modules use `||`. + Prefer `??` in new code. + +## Types module shape + +One interface per shape, plus the `*OptionsPartial` with **both** keys optional: + +```ts +/** + * *Progressive blur*. **PRO feature** + * + * @param {number} sigma - Defines the size of the mask … + * @param {ProgressiveBlurDirection} [direction] - … + * + * @example + * {progressive_blur: {sigma: 5, direction: "up"}} + * + * @see {@link https://docs.imgproxy.net/usage/processing#progressive-blur | progressive blur option imgproxy docs} + */ +interface ProgressiveBlur { … } + +/** + * *Progressive blur option*. **PRO feature** + * + * To describe the Progressive blur option, you can use the keyword `progressive_blur` or `pbl`. + * + * @see https://docs.imgproxy.net/usage/processing#progressive-blur + */ +interface ProgressiveBlurOptionsPartial { + progressive_blur?: ProgressiveBlur; + pbl?: ProgressiveBlur; +} + +export { ProgressiveBlur, ProgressiveBlurOptionsPartial }; +``` + +- Mark PRO options with `**PRO feature**` in every JSDoc block of the file. +- Always carry a `@see` link to the exact docs anchor — that link is how the next sync verifies us. +- Export with a plain `export { … }` at the bottom; the file has no default export. +- A scalar option (e.g. `blur: number`) needs no wrapper interface — just the `*OptionsPartial`. + +## Guards (`src/utils.ts`) + +| Guard | Use for | Error text it produces | +| -------------------------------------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | +| `guardIsUndef(v, "name", addInfo?)` | required presence | `name option is undefined` | +| `guardIsNotNum(v, "name", { addParam: { min, max, isInt, minEqual }, addInfo })` | numbers + ranges | `name option is not a number` / `… value can't be less than 0` / `… can't be more than 1` / `… is must be an integer` | +| `guardIsNotStr(v, "name", isHex?)` | strings; `isHex` also enforces 3/6/8 hex chars | `name option is not a string` / `must be hexadecimal` | +| `guardIsValidVal(recordOfTrue, v, "name")` | enum from a `{ a: true, b: true }` map | `name option is invalid. Valid values are: a, b` | +| `guardIsOneOf(["a","b"], v, "name")` | enum from an array | same shape | +| `guardIsNotArray(v, "name")` | arrays; also rejects empty | `name option is not an array` / `… is empty` | +| `guardIsNotBool(v, "name")` | booleans | `name option is not a boolean` | +| `normalizeBoolean(v)` | `1/0/"t"/"f"/true/false` → `"t"` / `"f"` | — | + +A dotted `paramName` (`"blur_areas.sigma"`) suppresses the ` option` suffix in the message — that's +why nested fields are named with dots and top-level ones aren't. For array elements, index the name: +`` `blur_areas.areas[${index}].left` ``. + +Note the quirk in `guardIsNotNum`: `max` is only checked when `min` is also given. If an option has an +upper bound only, pass `min` too (e.g. `{ min: 0, max: 1 }`). + +## Tests + +`vitest`, one file per option, mirroring `tests/optionsBasic/blurAreas.test.ts`: + +```ts +import { describe, expect, it } from "vitest"; +import { test, build } from "../../src/options/progressiveBlur"; + +describe("progressiveBlur", () => { + describe("test", () => { + // true for the long key, true for the short key, false for {} + }); + + describe("build", () => { + // throws when the option is undefined + // one throw case per guard, asserting the exact message + // happy paths: minimal args, all args, and every optional arg explicitly set to 0 + }); +}); +``` + +- Assert exact error strings with `toThrow("…")` — they are part of the public contract. +- Cover invalid input a vanilla-JS consumer could pass, with `// @ts-expect-error: Let's ignore an error (check for users with vanilla js).` above the line. +- Always include the `0`-valued optional-arg case for anything with optional positional args. +- Run with `npm run test -- --run` (plain `npm run test` starts vitest in watch mode). + +## README + +`README.md` documents methods, URL kinds, and settings — **not** individual options. Update it only +when the change touches that surface (as the `filename` support in 887dd39 did). A new processing +option needs no README edit. diff --git a/.gitignore b/.gitignore index 0c62379..8a35cb2 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ dist-ssr coverage # Editor directories and files +.claude/settings.local.json .vscode/* .history/* !.vscode/extensions.json diff --git a/README.md b/README.md index 8d4bc08..5195054 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ imgproxy can be used to provide a fast and secure way to _get rid of all the ima - [Usage](#usage) - [Methods](#methods) - [Development](#development) +- [Syncing with imgproxy docs](#syncing-with-imgproxy-docs) +- [Publication Workflow](#publication-workflow) ## Install @@ -129,6 +131,101 @@ npm install npm run dev ``` +## Syncing with imgproxy docs + +This package mirrors the [Usage](https://docs.imgproxy.net/category/usage) part of the imgproxy +documentation, and the mirroring is semi-automated. + +### How an update arrives + +1. The [`imgproxy-docs`](https://github.com/imgproxy/imgproxy-docs) repository sends a + `repository_dispatch` event of type `imgproxy-usage-updated` whenever its Usage docs change. +2. [`.github/workflows/imgproxy-usage-updated.yml`](./.github/workflows/imgproxy-usage-updated.yml) + picks it up and opens an issue from [`.github/templates/ISSUE.md`](./.github/templates/ISSUE.md), + always titled **"Usage docs of imgproxy have been updated"** and containing a link to a + `imgproxy-docs/compare/...` range. +3. Someone turns that diff into code here. + +### Doing step 3 with the bundled skill + +The repo ships a [Claude Code](https://claude.com/claude-code) skill, +[`.claude/skills/imgproxy-docs-sync`](./.claude/skills/imgproxy-docs-sync), that walks the whole of +step 3 for you. It is committed to the repo, so cloning is the installation. + +#### One-time setup + +```bash +npm install -g @anthropic-ai/claude-code # if you don't have Claude Code yet +gh auth login # the skill reads issues and diffs through the GitHub CLI +``` + +Check the second one with `gh auth status` — without it, the skill can't read the issue. + +#### Running it + +```bash +cd imgproxy-js-core +claude # starts Claude Code in the project +``` + +Then type this at the prompt (the leading slash is part of it): + +``` +/imgproxy-docs-sync 82 +``` + +`82` is the number of the auto-generated issue you want to work on. You can also paste the issue URL, +or type `/imgproxy-docs-sync` with nothing after it — then it lists the open +_"Usage docs of imgproxy have been updated"_ issues and asks which one you mean. + +#### What happens next + +1. It reads issue #82, pulls the `imgproxy-docs` compare range out of the issue body, and fetches + that diff — keeping only `docs/usage/**`, because server-side docs (`docs/configuration/**`, + `docs/image_sources/**`, …) don't affect a URL-building package. Anything skipped is listed with a + reason rather than dropped in silence. +2. It sorts each change into new option / changed option / removed or deprecated / prose only / out + of scope, checking `src/` first — an old issue may already be covered by a merged PR. +3. **It stops and shows you a plan.** That report is the part to actually read: what changed + upstream, a file-by-file table of what it wants to write, and the literal URL strings the new code + would produce, e.g. + + ``` + { progressive_blur: { sigma: 5 } } -> pbl:5 + ``` + + Comparing those strings against the imgproxy docs is the quickest way to catch a misreading. + +4. **You answer the approval prompt.** Four choices: _Proceed as planned_, _Proceed with changes_ + (say what to do differently — e.g. "`start` is optional, don't require it"), _Report only_ (stop + here, nothing is written, working tree stays clean), or _Cancel_. **Nothing is written to disk + before you pick.** +5. On approval it branches off `main`, writes the option module, its types, the wiring and the tests + following + [`references/repo-conventions.md`](./.claude/skills/imgproxy-docs-sync/references/repo-conventions.md), + then runs the same four commands CI runs: `npm run lint`, `npm run check-types`, + `npm run test -- --run`, `npm run build`. All four must pass. +6. It adds a changeset (`minor` for a new option or argument, `patch` for a fix), commits, pushes, + and opens a PR whose body starts with `Closes #82` so the issue closes on merge. **It does not + merge** — you review the PR as usual. + +If you'd rather see the analysis without any code being written, run it and pick _Report only_ at +step 4. + +#### Several issues open at once + +Sequential issues share shas — issue N's `base` is usually issue N-1's `head` — so the skill offers +either to take the oldest first, or to collapse the whole backlog into one compare range and one PR +that closes all of them. It asks; it doesn't decide for you. + +### Doing step 3 by hand + +The same skill files are a plain checklist — read +[`SKILL.md`](./.claude/skills/imgproxy-docs-sync/SKILL.md) for the process and +[`references/repo-conventions.md`](./.claude/skills/imgproxy-docs-sync/references/repo-conventions.md) +for the conventions a new option module must follow. Keep both up to date when those conventions +change; they are the source of truth for the next sync, automated or not. + ## Publication Workflow The project uses [changesets](https://github.com/changesets/changesets) to manage versioning and changelog.