From 4b96b2aedfb195dccc0dc604e6ebbfcc19be84f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 19:16:37 +0000 Subject: [PATCH 1/5] docs: add plugin authoring best practices to plugins guide Agent-Logs-Url: https://github.com/dmno-dev/varlock/sessions/5df62f64-9242-42ce-930b-beb085280d9a Co-authored-by: philmillman <3722211+philmillman@users.noreply.github.com> --- .../src/content/docs/guides/plugins.mdx | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/varlock-website/src/content/docs/guides/plugins.mdx b/packages/varlock-website/src/content/docs/guides/plugins.mdx index ae9948c9a..d269e4ba0 100644 --- a/packages/varlock-website/src/content/docs/guides/plugins.mdx +++ b/packages/varlock-website/src/content/docs/guides/plugins.mdx @@ -111,3 +111,48 @@ Once installed, all decorators, data types, and resolver functions provided by t Some decorators or resolver functions may require the plugin to be initialized and will throw an error if not set up properly. Please refer to the specific plugin's documentation for details on usage. + +## Plugin authoring best practices + +If you're building a first-party or third-party plugin, these are the patterns we recommend (and follow in existing plugins): + +### Build on `varlock/plugin-lib` + +- Import from `varlock/plugin-lib` and register everything at module load time. +- Set core metadata early (`plugin.name`, `plugin.icon`, and `plugin.standardVars` when relevant). +- Use plugin-lib error classes (`ValidationError`, `SchemaError`, `CoercionError`, `ResolutionError`) for clear and consistent error handling. + +### Design for safe, predictable schemas + +- Keep initialization explicit via root decorators (for example, `@initYourPlugin(...)`). +- Support multiple named instances (`id=`) when users may need separate auth/scope boundaries. +- Accept secrets through schema references (for example, `token=$MY_TOKEN`) so users can mark and manage sensitive values in one place. +- Use clear names for decorators/data types/functions to avoid global naming collisions. + +### Package for compatibility + +- Publish a dedicated plugin entry point via `exports`: + +```json title="package.json" +{ + "exports": { + "./plugin": "./dist/plugin.cjs" + } +} +``` + +- Keep `varlock` as a peer dependency. +- Build the plugin entry as CJS (`dist/plugin.cjs`) for runtime loading compatibility. +- Prefer bundling SDK/client libraries into the plugin build when possible, keeping runtime requirements minimal. + +### Test like first-party plugins + +- Add focused tests for initialization, reference parsing, and resolver edge cases. +- Include a `vitest.config.ts` with `resolve.conditions = ['ts-src']`. +- Define `__VARLOCK_BUILD_TYPE__` and `__VARLOCK_SEA_BUILD__` in tests to match plugin runtime assumptions used by first-party plugins. + +### Document plugin usage clearly + +- Document installation and initialization requirements. +- Include authentication modes and expected schema examples. +- Call out unsupported features and behavior differences early (for example, missing bulk-load support in a specific provider mode). From f2ea280545f780545ea583707b15c9c6c5fff1a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 19:17:06 +0000 Subject: [PATCH 2/5] docs: clarify plugin icon format and secret reference rationale Agent-Logs-Url: https://github.com/dmno-dev/varlock/sessions/5df62f64-9242-42ce-930b-beb085280d9a Co-authored-by: philmillman <3722211+philmillman@users.noreply.github.com> --- packages/varlock-website/src/content/docs/guides/plugins.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/varlock-website/src/content/docs/guides/plugins.mdx b/packages/varlock-website/src/content/docs/guides/plugins.mdx index d269e4ba0..50c62f812 100644 --- a/packages/varlock-website/src/content/docs/guides/plugins.mdx +++ b/packages/varlock-website/src/content/docs/guides/plugins.mdx @@ -120,13 +120,14 @@ If you're building a first-party or third-party plugin, these are the patterns w - Import from `varlock/plugin-lib` and register everything at module load time. - Set core metadata early (`plugin.name`, `plugin.icon`, and `plugin.standardVars` when relevant). + - Use a consistent icon identifier format (first-party plugins typically use `simple-icons:`). - Use plugin-lib error classes (`ValidationError`, `SchemaError`, `CoercionError`, `ResolutionError`) for clear and consistent error handling. ### Design for safe, predictable schemas - Keep initialization explicit via root decorators (for example, `@initYourPlugin(...)`). - Support multiple named instances (`id=`) when users may need separate auth/scope boundaries. -- Accept secrets through schema references (for example, `token=$MY_TOKEN`) so users can mark and manage sensitive values in one place. +- Accept secrets through schema references (for example, `token=$MY_TOKEN`) so users can mark and manage sensitive values in one place, enabling proper redaction and leak-prevention behavior. - Use clear names for decorators/data types/functions to avoid global naming collisions. ### Package for compatibility From 32a616c520fa3714e5ec0de9b7b9ff39804df238 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 19:25:57 +0000 Subject: [PATCH 3/5] docs: plan plugin API reference pages Agent-Logs-Url: https://github.com/dmno-dev/varlock/sessions/1d76414c-3948-437f-9607-441289d4a2fd Co-authored-by: philmillman <3722211+philmillman@users.noreply.github.com> --- .../src/content/docs/guides/plugins.mdx | 248 ++++++++++++++++-- 1 file changed, 222 insertions(+), 26 deletions(-) diff --git a/packages/varlock-website/src/content/docs/guides/plugins.mdx b/packages/varlock-website/src/content/docs/guides/plugins.mdx index 50c62f812..1b78e797a 100644 --- a/packages/varlock-website/src/content/docs/guides/plugins.mdx +++ b/packages/varlock-website/src/content/docs/guides/plugins.mdx @@ -114,46 +114,242 @@ Please refer to the specific plugin's documentation for details on usage. ## Plugin authoring best practices -If you're building a first-party or third-party plugin, these are the patterns we recommend (and follow in existing plugins): +Plugins are TypeScript modules that import from `varlock/plugin-lib` and register their functionality at module load time. Everything below is drawn from real first-party plugins. -### Build on `varlock/plugin-lib` +### Scaffold and metadata -- Import from `varlock/plugin-lib` and register everything at module load time. -- Set core metadata early (`plugin.name`, `plugin.icon`, and `plugin.standardVars` when relevant). - - Use a consistent icon identifier format (first-party plugins typically use `simple-icons:`). -- Use plugin-lib error classes (`ValidationError`, `SchemaError`, `CoercionError`, `ResolutionError`) for clear and consistent error handling. +A plugin is a single TypeScript file (`src/plugin.ts`). All registration calls run at the top level when the module is loaded. -### Design for safe, predictable schemas +```ts title="src/plugin.ts" +import { type Resolver, plugin } from 'varlock/plugin-lib'; -- Keep initialization explicit via root decorators (for example, `@initYourPlugin(...)`). -- Support multiple named instances (`id=`) when users may need separate auth/scope boundaries. -- Accept secrets through schema references (for example, `token=$MY_TOKEN`) so users can mark and manage sensitive values in one place, enabling proper redaction and leak-prevention behavior. -- Use clear names for decorators/data types/functions to avoid global naming collisions. +// Destructure the error classes you need +const { ValidationError, SchemaError, ResolutionError } = plugin.ERRORS; -### Package for compatibility +// Short identifier used internally (e.g. for logging) +plugin.name = 'myplugin'; -- Publish a dedicated plugin entry point via `exports`: +// Optional: debug logger (only active when VARLOCK_DEBUG is set) +const { debug } = plugin; +debug('init - version =', plugin.version); + +// Icon from simple-icons (https://simpleicons.org) or any Iconify set +plugin.icon = 'simple-icons:yourservice'; + +// Optional: declare well-known env var names so users get warnings if they +// forget to wire them up as schema items +plugin.standardVars = { + initDecorator: '@initMyPlugin', + params: { + token: { key: 'MY_SERVICE_TOKEN' }, + url: { key: 'MY_SERVICE_URL' }, + }, +}; + +// registration calls follow… +``` + +### `plugin.registerRootDecorator` + +Root decorators appear as `@decoratorName(...)` comments at the top of a `.env` file. They are used for plugin initialization and run in two phases: + +- **`process`** — runs during schema parsing. Validates static arguments (e.g. `id=`), creates the instance record, and returns a plain serialisable object containing any `Resolver` references for dynamic args. +- **`execute`** — runs during value resolution. Awaits the dynamic resolvers returned by `process` and performs auth/connection setup. + +```ts title="src/plugin.ts" +interface PluginInstance { + token?: string; +} +const instances: Record = {}; + +plugin.registerRootDecorator({ + name: 'initMyPlugin', + description: 'Initialise a MyPlugin instance', + isFunction: true, // required when the decorator accepts arguments + + async process(argsVal) { + const { objArgs } = argsVal; + if (!objArgs) throw new SchemaError('@initMyPlugin requires arguments'); + + // id must be a literal string so we can key the instance map at parse time + if (objArgs.id && !objArgs.id.isStatic) { + throw new SchemaError('id must be a static value'); + } + const id = String(objArgs.id?.staticValue ?? '_default'); + + if (instances[id]) { + throw new SchemaError(`Instance "${id}" is already initialised`); + } + instances[id] = {}; // reserve the slot + + // Return resolvers for dynamic args alongside any static data + return { id, tokenResolver: objArgs.token }; + }, + + async execute({ id, tokenResolver }) { + // Await dynamic values (these may reference other schema items) + const token = await tokenResolver?.resolve(); + instances[id].token = token ? String(token) : undefined; + }, +}); +``` + +### `plugin.registerDataType` + +Data types appear as `@type=myType` on an item. They can mark values as sensitive, add validation, and surface documentation links. + +```ts title="src/plugin.ts" +plugin.registerDataType({ + name: 'myServiceToken', + sensitive: true, // value will be redacted in logs + typeDescription: 'Authentication token for MyService', + icon: 'simple-icons:yourservice', + docs: [ + { + description: 'Creating API tokens', + url: 'https://docs.yourservice.example/tokens', + }, + ], + // Optional: validate the raw string value + async validate(val) { + if (typeof val !== 'string' || !val.startsWith('mst_')) { + throw new ValidationError('Token must start with "mst_"'); + } + }, +}); +``` + +### `plugin.registerResolverFunction` + +Resolver functions appear as values in `.env` files: `MY_SECRET=myPlugin(ref)`. They also run in two phases: + +- **`process`** — runs at parse time. Validate argument shapes and return the resolvers + metadata your `resolve` call needs. +- **`resolve`** — runs at resolution time. Awaits resolvers, contacts the external service, and returns the final string value. + +```ts title="src/plugin.ts" +plugin.registerResolverFunction({ + name: 'myPlugin', + label: 'Fetch secret from MyService', + icon: 'simple-icons:yourservice', + argsSchema: { + type: 'array', + arrayMinLength: 1, + arrayMaxLength: 2, // myPlugin(ref) or myPlugin(instanceId, ref) + }, + + process() { + let instanceId = '_default'; + let refResolver: Resolver; + + if (this.arrArgs!.length === 1) { + refResolver = this.arrArgs![0]; + } else { + // first arg is the instance id – must be a literal + if (!this.arrArgs![0].isStatic) { + throw new SchemaError('Instance id must be a static value'); + } + instanceId = String(this.arrArgs![0].staticValue); + refResolver = this.arrArgs![1]; + } + + if (!instances[instanceId]) { + throw new SchemaError( + `No MyPlugin instance "${instanceId}" found`, + { tip: 'Add @initMyPlugin() to your .env.schema file' }, + ); + } + + return { instanceId, refResolver }; + }, + + async resolve({ instanceId, refResolver }) { + const ref = await refResolver.resolve(); + if (typeof ref !== 'string') throw new SchemaError('Expected a string reference'); + + const instance = instances[instanceId]; + // ... call your SDK / API and return the secret value + return `fetched:${ref}`; + }, +}); +``` + +### Error handling + +Always use error classes from `plugin.ERRORS`: + +| Class | When to use | +|---|---| +| `SchemaError` | Problems detected at parse/schema-build time (bad args, missing config) | +| `ResolutionError` | Problems at value-fetch time (secret not found, network error) | +| `ValidationError` | Value fails a `@type` constraint | +| `CoercionError` | Value cannot be converted to the expected type | + +Pass a `tip` string (or array of strings) to guide users toward a fix: + +```ts +throw new ResolutionError(`Secret "${ref}" not found`, { + tip: [ + 'Verify the secret name is correct in MyService', + 'Check your token has read access', + ], +}); +``` + +### Package setup ```json title="package.json" { + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", "exports": { "./plugin": "./dist/plugin.cjs" + }, + "files": ["dist"], + "engines": { "node": ">=22" }, + "peerDependencies": { "varlock": "*" }, + "devDependencies": { + "varlock": "...", + "tsup": "...", + "vitest": "..." } } ``` -- Keep `varlock` as a peer dependency. -- Build the plugin entry as CJS (`dist/plugin.cjs`) for runtime loading compatibility. -- Prefer bundling SDK/client libraries into the plugin build when possible, keeping runtime requirements minimal. - -### Test like first-party plugins - -- Add focused tests for initialization, reference parsing, and resolver edge cases. -- Include a `vitest.config.ts` with `resolve.conditions = ['ts-src']`. -- Define `__VARLOCK_BUILD_TYPE__` and `__VARLOCK_SEA_BUILD__` in tests to match plugin runtime assumptions used by first-party plugins. +Key points: +- `"./plugin"` exports to a **CJS** file — this is required for runtime plugin loading. +- SDK/client libraries should go in `devDependencies` and be bundled via tsup; they must **not** be listed as runtime `dependencies` (which would require the user to install them separately). +- `varlock` is a `peerDependency` so that `instanceof` checks on error classes work correctly. + +```ts title="tsup.config.ts" +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/plugin.ts'], + format: ['cjs'], // CJS required for plugin loading + dts: true, + sourcemap: true, + treeshake: true, + external: ['varlock'], // peer – do NOT bundle +}); +``` -### Document plugin usage clearly +### Testing + +```ts title="vitest.config.ts" +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + resolve: { + // Resolve varlock's `ts-src` condition so tests run against TypeScript source + conditions: ['ts-src'], + }, + define: { + // Required – varlock uses these globals at import time + __VARLOCK_BUILD_TYPE__: JSON.stringify('test'), + __VARLOCK_SEA_BUILD__: 'false', + }, +}); +``` -- Document installation and initialization requirements. -- Include authentication modes and expected schema examples. -- Call out unsupported features and behavior differences early (for example, missing bulk-load support in a specific provider mode). +Without `conditions: ['ts-src']` and the two `define` entries your tests will fail with a `ReferenceError`. From 3b64e69c040e7631e6d1cf7c6e2278a2c1bc3dd2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 19:28:32 +0000 Subject: [PATCH 4/5] docs: restructure plugin API reference as single page Agent-Logs-Url: https://github.com/dmno-dev/varlock/sessions/1d76414c-3948-437f-9607-441289d4a2fd Co-authored-by: philmillman <3722211+philmillman@users.noreply.github.com> --- packages/varlock-website/astro.config.ts | 11 ++ .../docs/reference/plugin-api/overview.mdx | 152 +++++++++++++++ .../plugin-api/register-data-type.mdx | 119 ++++++++++++ .../plugin-api/register-item-decorator.mdx | 91 +++++++++ .../plugin-api/register-resolver-function.mdx | 175 ++++++++++++++++++ .../plugin-api/register-root-decorator.mdx | 122 ++++++++++++ 6 files changed, 670 insertions(+) create mode 100644 packages/varlock-website/src/content/docs/reference/plugin-api/overview.mdx create mode 100644 packages/varlock-website/src/content/docs/reference/plugin-api/register-data-type.mdx create mode 100644 packages/varlock-website/src/content/docs/reference/plugin-api/register-item-decorator.mdx create mode 100644 packages/varlock-website/src/content/docs/reference/plugin-api/register-resolver-function.mdx create mode 100644 packages/varlock-website/src/content/docs/reference/plugin-api/register-root-decorator.mdx diff --git a/packages/varlock-website/astro.config.ts b/packages/varlock-website/astro.config.ts index a70111749..15f88547b 100644 --- a/packages/varlock-website/astro.config.ts +++ b/packages/varlock-website/astro.config.ts @@ -206,6 +206,17 @@ export default defineConfig({ { label: 'Builtin variables', slug: 'reference/builtin-variables', badge: 'new' }, ], }, + { + label: 'Plugin API', + collapsed: true, + items: [ + { label: 'Overview', slug: 'reference/plugin-api/overview' }, + { label: 'registerRootDecorator()', slug: 'reference/plugin-api/register-root-decorator' }, + { label: 'registerItemDecorator()', slug: 'reference/plugin-api/register-item-decorator' }, + { label: 'registerDataType()', slug: 'reference/plugin-api/register-data-type' }, + { label: 'registerResolverFunction()', slug: 'reference/plugin-api/register-resolver-function' }, + ], + }, { label: '@env-spec', collapsed: true, diff --git a/packages/varlock-website/src/content/docs/reference/plugin-api/overview.mdx b/packages/varlock-website/src/content/docs/reference/plugin-api/overview.mdx new file mode 100644 index 000000000..7e65a336c --- /dev/null +++ b/packages/varlock-website/src/content/docs/reference/plugin-api/overview.mdx @@ -0,0 +1,152 @@ +--- +title: Plugin API overview +description: Reference for the varlock/plugin-lib module — metadata properties, error classes, and the debug helper available to plugin authors +--- + +Plugins are TypeScript modules that import from `varlock/plugin-lib` and register their behaviour at module load time. + +```ts +import { type Resolver, plugin } from 'varlock/plugin-lib'; +``` + +The `plugin` object is a proxy that points to the currently-loading plugin instance. It is only valid during module execution — accessing it outside of plugin code will throw. + +## Metadata properties + +Set these early in your plugin file, before any registration calls. + +### `plugin.name` +**Type:** `string` + +A short internal identifier for the plugin, used in log output and error messages. Convention is lowercase with hyphens (e.g. `'my-service'`). + +```ts +plugin.name = 'my-service'; +``` + +--- + +### `plugin.version` + +**Type:** `string` (read-only) + +The version string read from the plugin package's `package.json`. Set automatically — do not assign. + +```ts +const { debug } = plugin; +debug('loaded version', plugin.version); +``` + +--- + +### `plugin.icon` + +**Type:** `string` + +An icon identifier used in the Varlock VS Code extension and website UI. First-party plugins use [Simple Icons](https://simpleicons.org) identifiers (e.g. `simple-icons:vault`). Any [Iconify](https://icones.js.org) identifier is accepted. + +```ts +plugin.icon = 'simple-icons:yourservice'; +``` + +--- + +### `plugin.standardVars` + +**Type:** +```ts +{ + initDecorator: string; + params: Record; + dataType?: string; + }>; +} +``` + +Declares well-known environment variable names that your plugin reads (e.g. `VAULT_TOKEN`). Varlock will emit a warning if those variables are detected in the process environment but are _not_ wired up to the init decorator in the schema. + +- `initDecorator` — the name of your init root decorator, including the `@` prefix (e.g. `'@initMyPlugin'`). +- `params` — a map of parameter name to the env var key(s) it corresponds to. If multiple keys are given, the first one found in the environment is used. +- `dataType` — optional; a custom data type name to suggest in the warning message. + +```ts +plugin.standardVars = { + initDecorator: '@initMyPlugin', + params: { + token: { key: 'MY_SERVICE_TOKEN', dataType: 'myServiceToken' }, + url: { key: ['MY_SERVICE_URL', 'MY_SVC_URL'] }, + }, +}; +``` + +--- + +## The `debug` helper + +### `plugin.debug` + +**Type:** `(...args: any[]) => void` + +A scoped debug logger. Messages are only printed when the `VARLOCK_DEBUG` environment variable is set. The logger is namespaced to `varlock:plugin:`. + +```ts +const { debug } = plugin; +debug('init - version =', plugin.version); +debug('resolving reference', ref); +``` + +Destructure `debug` before use — calling `plugin.debug(...)` directly also works but destructuring keeps code cleaner. + +--- + +## Error classes + +### `plugin.ERRORS` + +**Type:** +```ts +{ + ValidationError: typeof ValidationError; + CoercionError: typeof CoercionError; + SchemaError: typeof SchemaError; + ResolutionError: typeof ResolutionError; +} +``` + +Always use these error classes instead of plain `Error`. They carry structured metadata (see below) and are handled specially by the Varlock runtime. + +```ts +const { ValidationError, SchemaError, CoercionError, ResolutionError } = plugin.ERRORS; +``` + +All four classes extend a common base and accept the same constructor signature: + +```ts +throw new SchemaError('message', { + tip: 'How to fix this', // shown to the user below the error + code: 'OPTIONAL_CODE', // machine-readable code +}); +``` + +`tip` may be a single string or an array of strings (joined with newlines). + +| Class | When to use | +|---|---| +| `SchemaError` | Problems caught at parse / schema-build time — bad arguments, missing required config, duplicate instance IDs | +| `ResolutionError` | Problems at value-fetch time — secret not found, network error, auth failure | +| `ValidationError` | A value fails a `@type` constraint registered via `registerDataType` | +| `CoercionError` | A value cannot be converted to the expected type | + +--- + +## Registration methods + +Each method below registers one piece of functionality that plugins expose to `.env` files. See the individual reference pages for full API details. + +| Method | What it registers | Used in `.env` as | +|---|---|---| +| [`registerRootDecorator()`](/reference/plugin-api/register-root-decorator/) | Root decorator | `# @decoratorName(...)` in file header | +| [`registerItemDecorator()`](/reference/plugin-api/register-item-decorator/) | Item decorator | `# @decoratorName` on an item | +| [`registerDataType()`](/reference/plugin-api/register-data-type/) | Data type | `# @type=typeName` on an item | +| [`registerResolverFunction()`](/reference/plugin-api/register-resolver-function/) | Resolver function | `ITEM=fnName(arg)` as a value | diff --git a/packages/varlock-website/src/content/docs/reference/plugin-api/register-data-type.mdx b/packages/varlock-website/src/content/docs/reference/plugin-api/register-data-type.mdx new file mode 100644 index 000000000..e7b921aab --- /dev/null +++ b/packages/varlock-website/src/content/docs/reference/plugin-api/register-data-type.mdx @@ -0,0 +1,119 @@ +--- +title: plugin.registerDataType() +description: Register a custom data type that users can apply with @type=typeName on config items to get validation, coercion, sensitive-marking, and documentation links +--- + +Data types are applied to config items with the `@type=` item decorator. A plugin can register new types to model domain-specific values — API tokens, connection strings, region enums, and so on. + +```ts +plugin.registerDataType(def); +``` + +## Type definition + +```ts +type DataTypeDef = { + /** Name used in .env files as @type=name */ + name: string; + /** Short description of what kind of value this type represents */ + typeDescription?: string; + /** Iconify icon identifier (e.g. 'simple-icons:aws') */ + icon?: string; + /** + * When true, items of this type are sensitive by default. + * The user can still override with @public on a specific item. + */ + sensitive?: boolean; + /** + * Coerce a raw value to the target type. + * Return the coerced value, undefined (skip coercion), or a CoercionError. + * Called before validate. + */ + coerce?: (value: any) => CoercedType | CoercionError | undefined; + /** + * Validate the (already-coerced) value. + * Return nothing / true on success, or throw / return a ValidationError on failure. + */ + validate?: (value: any) => void | true | undefined | ValidationError | Array + | Promise>; + /** Documentation links surfaced in tooling and the VS Code extension */ + docs?: Array; +}; +``` + +## Fields + +### `name` +The type identifier used in `.env` files: `# @type=name`. Must be unique across all loaded plugins. + +### `typeDescription` +A human-readable description shown in the VS Code extension hover and the Varlock website. + +### `icon` +An [Iconify](https://icones.js.org) icon identifier. First-party plugins use Simple Icons (e.g. `simple-icons:vault`). + +### `sensitive` +When `true`, items of this type are automatically treated as sensitive — their values are redacted in logs and protected from accidental leakage. Users can override with `# @public` on a specific item. + +### `coerce` +Called first, before `validate`. Accepts any incoming value and should return the canonical form, `undefined` to skip coercion, or a `CoercionError`. If omitted, incoming values are cast to `string`. + +### `validate` +Called after a successful coercion. Throw a `ValidationError` (or return it) to reject the value, return nothing or `true` to accept it. + +### `docs` +An array of documentation references shown in tooling. Each entry is either a URL string or an object with `url` and `description`. + +## Examples + +### Sensitive token type + +```ts title="src/plugin.ts" +plugin.registerDataType({ + name: 'myServiceToken', + sensitive: true, + typeDescription: 'API token for MyService', + icon: 'simple-icons:yourservice', + docs: [ + { + description: 'Generating API tokens', + url: 'https://docs.yourservice.example/tokens', + }, + ], + validate(val) { + if (typeof val !== 'string' || !val.startsWith('mst_')) { + throw new ValidationError('Token must start with "mst_"'); + } + }, +}); +``` + +Usage: + +```env-spec title=".env.schema" "@type" +# @type=myServiceToken @sensitive +MY_SERVICE_TOKEN= +``` + +### Type with coercion + +```ts title="src/plugin.ts" +plugin.registerDataType({ + name: 'awsRegion', + typeDescription: 'AWS region identifier', + icon: 'simple-icons:amazonaws', + coerce(val) { + // Normalise to lowercase + if (typeof val === 'string') return val.toLowerCase(); + }, + validate(val) { + const REGIONS = ['us-east-1', 'eu-west-1', 'ap-southeast-1']; + if (!REGIONS.includes(val as string)) { + throw new ValidationError( + `"${val}" is not a recognised AWS region`, + { tip: `Valid regions: ${REGIONS.join(', ')}` }, + ); + } + }, +}); +``` diff --git a/packages/varlock-website/src/content/docs/reference/plugin-api/register-item-decorator.mdx b/packages/varlock-website/src/content/docs/reference/plugin-api/register-item-decorator.mdx new file mode 100644 index 000000000..70ce0411b --- /dev/null +++ b/packages/varlock-website/src/content/docs/reference/plugin-api/register-item-decorator.mdx @@ -0,0 +1,91 @@ +--- +title: plugin.registerItemDecorator() +description: Register an item decorator that users can attach to individual config items in a .env file +--- + +Item decorators are attached to individual config items — the key-value pairs in a `.env` file. They appear as `# @decoratorName` or `# @decoratorName(...)` comments directly above the item they modify. + +```ts +plugin.registerItemDecorator(def); +``` + +## Type definition + +```ts +type ItemDecoratorDef = { + /** Name used in .env files as @name or @name(...) */ + name: string; + /** Names of other item decorators that cannot appear on the same item */ + incompatibleWith?: Array; + /** true → decorator must be used as a function call: @name(...) */ + isFunction?: boolean; + /** true or message → decorator is deprecated */ + deprecated?: boolean | string; + /** + * Phase 1 — runs during schema parsing. + * Receives a Resolver for the decorator's value. Validate arguments and + * return any state needed by execute. + */ + process?: (decoratorValue: Resolver) => Processed | Promise; + /** + * Phase 2 — runs during value resolution. + * Receives whatever process returned. + */ + execute?: (state: Processed) => void | Promise; +}; +``` + +## Decorator value syntax + +| `.env` syntax | When to use | How to read in `process` | +|---|---|---| +| `@name` | Flag with no value | `decoratorValue` is empty; just register intent | +| `@name=value` | Simple assignment | `decoratorValue.staticValue` | +| `@name(arg1, arg2)` | Positional args | `decoratorValue.arrArgs` | +| `@name(key=val)` | Keyword args | `decoratorValue.objArgs` | + +Set `isFunction: true` when using `@name(...)` syntax. Varlock enforces consistency — an error is thrown if users mix the two styles. + +## Example + +The following registers a `@region` decorator that stores a cloud region on a config item and validates it against a list of allowed values: + +```ts title="src/plugin.ts" +const VALID_REGIONS = ['us-east-1', 'eu-west-1', 'ap-southeast-1']; + +plugin.registerItemDecorator({ + name: 'awsRegion', + incompatibleWith: [], // list decorator names that conflict with this one + + process(decoratorValue) { + if (!decoratorValue.isStatic) { + throw new SchemaError('@awsRegion value must be a static string'); + } + const region = String(decoratorValue.staticValue); + if (!VALID_REGIONS.includes(region)) { + throw new SchemaError( + `@awsRegion: "${region}" is not a recognised AWS region`, + { tip: `Valid regions: ${VALID_REGIONS.join(', ')}` }, + ); + } + return { region }; + }, + + execute({ region }) { + // Attach extra metadata to the item at resolution time if needed + }, +}); +``` + +Usage in a `.env` file: + +```env-spec title=".env.schema" "@awsRegion" +# @awsRegion=us-east-1 +AWS_BUCKET=my-prod-bucket +``` + +## Notes + +- Item decorators are scoped to the item they annotate; they cannot affect other items. +- If no `process` function is provided the decorator simply acts as an inert marker. +- Errors thrown from `process` are reported as schema errors on that specific item. diff --git a/packages/varlock-website/src/content/docs/reference/plugin-api/register-resolver-function.mdx b/packages/varlock-website/src/content/docs/reference/plugin-api/register-resolver-function.mdx new file mode 100644 index 000000000..885d108b6 --- /dev/null +++ b/packages/varlock-website/src/content/docs/reference/plugin-api/register-resolver-function.mdx @@ -0,0 +1,175 @@ +--- +title: plugin.registerResolverFunction() +description: Register a resolver function that users can use as a config item value to fetch or compute values at resolution time +--- + +Resolver functions appear as values in `.env` files: `MY_SECRET=myPlugin(arg)`. Varlock calls them at resolution time to produce the final config value. The most common use case is fetching a secret from an external provider. + +```ts +plugin.registerResolverFunction(def); +``` + +## Type definition + +```ts +type ResolverDef = { + /** Name used in .env files as fnName(...) */ + name: string; + /** Short human-readable description */ + description?: string; + /** Label shown in tooling */ + label?: string; + /** Iconify icon identifier */ + icon?: string; + /** + * Argument shape constraints validated at parse time. + * type 'array' → only positional args allowed + * type 'object' → only key-value args allowed + * type 'mixed' → both positional and key-value args allowed + */ + argsSchema?: { + type: 'array' | 'object' | 'mixed'; + arrayExactLength?: number; + arrayMinLength?: number; + arrayMaxLength?: number; + objKeyMinLength?: number; + }; + /** + * Phase 1 — called at parse / schema-build time. + * `this` is the Resolver instance. Validate argument shapes and return + * whatever state the resolve phase needs. + */ + process?: (this: Resolver) => Processed; + /** + * Phase 2 — called at value-resolution time. + * `this` is the Resolver instance. Receives state from process. + * Return the final resolved value as a string (or number / boolean, which + * Varlock will coerce to a string). + */ + resolve: (this: Resolver, state: Processed) => ResolvedValue | Promise; +}; +``` + +## Two-phase execution + +Like root decorators, resolver functions run in two phases. + +**`process`** runs at schema-build time. `this` is the `Resolver` instance. Access arguments via: + +| Property | Type | Description | +|---|---|---| +| `this.arrArgs` | `Array \| undefined` | Positional arguments | +| `this.objArgs` | `Record \| undefined` | Keyword arguments | + +Validate argument shapes here and return a plain object with any `Resolver` references you need to await later. Do not `await` resolvers in `process` — they depend on config items that may not be resolved yet. + +**`resolve`** runs when Varlock fetches the actual value. Receives `state` from `process`. Await resolvers here, call your SDK/API, and return the value. + +## The `Resolver` argument type + +Each argument is itself a `Resolver`: + +| Property | Type | Description | +|---|---|---| +| `isStatic` | `boolean` | `true` when the argument is a literal value | +| `staticValue` | `ResolvedValue` | Literal value when `isStatic` is true (use at process time) | +| `resolve()` | `() => Promise` | Awaitable value at resolve time | + +## Example — fetching from an external provider + +```ts title="src/plugin.ts" +plugin.registerResolverFunction({ + name: 'myPlugin', + label: 'Fetch secret from MyService', + icon: 'simple-icons:yourservice', + argsSchema: { + type: 'array', + arrayMinLength: 1, + arrayMaxLength: 2, // myPlugin(ref) or myPlugin(instanceId, ref) + }, + + process() { + let instanceId = '_default'; + let refResolver: Resolver; + + if (this.arrArgs!.length === 1) { + refResolver = this.arrArgs![0]; + } else { + // first positional arg is a static instance id + if (!this.arrArgs![0].isStatic) { + throw new SchemaError('Instance id must be a static value'); + } + instanceId = String(this.arrArgs![0].staticValue); + refResolver = this.arrArgs![1]; + } + + if (!instances[instanceId]) { + throw new SchemaError( + `No MyPlugin instance "${instanceId}" found`, + { tip: 'Add @initMyPlugin() to your .env.schema file' }, + ); + } + + return { instanceId, refResolver }; + }, + + async resolve({ instanceId, refResolver }) { + const ref = await refResolver.resolve(); + if (typeof ref !== 'string') { + throw new SchemaError('Expected a string reference'); + } + const instance = instances[instanceId]; + // Call your SDK / API here: + return await instance.fetchSecret(ref); + }, +}); +``` + +Usage in a `.env` file: + +```env-spec title=".env.schema" /myPlugin\\(.*\\)/ +# @sensitive +DB_PASSWORD=myPlugin(services/prod/db#password) +# Use a named instance: +API_KEY=myPlugin(prod, services/prod/api#key) +``` + +## `argsSchema` reference + +Use `argsSchema` to let Varlock enforce argument counts before `process` even runs, so your validation code can assume the right number of args. + +| Field | Type | Description | +|---|---|---| +| `type` | `'array' \| 'object' \| 'mixed'` | Which argument style is permitted | +| `arrayExactLength` | `number` | Exactly this many positional args | +| `arrayMinLength` | `number` | At least this many positional args | +| `arrayMaxLength` | `number` | At most this many positional args | +| `objKeyMinLength` | `number` | At least this many key-value args | + +## Keyword-args example + +```ts title="src/plugin.ts" +plugin.registerResolverFunction({ + name: 'myPluginKv', + argsSchema: { type: 'object', objKeyMinLength: 1 }, + + process() { + const pathResolver = this.objArgs?.path; + const keyResolver = this.objArgs?.key; + if (!pathResolver) throw new SchemaError('path= argument is required'); + return { pathResolver, keyResolver }; + }, + + async resolve({ pathResolver, keyResolver }) { + const path = String(await pathResolver.resolve()); + const key = keyResolver ? String(await keyResolver.resolve()) : undefined; + return await instances['_default'].fetchSecret(path, key); + }, +}); +``` + +Usage: + +```env-spec title=".env.schema" /myPluginKv\\(.*\\)/ +DB_PASSWORD=myPluginKv(path=services/prod/db, key=password) +``` diff --git a/packages/varlock-website/src/content/docs/reference/plugin-api/register-root-decorator.mdx b/packages/varlock-website/src/content/docs/reference/plugin-api/register-root-decorator.mdx new file mode 100644 index 000000000..cd2754384 --- /dev/null +++ b/packages/varlock-website/src/content/docs/reference/plugin-api/register-root-decorator.mdx @@ -0,0 +1,122 @@ +--- +title: plugin.registerRootDecorator() +description: Register a root decorator that users can place in the header section of a .env file — typically used for plugin initialization +--- + +Root decorators appear in the _header_ section of a `.env` file (before the first config item). Plugins register them primarily for initialization — wiring up authentication, setting instance IDs, and so on. + +```ts +plugin.registerRootDecorator(def); +``` + +## Type definition + +```ts +type RootDecoratorDef = { + /** Name used in .env files as @name or @name(...) */ + name: string; + /** Human-readable description shown in tooling */ + description?: string; + /** true → decorator must be used as a function call: @name(...) */ + isFunction?: boolean; + /** true or message → decorator is deprecated */ + deprecated?: boolean | string; + /** Names of other decorators that cannot appear in the same file */ + incompatibleWith?: Array; + /** + * Phase 1 — runs during schema parsing. + * Validate static arguments, create instance records, and return resolvers + * for dynamic arguments (those that reference schema items). + */ + process?: (decoratorValue: Resolver) => Processed | Promise; + /** + * Phase 2 — runs during value resolution. + * Receives the object returned by process; awaits dynamic resolvers and + * performs auth / connection setup. + */ + execute?: (state: Processed) => void | Promise; +}; +``` + +## Two-phase execution + +Root decorator processing is split across two phases to handle the fact that decorator arguments may reference other schema items whose values are not yet known at parse time. + +**`process(decoratorValue)`** is called during schema building. `decoratorValue` is a `Resolver` for the decorator's value. For function-call decorators (`isFunction: true`) you will usually read `decoratorValue.objArgs` (key-value args) or `decoratorValue.arrArgs` (positional args). Use this phase to: + +- Validate that static arguments have the expected shape +- Create and reserve instance slots in a module-level map +- Return an object containing any `Resolver` references that need to be awaited later + +**`execute(state)`** is called once all schema items have resolved. `state` is whatever `process` returned. Use this phase to: + +- Await dynamic resolvers (e.g. `await state.tokenResolver.resolve()`) +- Perform SDK/client initialisation + +## The `Resolver` argument + +Inside `process`, `decoratorValue.objArgs` is a `Record` for keyword arguments. Each `Resolver` has: + +| Property | Type | Description | +|---|---|---| +| `isStatic` | `boolean` | `true` if the argument is a literal value | +| `staticValue` | `ResolvedValue` | The literal value when `isStatic` is true | +| `resolve()` | `() => Promise` | Awaitable at `execute` time | + +## Example + +```ts title="src/plugin.ts" +interface PluginInstance { + token?: string; + url?: string; +} +const instances: Record = {}; + +plugin.registerRootDecorator({ + name: 'initMyPlugin', + description: 'Initialise a connection to MyService', + isFunction: true, + + async process(argsVal) { + const { objArgs } = argsVal; + if (!objArgs) throw new SchemaError('@initMyPlugin requires arguments'); + + // id must be resolvable at parse time — it keys the instance map + if (objArgs.id && !objArgs.id.isStatic) { + throw new SchemaError('id must be a static string value'); + } + const id = String(objArgs.id?.staticValue ?? '_default'); + + if (instances[id]) { + throw new SchemaError(`Instance "${id}" is already initialised`); + } + instances[id] = {}; + + return { id, tokenResolver: objArgs.token, urlResolver: objArgs.url }; + }, + + async execute({ id, tokenResolver, urlResolver }) { + instances[id].token = tokenResolver ? String(await tokenResolver.resolve()) : undefined; + instances[id].url = urlResolver ? String(await urlResolver.resolve()) : undefined; + }, +}); +``` + +Usage in a `.env.schema` file: + +```env-spec title=".env.schema" "@initMyPlugin" +# @plugin(my-plugin) +# @initMyPlugin(token=$MY_SERVICE_TOKEN, url=https://api.example.com) +# --- +# @sensitive +MY_SERVICE_TOKEN= +``` + +## Multiple instances + +Support multiple named instances by checking `objArgs.id` and keying your instance map. Resolver functions can then accept an `id` argument to select a specific instance. + +```env-spec title=".env.schema" "@initMyPlugin" /id=[a-z]+/ +# @initMyPlugin(id=dev, token=$TOKEN_DEV) +# @initMyPlugin(id=prod, token=$TOKEN_PROD) +``` From caaadfb1630557aedeeced211b57c95711f7abb0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 19:30:01 +0000 Subject: [PATCH 5/5] docs: replace per-method pages with single plugin-api reference page Agent-Logs-Url: https://github.com/dmno-dev/varlock/sessions/1d76414c-3948-437f-9607-441289d4a2fd Co-authored-by: philmillman <3722211+philmillman@users.noreply.github.com> --- packages/varlock-website/astro.config.ts | 12 +- .../src/content/docs/reference/plugin-api.mdx | 408 ++++++++++++++++++ .../docs/reference/plugin-api/overview.mdx | 152 ------- .../plugin-api/register-data-type.mdx | 119 ----- .../plugin-api/register-item-decorator.mdx | 91 ---- .../plugin-api/register-resolver-function.mdx | 175 -------- .../plugin-api/register-root-decorator.mdx | 122 ------ 7 files changed, 409 insertions(+), 670 deletions(-) create mode 100644 packages/varlock-website/src/content/docs/reference/plugin-api.mdx delete mode 100644 packages/varlock-website/src/content/docs/reference/plugin-api/overview.mdx delete mode 100644 packages/varlock-website/src/content/docs/reference/plugin-api/register-data-type.mdx delete mode 100644 packages/varlock-website/src/content/docs/reference/plugin-api/register-item-decorator.mdx delete mode 100644 packages/varlock-website/src/content/docs/reference/plugin-api/register-resolver-function.mdx delete mode 100644 packages/varlock-website/src/content/docs/reference/plugin-api/register-root-decorator.mdx diff --git a/packages/varlock-website/astro.config.ts b/packages/varlock-website/astro.config.ts index 15f88547b..015128ad8 100644 --- a/packages/varlock-website/astro.config.ts +++ b/packages/varlock-website/astro.config.ts @@ -204,17 +204,7 @@ export default defineConfig({ { label: '> @type data types', slug: 'reference/data-types' }, { label: 'Value functions', slug: 'reference/functions' }, { label: 'Builtin variables', slug: 'reference/builtin-variables', badge: 'new' }, - ], - }, - { - label: 'Plugin API', - collapsed: true, - items: [ - { label: 'Overview', slug: 'reference/plugin-api/overview' }, - { label: 'registerRootDecorator()', slug: 'reference/plugin-api/register-root-decorator' }, - { label: 'registerItemDecorator()', slug: 'reference/plugin-api/register-item-decorator' }, - { label: 'registerDataType()', slug: 'reference/plugin-api/register-data-type' }, - { label: 'registerResolverFunction()', slug: 'reference/plugin-api/register-resolver-function' }, + { label: 'Plugin API', slug: 'reference/plugin-api' }, ], }, { diff --git a/packages/varlock-website/src/content/docs/reference/plugin-api.mdx b/packages/varlock-website/src/content/docs/reference/plugin-api.mdx new file mode 100644 index 000000000..cac257778 --- /dev/null +++ b/packages/varlock-website/src/content/docs/reference/plugin-api.mdx @@ -0,0 +1,408 @@ +--- +title: Plugin API +description: Reference for the varlock/plugin-lib module — all properties and methods available to plugin authors +--- + +Plugins import from `varlock/plugin-lib` and register their behaviour at module-load time. + +```ts +import { type Resolver, plugin } from 'varlock/plugin-lib'; +``` + +The `plugin` object is a proxy that delegates to the currently-loading plugin instance. It is valid only during plugin module execution. + +See the [plugins guide](/guides/plugins/#plugin-authoring-best-practices) for authoring patterns, packaging conventions, and testing setup. + +## Metadata properties + +
+ +
+ +### `plugin.name` +**Type:** `string` + +A short internal identifier for the plugin, used in log output and debug messages. Convention is lowercase with hyphens. + +```ts +plugin.name = 'my-service'; +``` + +
+ +
+ +### `plugin.version` +**Type:** `string` (read-only) + +The version string from the plugin package's `package.json`. Populated automatically — do not assign. + +```ts +debug('loaded version', plugin.version); +``` + +
+ +
+ +### `plugin.icon` +**Type:** `string` + +An icon identifier shown in the VS Code extension and website UI. First-party plugins use [Simple Icons](https://simpleicons.org) identifiers (e.g. `simple-icons:vault`). Any [Iconify](https://icones.js.org) identifier is accepted. + +```ts +plugin.icon = 'simple-icons:yourservice'; +``` + +
+ +
+ +### `plugin.standardVars` +**Type:** +```ts +{ + initDecorator: string; + params: Record; + dataType?: string; + }>; +} +``` + +Declares well-known environment variable names that your plugin reads (e.g. `VAULT_TOKEN`). Varlock will warn if those variables are detected in the process environment but are not wired up to the init decorator in the schema. + +- `initDecorator` — the init decorator name including the `@` prefix (e.g. `'@initMyPlugin'`). +- `params` — maps a parameter name to the env var key(s) it corresponds to. If multiple keys are given the first found in the environment is used. +- `dataType` — optional; suggests a custom type name in the warning hint. + +```ts +plugin.standardVars = { + initDecorator: '@initMyPlugin', + params: { + token: { key: 'MY_SERVICE_TOKEN', dataType: 'myServiceToken' }, + url: { key: ['MY_SERVICE_URL', 'MY_SVC_URL'] }, + }, +}; +``` + +
+ +
+ +### `plugin.debug` +**Type:** `(...args: any[]) => void` + +A scoped debug logger. Messages are only printed when the `VARLOCK_DEBUG` environment variable is set. Namespaced to `varlock:plugin:` — set `plugin.name` before using it. + +Destructure before use so it can be called without referencing `plugin` each time: + +```ts +plugin.name = 'my-service'; +const { debug } = plugin; + +debug('init - version =', plugin.version); +debug('resolving reference', ref); +``` + +
+ +
+ +### `plugin.ERRORS` +**Type:** +```ts +{ + ValidationError: typeof ValidationError; + CoercionError: typeof CoercionError; + SchemaError: typeof SchemaError; + ResolutionError: typeof ResolutionError; +} +``` + +Always destructure and use these classes instead of plain `Error`. They carry structured metadata and are handled specially by the runtime. + +```ts +const { ValidationError, SchemaError, CoercionError, ResolutionError } = plugin.ERRORS; +``` + +All four share the same constructor signature: + +```ts +throw new ResolutionError('Secret not found', { + tip: 'Check the path and verify your token has read access', + code: 'NOT_FOUND', // optional machine-readable code +}); +``` + +`tip` may be a string or an array of strings (joined with newlines). + +| Class | When to use | +|---|---| +| `SchemaError` | Problems at parse / schema-build time — bad arguments, missing config, duplicate IDs | +| `ResolutionError` | Problems at value-fetch time — secret not found, network error, auth failure | +| `ValidationError` | Value fails a `registerDataType` constraint | +| `CoercionError` | Value cannot be converted to the expected type | + +
+ +
+ +## Registration methods + +
+ +
+ +### `plugin.registerRootDecorator()` ||registerRootDecorator|| + +Registers a decorator that users can place in the _header_ section of a `.env` file. Root decorators are mainly used for plugin initialisation. + +**Argument type:** + +```ts +type RootDecoratorDef = { + name: string; + description?: string; + isFunction?: boolean; // true → must be called as @name(...) + deprecated?: boolean | string; + incompatibleWith?: Array; + process?: (decoratorValue: Resolver) => Processed | Promise; + execute?: (state: Processed) => void | Promise; +}; +``` + +**Two-phase execution:** `process` runs during schema parsing; `execute` runs at resolution time. Return resolvers from `process` so `execute` can await them once dependent schema items have resolved. + +Inside `process`, `decoratorValue.objArgs` is a `Record` (keyword args) and `decoratorValue.arrArgs` is an `Array` (positional args). Each `Resolver` exposes: +- `isStatic` / `staticValue` — available at parse time for literal arguments +- `resolve()` — awaitable at `execute` time for dynamic arguments + +```ts +const instances: Record = {}; + +plugin.registerRootDecorator({ + name: 'initMyPlugin', + description: 'Initialise a MyPlugin connection', + isFunction: true, + + async process(argsVal) { + const { objArgs } = argsVal; + if (!objArgs) throw new SchemaError('@initMyPlugin requires arguments'); + + if (objArgs.id && !objArgs.id.isStatic) { + throw new SchemaError('id must be a static value'); + } + const id = String(objArgs.id?.staticValue ?? '_default'); + + if (instances[id]) throw new SchemaError(`Instance "${id}" already initialised`); + instances[id] = {}; + + return { id, tokenResolver: objArgs.token }; + }, + + async execute({ id, tokenResolver }) { + instances[id].token = tokenResolver + ? String(await tokenResolver.resolve()) + : undefined; + }, +}); +``` + +Usage in a `.env.schema` file: + +```env-spec title=".env.schema" "@initMyPlugin" +# @plugin(my-plugin) +# @initMyPlugin(token=$MY_SERVICE_TOKEN) +# --- +# @sensitive +MY_SERVICE_TOKEN= +``` + +
+ +
+ +### `plugin.registerItemDecorator()` ||registerItemDecorator|| + +Registers a decorator that users can attach to individual config items. + +**Argument type:** + +```ts +type ItemDecoratorDef = { + name: string; + incompatibleWith?: Array; + isFunction?: boolean; + deprecated?: boolean | string; + process?: (decoratorValue: Resolver) => Processed | Promise; + execute?: (state: Processed) => void | Promise; +}; +``` + +Like root decorators, `process` runs at parse time and `execute` at resolution time. If neither is provided the decorator acts as an inert marker. + +```ts +plugin.registerItemDecorator({ + name: 'awsRegion', + + process(decoratorValue) { + if (!decoratorValue.isStatic) { + throw new SchemaError('@awsRegion value must be a static string'); + } + const region = String(decoratorValue.staticValue); + const VALID = ['us-east-1', 'eu-west-1', 'ap-southeast-1']; + if (!VALID.includes(region)) { + throw new SchemaError( + `@awsRegion: "${region}" is not a valid region`, + { tip: `Valid regions: ${VALID.join(', ')}` }, + ); + } + return { region }; + }, +}); +``` + +Usage: + +```env-spec title=".env.schema" "@awsRegion" +# @awsRegion=us-east-1 +AWS_BUCKET=my-prod-bucket +``` + +
+ +
+ +### `plugin.registerDataType()` ||registerDataType|| + +Registers a named data type that users apply with `# @type=name`. Data types add validation, coercion, sensitive-marking, and documentation links to config items. + +**Argument type:** + +```ts +type DataTypeDef = { + name: string; + typeDescription?: string; + icon?: string; + sensitive?: boolean; + coerce?: (value: any) => CoercedType | CoercionError | undefined; + validate?: (value: any) => void | true | ValidationError | Array + | Promise>; + docs?: Array; +}; +``` + +- `coerce` is called first; return the canonical form, `undefined` to skip, or a `CoercionError`. +- `validate` is called after coercion. Throw or return a `ValidationError` to reject the value. +- `sensitive: true` marks all items of this type as sensitive by default (users can override with `# @public`). + +```ts +plugin.registerDataType({ + name: 'myServiceToken', + sensitive: true, + typeDescription: 'API token for MyService', + icon: 'simple-icons:yourservice', + docs: [{ description: 'Generating tokens', url: 'https://docs.example.com/tokens' }], + validate(val) { + if (typeof val !== 'string' || !val.startsWith('mst_')) { + throw new ValidationError('Token must start with "mst_"'); + } + }, +}); +``` + +Usage: + +```env-spec title=".env.schema" "@type" +# @type=myServiceToken +MY_SERVICE_TOKEN= +``` + +
+ +
+ +### `plugin.registerResolverFunction()` ||registerResolverFunction|| + +Registers a function that users can use as a config item value. At resolution time Varlock calls the function to produce the final value — the primary mechanism for fetching secrets from external providers. + +**Argument type:** + +```ts +type ResolverDef = { + name: string; + description?: string; + label?: string; + icon?: string; + argsSchema?: { + type: 'array' | 'object' | 'mixed'; + arrayExactLength?: number; + arrayMinLength?: number; + arrayMaxLength?: number; + objKeyMinLength?: number; + }; + process?: (this: Resolver) => Processed; + resolve: (this: Resolver, state: Processed) => ResolvedValue | Promise; +}; +``` + +**Two-phase execution:** `process` runs at schema-build time (`this` is the `Resolver` instance); validate argument shapes and return state for `resolve`. `resolve` runs at value-fetch time; await resolvers and call your SDK/API. + +`argsSchema` constraints are enforced _before_ `process` runs, so validation code can assume the correct number of arguments. + +Access arguments via `this.arrArgs` (positional, `Array`) and `this.objArgs` (keyword, `Record`). Each `Resolver` exposes `isStatic`, `staticValue`, and `resolve()`. + +```ts +plugin.registerResolverFunction({ + name: 'myPlugin', + label: 'Fetch secret from MyService', + icon: 'simple-icons:yourservice', + argsSchema: { + type: 'array', + arrayMinLength: 1, + arrayMaxLength: 2, // myPlugin(ref) or myPlugin(instanceId, ref) + }, + + process() { + let instanceId = '_default'; + let refResolver: Resolver; + + if (this.arrArgs!.length === 1) { + refResolver = this.arrArgs![0]; + } else { + if (!this.arrArgs![0].isStatic) { + throw new SchemaError('Instance id must be a static value'); + } + instanceId = String(this.arrArgs![0].staticValue); + refResolver = this.arrArgs![1]; + } + + if (!instances[instanceId]) { + throw new SchemaError( + `No MyPlugin instance "${instanceId}" found`, + { tip: 'Add @initMyPlugin() to your .env.schema file' }, + ); + } + return { instanceId, refResolver }; + }, + + async resolve({ instanceId, refResolver }) { + const ref = String(await refResolver.resolve()); + return await instances[instanceId].fetchSecret(ref); + }, +}); +``` + +Usage: + +```env-spec title=".env.schema" /myPlugin\\(.*\\)/ +# @sensitive +DB_PASSWORD=myPlugin(services/db#password) +# Named instance: +API_KEY=myPlugin(prod, services/api#key) +``` + +
+ +
diff --git a/packages/varlock-website/src/content/docs/reference/plugin-api/overview.mdx b/packages/varlock-website/src/content/docs/reference/plugin-api/overview.mdx deleted file mode 100644 index 7e65a336c..000000000 --- a/packages/varlock-website/src/content/docs/reference/plugin-api/overview.mdx +++ /dev/null @@ -1,152 +0,0 @@ ---- -title: Plugin API overview -description: Reference for the varlock/plugin-lib module — metadata properties, error classes, and the debug helper available to plugin authors ---- - -Plugins are TypeScript modules that import from `varlock/plugin-lib` and register their behaviour at module load time. - -```ts -import { type Resolver, plugin } from 'varlock/plugin-lib'; -``` - -The `plugin` object is a proxy that points to the currently-loading plugin instance. It is only valid during module execution — accessing it outside of plugin code will throw. - -## Metadata properties - -Set these early in your plugin file, before any registration calls. - -### `plugin.name` -**Type:** `string` - -A short internal identifier for the plugin, used in log output and error messages. Convention is lowercase with hyphens (e.g. `'my-service'`). - -```ts -plugin.name = 'my-service'; -``` - ---- - -### `plugin.version` - -**Type:** `string` (read-only) - -The version string read from the plugin package's `package.json`. Set automatically — do not assign. - -```ts -const { debug } = plugin; -debug('loaded version', plugin.version); -``` - ---- - -### `plugin.icon` - -**Type:** `string` - -An icon identifier used in the Varlock VS Code extension and website UI. First-party plugins use [Simple Icons](https://simpleicons.org) identifiers (e.g. `simple-icons:vault`). Any [Iconify](https://icones.js.org) identifier is accepted. - -```ts -plugin.icon = 'simple-icons:yourservice'; -``` - ---- - -### `plugin.standardVars` - -**Type:** -```ts -{ - initDecorator: string; - params: Record; - dataType?: string; - }>; -} -``` - -Declares well-known environment variable names that your plugin reads (e.g. `VAULT_TOKEN`). Varlock will emit a warning if those variables are detected in the process environment but are _not_ wired up to the init decorator in the schema. - -- `initDecorator` — the name of your init root decorator, including the `@` prefix (e.g. `'@initMyPlugin'`). -- `params` — a map of parameter name to the env var key(s) it corresponds to. If multiple keys are given, the first one found in the environment is used. -- `dataType` — optional; a custom data type name to suggest in the warning message. - -```ts -plugin.standardVars = { - initDecorator: '@initMyPlugin', - params: { - token: { key: 'MY_SERVICE_TOKEN', dataType: 'myServiceToken' }, - url: { key: ['MY_SERVICE_URL', 'MY_SVC_URL'] }, - }, -}; -``` - ---- - -## The `debug` helper - -### `plugin.debug` - -**Type:** `(...args: any[]) => void` - -A scoped debug logger. Messages are only printed when the `VARLOCK_DEBUG` environment variable is set. The logger is namespaced to `varlock:plugin:`. - -```ts -const { debug } = plugin; -debug('init - version =', plugin.version); -debug('resolving reference', ref); -``` - -Destructure `debug` before use — calling `plugin.debug(...)` directly also works but destructuring keeps code cleaner. - ---- - -## Error classes - -### `plugin.ERRORS` - -**Type:** -```ts -{ - ValidationError: typeof ValidationError; - CoercionError: typeof CoercionError; - SchemaError: typeof SchemaError; - ResolutionError: typeof ResolutionError; -} -``` - -Always use these error classes instead of plain `Error`. They carry structured metadata (see below) and are handled specially by the Varlock runtime. - -```ts -const { ValidationError, SchemaError, CoercionError, ResolutionError } = plugin.ERRORS; -``` - -All four classes extend a common base and accept the same constructor signature: - -```ts -throw new SchemaError('message', { - tip: 'How to fix this', // shown to the user below the error - code: 'OPTIONAL_CODE', // machine-readable code -}); -``` - -`tip` may be a single string or an array of strings (joined with newlines). - -| Class | When to use | -|---|---| -| `SchemaError` | Problems caught at parse / schema-build time — bad arguments, missing required config, duplicate instance IDs | -| `ResolutionError` | Problems at value-fetch time — secret not found, network error, auth failure | -| `ValidationError` | A value fails a `@type` constraint registered via `registerDataType` | -| `CoercionError` | A value cannot be converted to the expected type | - ---- - -## Registration methods - -Each method below registers one piece of functionality that plugins expose to `.env` files. See the individual reference pages for full API details. - -| Method | What it registers | Used in `.env` as | -|---|---|---| -| [`registerRootDecorator()`](/reference/plugin-api/register-root-decorator/) | Root decorator | `# @decoratorName(...)` in file header | -| [`registerItemDecorator()`](/reference/plugin-api/register-item-decorator/) | Item decorator | `# @decoratorName` on an item | -| [`registerDataType()`](/reference/plugin-api/register-data-type/) | Data type | `# @type=typeName` on an item | -| [`registerResolverFunction()`](/reference/plugin-api/register-resolver-function/) | Resolver function | `ITEM=fnName(arg)` as a value | diff --git a/packages/varlock-website/src/content/docs/reference/plugin-api/register-data-type.mdx b/packages/varlock-website/src/content/docs/reference/plugin-api/register-data-type.mdx deleted file mode 100644 index e7b921aab..000000000 --- a/packages/varlock-website/src/content/docs/reference/plugin-api/register-data-type.mdx +++ /dev/null @@ -1,119 +0,0 @@ ---- -title: plugin.registerDataType() -description: Register a custom data type that users can apply with @type=typeName on config items to get validation, coercion, sensitive-marking, and documentation links ---- - -Data types are applied to config items with the `@type=` item decorator. A plugin can register new types to model domain-specific values — API tokens, connection strings, region enums, and so on. - -```ts -plugin.registerDataType(def); -``` - -## Type definition - -```ts -type DataTypeDef = { - /** Name used in .env files as @type=name */ - name: string; - /** Short description of what kind of value this type represents */ - typeDescription?: string; - /** Iconify icon identifier (e.g. 'simple-icons:aws') */ - icon?: string; - /** - * When true, items of this type are sensitive by default. - * The user can still override with @public on a specific item. - */ - sensitive?: boolean; - /** - * Coerce a raw value to the target type. - * Return the coerced value, undefined (skip coercion), or a CoercionError. - * Called before validate. - */ - coerce?: (value: any) => CoercedType | CoercionError | undefined; - /** - * Validate the (already-coerced) value. - * Return nothing / true on success, or throw / return a ValidationError on failure. - */ - validate?: (value: any) => void | true | undefined | ValidationError | Array - | Promise>; - /** Documentation links surfaced in tooling and the VS Code extension */ - docs?: Array; -}; -``` - -## Fields - -### `name` -The type identifier used in `.env` files: `# @type=name`. Must be unique across all loaded plugins. - -### `typeDescription` -A human-readable description shown in the VS Code extension hover and the Varlock website. - -### `icon` -An [Iconify](https://icones.js.org) icon identifier. First-party plugins use Simple Icons (e.g. `simple-icons:vault`). - -### `sensitive` -When `true`, items of this type are automatically treated as sensitive — their values are redacted in logs and protected from accidental leakage. Users can override with `# @public` on a specific item. - -### `coerce` -Called first, before `validate`. Accepts any incoming value and should return the canonical form, `undefined` to skip coercion, or a `CoercionError`. If omitted, incoming values are cast to `string`. - -### `validate` -Called after a successful coercion. Throw a `ValidationError` (or return it) to reject the value, return nothing or `true` to accept it. - -### `docs` -An array of documentation references shown in tooling. Each entry is either a URL string or an object with `url` and `description`. - -## Examples - -### Sensitive token type - -```ts title="src/plugin.ts" -plugin.registerDataType({ - name: 'myServiceToken', - sensitive: true, - typeDescription: 'API token for MyService', - icon: 'simple-icons:yourservice', - docs: [ - { - description: 'Generating API tokens', - url: 'https://docs.yourservice.example/tokens', - }, - ], - validate(val) { - if (typeof val !== 'string' || !val.startsWith('mst_')) { - throw new ValidationError('Token must start with "mst_"'); - } - }, -}); -``` - -Usage: - -```env-spec title=".env.schema" "@type" -# @type=myServiceToken @sensitive -MY_SERVICE_TOKEN= -``` - -### Type with coercion - -```ts title="src/plugin.ts" -plugin.registerDataType({ - name: 'awsRegion', - typeDescription: 'AWS region identifier', - icon: 'simple-icons:amazonaws', - coerce(val) { - // Normalise to lowercase - if (typeof val === 'string') return val.toLowerCase(); - }, - validate(val) { - const REGIONS = ['us-east-1', 'eu-west-1', 'ap-southeast-1']; - if (!REGIONS.includes(val as string)) { - throw new ValidationError( - `"${val}" is not a recognised AWS region`, - { tip: `Valid regions: ${REGIONS.join(', ')}` }, - ); - } - }, -}); -``` diff --git a/packages/varlock-website/src/content/docs/reference/plugin-api/register-item-decorator.mdx b/packages/varlock-website/src/content/docs/reference/plugin-api/register-item-decorator.mdx deleted file mode 100644 index 70ce0411b..000000000 --- a/packages/varlock-website/src/content/docs/reference/plugin-api/register-item-decorator.mdx +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: plugin.registerItemDecorator() -description: Register an item decorator that users can attach to individual config items in a .env file ---- - -Item decorators are attached to individual config items — the key-value pairs in a `.env` file. They appear as `# @decoratorName` or `# @decoratorName(...)` comments directly above the item they modify. - -```ts -plugin.registerItemDecorator(def); -``` - -## Type definition - -```ts -type ItemDecoratorDef = { - /** Name used in .env files as @name or @name(...) */ - name: string; - /** Names of other item decorators that cannot appear on the same item */ - incompatibleWith?: Array; - /** true → decorator must be used as a function call: @name(...) */ - isFunction?: boolean; - /** true or message → decorator is deprecated */ - deprecated?: boolean | string; - /** - * Phase 1 — runs during schema parsing. - * Receives a Resolver for the decorator's value. Validate arguments and - * return any state needed by execute. - */ - process?: (decoratorValue: Resolver) => Processed | Promise; - /** - * Phase 2 — runs during value resolution. - * Receives whatever process returned. - */ - execute?: (state: Processed) => void | Promise; -}; -``` - -## Decorator value syntax - -| `.env` syntax | When to use | How to read in `process` | -|---|---|---| -| `@name` | Flag with no value | `decoratorValue` is empty; just register intent | -| `@name=value` | Simple assignment | `decoratorValue.staticValue` | -| `@name(arg1, arg2)` | Positional args | `decoratorValue.arrArgs` | -| `@name(key=val)` | Keyword args | `decoratorValue.objArgs` | - -Set `isFunction: true` when using `@name(...)` syntax. Varlock enforces consistency — an error is thrown if users mix the two styles. - -## Example - -The following registers a `@region` decorator that stores a cloud region on a config item and validates it against a list of allowed values: - -```ts title="src/plugin.ts" -const VALID_REGIONS = ['us-east-1', 'eu-west-1', 'ap-southeast-1']; - -plugin.registerItemDecorator({ - name: 'awsRegion', - incompatibleWith: [], // list decorator names that conflict with this one - - process(decoratorValue) { - if (!decoratorValue.isStatic) { - throw new SchemaError('@awsRegion value must be a static string'); - } - const region = String(decoratorValue.staticValue); - if (!VALID_REGIONS.includes(region)) { - throw new SchemaError( - `@awsRegion: "${region}" is not a recognised AWS region`, - { tip: `Valid regions: ${VALID_REGIONS.join(', ')}` }, - ); - } - return { region }; - }, - - execute({ region }) { - // Attach extra metadata to the item at resolution time if needed - }, -}); -``` - -Usage in a `.env` file: - -```env-spec title=".env.schema" "@awsRegion" -# @awsRegion=us-east-1 -AWS_BUCKET=my-prod-bucket -``` - -## Notes - -- Item decorators are scoped to the item they annotate; they cannot affect other items. -- If no `process` function is provided the decorator simply acts as an inert marker. -- Errors thrown from `process` are reported as schema errors on that specific item. diff --git a/packages/varlock-website/src/content/docs/reference/plugin-api/register-resolver-function.mdx b/packages/varlock-website/src/content/docs/reference/plugin-api/register-resolver-function.mdx deleted file mode 100644 index 885d108b6..000000000 --- a/packages/varlock-website/src/content/docs/reference/plugin-api/register-resolver-function.mdx +++ /dev/null @@ -1,175 +0,0 @@ ---- -title: plugin.registerResolverFunction() -description: Register a resolver function that users can use as a config item value to fetch or compute values at resolution time ---- - -Resolver functions appear as values in `.env` files: `MY_SECRET=myPlugin(arg)`. Varlock calls them at resolution time to produce the final config value. The most common use case is fetching a secret from an external provider. - -```ts -plugin.registerResolverFunction(def); -``` - -## Type definition - -```ts -type ResolverDef = { - /** Name used in .env files as fnName(...) */ - name: string; - /** Short human-readable description */ - description?: string; - /** Label shown in tooling */ - label?: string; - /** Iconify icon identifier */ - icon?: string; - /** - * Argument shape constraints validated at parse time. - * type 'array' → only positional args allowed - * type 'object' → only key-value args allowed - * type 'mixed' → both positional and key-value args allowed - */ - argsSchema?: { - type: 'array' | 'object' | 'mixed'; - arrayExactLength?: number; - arrayMinLength?: number; - arrayMaxLength?: number; - objKeyMinLength?: number; - }; - /** - * Phase 1 — called at parse / schema-build time. - * `this` is the Resolver instance. Validate argument shapes and return - * whatever state the resolve phase needs. - */ - process?: (this: Resolver) => Processed; - /** - * Phase 2 — called at value-resolution time. - * `this` is the Resolver instance. Receives state from process. - * Return the final resolved value as a string (or number / boolean, which - * Varlock will coerce to a string). - */ - resolve: (this: Resolver, state: Processed) => ResolvedValue | Promise; -}; -``` - -## Two-phase execution - -Like root decorators, resolver functions run in two phases. - -**`process`** runs at schema-build time. `this` is the `Resolver` instance. Access arguments via: - -| Property | Type | Description | -|---|---|---| -| `this.arrArgs` | `Array \| undefined` | Positional arguments | -| `this.objArgs` | `Record \| undefined` | Keyword arguments | - -Validate argument shapes here and return a plain object with any `Resolver` references you need to await later. Do not `await` resolvers in `process` — they depend on config items that may not be resolved yet. - -**`resolve`** runs when Varlock fetches the actual value. Receives `state` from `process`. Await resolvers here, call your SDK/API, and return the value. - -## The `Resolver` argument type - -Each argument is itself a `Resolver`: - -| Property | Type | Description | -|---|---|---| -| `isStatic` | `boolean` | `true` when the argument is a literal value | -| `staticValue` | `ResolvedValue` | Literal value when `isStatic` is true (use at process time) | -| `resolve()` | `() => Promise` | Awaitable value at resolve time | - -## Example — fetching from an external provider - -```ts title="src/plugin.ts" -plugin.registerResolverFunction({ - name: 'myPlugin', - label: 'Fetch secret from MyService', - icon: 'simple-icons:yourservice', - argsSchema: { - type: 'array', - arrayMinLength: 1, - arrayMaxLength: 2, // myPlugin(ref) or myPlugin(instanceId, ref) - }, - - process() { - let instanceId = '_default'; - let refResolver: Resolver; - - if (this.arrArgs!.length === 1) { - refResolver = this.arrArgs![0]; - } else { - // first positional arg is a static instance id - if (!this.arrArgs![0].isStatic) { - throw new SchemaError('Instance id must be a static value'); - } - instanceId = String(this.arrArgs![0].staticValue); - refResolver = this.arrArgs![1]; - } - - if (!instances[instanceId]) { - throw new SchemaError( - `No MyPlugin instance "${instanceId}" found`, - { tip: 'Add @initMyPlugin() to your .env.schema file' }, - ); - } - - return { instanceId, refResolver }; - }, - - async resolve({ instanceId, refResolver }) { - const ref = await refResolver.resolve(); - if (typeof ref !== 'string') { - throw new SchemaError('Expected a string reference'); - } - const instance = instances[instanceId]; - // Call your SDK / API here: - return await instance.fetchSecret(ref); - }, -}); -``` - -Usage in a `.env` file: - -```env-spec title=".env.schema" /myPlugin\\(.*\\)/ -# @sensitive -DB_PASSWORD=myPlugin(services/prod/db#password) -# Use a named instance: -API_KEY=myPlugin(prod, services/prod/api#key) -``` - -## `argsSchema` reference - -Use `argsSchema` to let Varlock enforce argument counts before `process` even runs, so your validation code can assume the right number of args. - -| Field | Type | Description | -|---|---|---| -| `type` | `'array' \| 'object' \| 'mixed'` | Which argument style is permitted | -| `arrayExactLength` | `number` | Exactly this many positional args | -| `arrayMinLength` | `number` | At least this many positional args | -| `arrayMaxLength` | `number` | At most this many positional args | -| `objKeyMinLength` | `number` | At least this many key-value args | - -## Keyword-args example - -```ts title="src/plugin.ts" -plugin.registerResolverFunction({ - name: 'myPluginKv', - argsSchema: { type: 'object', objKeyMinLength: 1 }, - - process() { - const pathResolver = this.objArgs?.path; - const keyResolver = this.objArgs?.key; - if (!pathResolver) throw new SchemaError('path= argument is required'); - return { pathResolver, keyResolver }; - }, - - async resolve({ pathResolver, keyResolver }) { - const path = String(await pathResolver.resolve()); - const key = keyResolver ? String(await keyResolver.resolve()) : undefined; - return await instances['_default'].fetchSecret(path, key); - }, -}); -``` - -Usage: - -```env-spec title=".env.schema" /myPluginKv\\(.*\\)/ -DB_PASSWORD=myPluginKv(path=services/prod/db, key=password) -``` diff --git a/packages/varlock-website/src/content/docs/reference/plugin-api/register-root-decorator.mdx b/packages/varlock-website/src/content/docs/reference/plugin-api/register-root-decorator.mdx deleted file mode 100644 index cd2754384..000000000 --- a/packages/varlock-website/src/content/docs/reference/plugin-api/register-root-decorator.mdx +++ /dev/null @@ -1,122 +0,0 @@ ---- -title: plugin.registerRootDecorator() -description: Register a root decorator that users can place in the header section of a .env file — typically used for plugin initialization ---- - -Root decorators appear in the _header_ section of a `.env` file (before the first config item). Plugins register them primarily for initialization — wiring up authentication, setting instance IDs, and so on. - -```ts -plugin.registerRootDecorator(def); -``` - -## Type definition - -```ts -type RootDecoratorDef = { - /** Name used in .env files as @name or @name(...) */ - name: string; - /** Human-readable description shown in tooling */ - description?: string; - /** true → decorator must be used as a function call: @name(...) */ - isFunction?: boolean; - /** true or message → decorator is deprecated */ - deprecated?: boolean | string; - /** Names of other decorators that cannot appear in the same file */ - incompatibleWith?: Array; - /** - * Phase 1 — runs during schema parsing. - * Validate static arguments, create instance records, and return resolvers - * for dynamic arguments (those that reference schema items). - */ - process?: (decoratorValue: Resolver) => Processed | Promise; - /** - * Phase 2 — runs during value resolution. - * Receives the object returned by process; awaits dynamic resolvers and - * performs auth / connection setup. - */ - execute?: (state: Processed) => void | Promise; -}; -``` - -## Two-phase execution - -Root decorator processing is split across two phases to handle the fact that decorator arguments may reference other schema items whose values are not yet known at parse time. - -**`process(decoratorValue)`** is called during schema building. `decoratorValue` is a `Resolver` for the decorator's value. For function-call decorators (`isFunction: true`) you will usually read `decoratorValue.objArgs` (key-value args) or `decoratorValue.arrArgs` (positional args). Use this phase to: - -- Validate that static arguments have the expected shape -- Create and reserve instance slots in a module-level map -- Return an object containing any `Resolver` references that need to be awaited later - -**`execute(state)`** is called once all schema items have resolved. `state` is whatever `process` returned. Use this phase to: - -- Await dynamic resolvers (e.g. `await state.tokenResolver.resolve()`) -- Perform SDK/client initialisation - -## The `Resolver` argument - -Inside `process`, `decoratorValue.objArgs` is a `Record` for keyword arguments. Each `Resolver` has: - -| Property | Type | Description | -|---|---|---| -| `isStatic` | `boolean` | `true` if the argument is a literal value | -| `staticValue` | `ResolvedValue` | The literal value when `isStatic` is true | -| `resolve()` | `() => Promise` | Awaitable at `execute` time | - -## Example - -```ts title="src/plugin.ts" -interface PluginInstance { - token?: string; - url?: string; -} -const instances: Record = {}; - -plugin.registerRootDecorator({ - name: 'initMyPlugin', - description: 'Initialise a connection to MyService', - isFunction: true, - - async process(argsVal) { - const { objArgs } = argsVal; - if (!objArgs) throw new SchemaError('@initMyPlugin requires arguments'); - - // id must be resolvable at parse time — it keys the instance map - if (objArgs.id && !objArgs.id.isStatic) { - throw new SchemaError('id must be a static string value'); - } - const id = String(objArgs.id?.staticValue ?? '_default'); - - if (instances[id]) { - throw new SchemaError(`Instance "${id}" is already initialised`); - } - instances[id] = {}; - - return { id, tokenResolver: objArgs.token, urlResolver: objArgs.url }; - }, - - async execute({ id, tokenResolver, urlResolver }) { - instances[id].token = tokenResolver ? String(await tokenResolver.resolve()) : undefined; - instances[id].url = urlResolver ? String(await urlResolver.resolve()) : undefined; - }, -}); -``` - -Usage in a `.env.schema` file: - -```env-spec title=".env.schema" "@initMyPlugin" -# @plugin(my-plugin) -# @initMyPlugin(token=$MY_SERVICE_TOKEN, url=https://api.example.com) -# --- -# @sensitive -MY_SERVICE_TOKEN= -``` - -## Multiple instances - -Support multiple named instances by checking `objArgs.id` and keying your instance map. Resolver functions can then accept an `id` argument to select a specific instance. - -```env-spec title=".env.schema" "@initMyPlugin" /id=[a-z]+/ -# @initMyPlugin(id=dev, token=$TOKEN_DEV) -# @initMyPlugin(id=prod, token=$TOKEN_PROD) -```