Skip to content

Commit fcfd107

Browse files
wan9chiclaude
andcommitted
feat(client): vite-task-client JS package
Adds the JS-side npm package `@voidzero-dev/vite-task-client` with its type-generation tooling. The package is a thin Node.js wrapper that attempts to load a runner-provided napi addon at runtime via the `VP_RUN_NODE_CLIENT_PATH` env var and gracefully no-ops when absent — so it's installable and importable on its own, independent of the runner's Rust implementation. The actual napi binding and the runner-side IPC server land in the follow-up PR on top of this one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5833b37 commit fcfd107

10 files changed

Lines changed: 322 additions & 3 deletions

File tree

.github/workflows/ci.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,11 @@ jobs:
275275
- name: Deduplicate dependencies
276276
run: pnpm dedupe --check
277277

278+
- name: Check vite-task-client types are not stale
279+
run: |
280+
pnpm build-vite-task-client-types
281+
git diff --exit-code packages/vite-task-client/index.d.ts
282+
278283
done:
279284
runs-on: namespace-profile-linux-x64-default
280285
if: always()

.oxfmtrc.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"ignorePatterns": [
44
"crates/fspy_detours_sys/detours",
55
"crates/vite_task_graph/run-config.ts",
6-
"**/fixtures/*/snapshots"
6+
"**/fixtures/*/snapshots",
7+
"packages/vite-task-client/index.d.ts"
78
]
89
}

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@
44
"license": "MIT",
55
"type": "module",
66
"scripts": {
7-
"prepare": "husky"
7+
"prepare": "husky",
8+
"build-vite-task-client-types": "tsc -p packages/vite-task-client/tsconfig.json"
89
},
910
"devDependencies": {
11+
"@tsconfig/strictest": "catalog:",
1012
"@types/node": "catalog:",
1113
"husky": "catalog:",
1214
"lint-staged": "catalog:",
1315
"oxfmt": "catalog:",
1416
"oxlint": "catalog:",
15-
"oxlint-tsgolint": "catalog:"
17+
"oxlint-tsgolint": "catalog:",
18+
"typescript": "catalog:"
1619
},
1720
"lint-staged": {
1821
"*": "oxfmt --no-error-on-unmatched-pattern",
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# @voidzero-dev/vite-task-client
2+
3+
A tiny Node.js client that lets your tool talk to the
4+
[Vite+](https://github.com/voidzero-dev/vite-plus) task runner (`vp run`)
5+
it's running under. Use it to hand the runner more precise
6+
cache-correctness information than it can infer from the outside.
7+
8+
Outside a runner-managed task, every call is a graceful no-op — you can
9+
call into this from a tool that's also used standalone without any
10+
runtime detection or conditionals.
11+
12+
## Install
13+
14+
```sh
15+
npm install @voidzero-dev/vite-task-client
16+
```
17+
18+
## Quick start
19+
20+
```js
21+
import { ignoreInput, ignoreOutput, disableCache, getEnv } from '@voidzero-dev/vite-task-client';
22+
23+
ignoreInput('./node_modules/.cache/my-tool');
24+
25+
// `vp run` only exposes envs the task config declares; for everything
26+
// else, `getEnv` fetches the value from the runner and registers it as
27+
// a cache-key dependency in the same call.
28+
const apiVersion = process.env.MY_API_VERSION ?? getEnv('MY_API_VERSION');
29+
30+
if (somethingNonDeterministicHappened) disableCache();
31+
```
32+
33+
## Why this exists
34+
35+
`vp run` decides whether a task's cached result is reusable by hashing
36+
everything your task read and everything it depends on. It infers that
37+
set automatically — watching filesystem syscalls, scanning declared
38+
inputs and env vars. That's safe but can be too coarse:
39+
40+
- Your tool maintains its own cache under `node_modules/.cache/`. Every
41+
miss there would invalidate every other run, even though the contents
42+
don't actually affect your output.
43+
- Your tool reads `process.env.MY_API_VERSION`, and `vp run`'s task
44+
config doesn't list it.
45+
- Your tool has a non-deterministic mode it sometimes falls into and
46+
should skip the cache entirely.
47+
48+
These functions give you a precise way to correct each case from inside
49+
your tool, without forcing your users to rewrite their `vp run` task
50+
config.
51+
52+
## API
53+
54+
See [`index.d.ts`](./index.d.ts) for the full signatures and per-function
55+
behavior.
56+
57+
## For `vite-task` developers
58+
59+
This package is a thin pure-JS wrapper with **no `dependencies` in
60+
`package.json`** — its only runtime artifact is the napi addon, which
61+
the runner provides at execution time.
62+
63+
That means when you change this package in a `vite-task` PR, the
64+
consuming tool can pull your unpublished commit directly via a git URL
65+
with a subpath, no npm release required:
66+
67+
```jsonc
68+
// In the consuming tool's package.json
69+
{
70+
"dependencies": {
71+
"@voidzero-dev/vite-task-client": "github:voidzero-dev/vite-task#<commit-sha>&path:/packages/vite-task-client",
72+
},
73+
}
74+
```
75+
76+
(`&path:` is supported by pnpm. For npm/yarn, see your package
77+
manager's docs on monorepo-subpath git installs.)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* Tell the runner to ignore reads under `path` when inferring cache inputs.
3+
*
4+
* No-op when not running inside a runner.
5+
*
6+
* @param {string} path
7+
* @returns {void}
8+
*/
9+
export function ignoreInput(path: string): void;
10+
/**
11+
* Tell the runner to ignore writes under `path` when inferring cache outputs.
12+
*
13+
* No-op when not running inside a runner.
14+
*
15+
* @param {string} path
16+
* @returns {void}
17+
*/
18+
export function ignoreOutput(path: string): void;
19+
/**
20+
* Tell the runner not to cache this run.
21+
*
22+
* No-op when not running inside a runner.
23+
*
24+
* @returns {void}
25+
*/
26+
export function disableCache(): void;
27+
/**
28+
* Ask the runner for the value of the env var `name` and return it, or
29+
* `undefined` when the runner has no such env.
30+
*
31+
* With `tracked: true` (the default) the runner records `name` as a
32+
* dependency, so a change to its value invalidates this run's cache entry.
33+
*
34+
* Has no effect on `process.env`; the caller decides what to do with the
35+
* returned value. Returns `undefined` when not running inside a runner.
36+
*
37+
* @param {string} name
38+
* @param {{ tracked?: boolean }} [options]
39+
* @returns {string | undefined}
40+
*/
41+
export function getEnv(name: string, options?: {
42+
tracked?: boolean;
43+
}): string | undefined;
44+
/**
45+
* Ask the runner for every env whose name matches `pattern` (a glob, e.g.
46+
* `VITE_*`) and return the match-set as a plain object.
47+
*
48+
* With `tracked: true` (the default) the runner records the pattern as a
49+
* dependency, so adding, removing, or changing a matching env invalidates
50+
* this run's cache entry.
51+
*
52+
* Has no effect on `process.env`; the caller decides what to do with the
53+
* returned values. Returns an empty object when not running inside a runner.
54+
*
55+
* @param {string} pattern
56+
* @param {{ tracked?: boolean }} [options]
57+
* @returns {Record<string, string>}
58+
*/
59+
export function getEnvs(pattern: string, options?: {
60+
tracked?: boolean;
61+
}): Record<string, string>;

packages/vite-task-client/index.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// The JSDoc in this file is the source of truth for the package's public
2+
// types. `index.d.ts` is generated from it via `pnpm run build:types`
3+
// (using `tsc` with `@tsconfig/strictest`) — edit JSDoc here, not the
4+
// `.d.ts`. CI fails if the committed `.d.ts` drifts from a fresh regen.
5+
6+
import { createRequire } from 'node:module';
7+
8+
/**
9+
* Methods exposed by the napi addon. Keep this shape in sync with the
10+
* `RunnerClient` returned by `load()` in
11+
* `crates/vite_task_client_napi/src/lib.rs` — any new method added there
12+
* needs a matching entry here, and vice versa.
13+
*
14+
* @type {{
15+
* ignoreInput: (path: string) => void,
16+
* ignoreOutput: (path: string) => void,
17+
* disableCache: () => void,
18+
* getEnv: (name: string, options?: { tracked?: boolean }) => string | undefined,
19+
* getEnvs: (pattern: string, options?: { tracked?: boolean }) => Record<string, string>,
20+
* } | null | undefined}
21+
*/
22+
let addon;
23+
24+
function load() {
25+
if (addon !== undefined) return addon;
26+
try {
27+
const path = process.env['VP_RUN_NODE_CLIENT_PATH'];
28+
if (path) {
29+
// The addon exports a `load(options?)` factory rather than the
30+
// methods directly, so the addon shape can evolve in lockstep with
31+
// this wrapper: a future wrapper can pass `{ version: N }` to opt
32+
// into a new shape without breaking older addons that only know v1.
33+
// Today's wrapper passes nothing and accepts whatever the addon's
34+
// current default version returns.
35+
addon = createRequire(import.meta.url)(path).load();
36+
return addon;
37+
}
38+
} catch {
39+
// Fall through — the runner's IPC env is absent or the addon refused to
40+
// load. Memoize the unavailable decision so subsequent calls don't retry.
41+
}
42+
addon = null;
43+
return addon;
44+
}
45+
46+
/**
47+
* Tell the runner to ignore reads under `path` when inferring cache inputs.
48+
*
49+
* No-op when not running inside a runner.
50+
*
51+
* @param {string} path
52+
* @returns {void}
53+
*/
54+
export function ignoreInput(path) {
55+
load()?.ignoreInput(path);
56+
}
57+
58+
/**
59+
* Tell the runner to ignore writes under `path` when inferring cache outputs.
60+
*
61+
* No-op when not running inside a runner.
62+
*
63+
* @param {string} path
64+
* @returns {void}
65+
*/
66+
export function ignoreOutput(path) {
67+
load()?.ignoreOutput(path);
68+
}
69+
70+
/**
71+
* Tell the runner not to cache this run.
72+
*
73+
* No-op when not running inside a runner.
74+
*
75+
* @returns {void}
76+
*/
77+
export function disableCache() {
78+
load()?.disableCache();
79+
}
80+
81+
/**
82+
* Ask the runner for the value of the env var `name` and return it, or
83+
* `undefined` when the runner has no such env.
84+
*
85+
* With `tracked: true` (the default) the runner records `name` as a
86+
* dependency, so a change to its value invalidates this run's cache entry.
87+
*
88+
* Has no effect on `process.env`; the caller decides what to do with the
89+
* returned value. Returns `undefined` when not running inside a runner.
90+
*
91+
* @param {string} name
92+
* @param {{ tracked?: boolean }} [options]
93+
* @returns {string | undefined}
94+
*/
95+
export function getEnv(name, options) {
96+
const a = load();
97+
if (!a) return undefined;
98+
return a.getEnv(name, options);
99+
}
100+
101+
/**
102+
* Ask the runner for every env whose name matches `pattern` (a glob, e.g.
103+
* `VITE_*`) and return the match-set as a plain object.
104+
*
105+
* With `tracked: true` (the default) the runner records the pattern as a
106+
* dependency, so adding, removing, or changing a matching env invalidates
107+
* this run's cache entry.
108+
*
109+
* Has no effect on `process.env`; the caller decides what to do with the
110+
* returned values. Returns an empty object when not running inside a runner.
111+
*
112+
* @param {string} pattern
113+
* @param {{ tracked?: boolean }} [options]
114+
* @returns {Record<string, string>}
115+
*/
116+
export function getEnvs(pattern, options) {
117+
const a = load();
118+
if (!a) return {};
119+
return a.getEnvs(pattern, options);
120+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "@voidzero-dev/vite-task-client",
3+
"version": "0.1.0",
4+
"private": true,
5+
"type": "module",
6+
"main": "./index.js",
7+
"types": "./index.d.ts"
8+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"extends": "@tsconfig/strictest/tsconfig.json",
3+
"compilerOptions": {
4+
"allowJs": true,
5+
"checkJs": true,
6+
"declaration": true,
7+
"emitDeclarationOnly": true,
8+
"module": "NodeNext",
9+
"moduleResolution": "NodeNext",
10+
"target": "ES2022",
11+
"lib": ["ES2022"],
12+
"types": ["node"]
13+
},
14+
"include": ["index.js"]
15+
}

0 commit comments

Comments
 (0)