Task runner and toolkit extending SvelteKit - conventions over configuration, filesystem as API
Gro (@fuzdev/gro) is a dev tool for building TypeScript projects with
SvelteKit, providing: convention-based task runner, Node loader for
TypeScript/Svelte/SvelteKit modules, code generation system, plugin
architecture, and integrations with Vite, esbuild, Vitest, Prettier, ESLint, and
Changesets. It's designed around conventions and the filesystem, not
configuration files.
Gro is a devDependency - it runs at build time, not in production bundles.
Key responsibilities:
- Task runner and CLI (
gro dev,gro build,gro test, etc.) - Code generation system (
.gen.tsfiles) - Node loader for TypeScript/Svelte without compilation step
- Plugin system for dev/build workflows
src_jsongeneration - analyzes TypeScript/Svelte source to produce metadata consumed by fuz_ui's API documentation system.well-known/package.jsonand.well-known/src.jsonpublishing for package metadata
Full documentation: src/docs/task.md
Tasks are TypeScript modules with .task.ts suffix that export a task object
with a run function. The task runner discovers tasks by convention, resolves
them through a search path, and provides a context with args, config, logger,
timings, and utilities.
Discovery and resolution:
- Searches
task_root_dirsin order (default:src/lib/→./→gro/dist/) gro foolooks forfoo.task.tsin search path, user tasks override builtinsgro gro/foodirectly calls builtin, bypassing local overridesgro some/dirlists all tasks in that directory- Absolute paths (starting with
/) and explicit relative paths (./,../) skip search - Implicit relative paths (
foo/bar) go through search path
Key features:
- Lazy loading - only imports the invoked task and its dependencies
- Zod schemas - optional
Argsproperty for validation, types, and auto-generated--help - Composition - use
invoke_taskhelper (respects overrides) or direct imports - Args forwarding - CLI args like
gro dev -- vite --port 3000forward to external commands- Multiple sections:
gro dev -- vite --port 3000 -- svelte-kit --debug - Special case for nested Gro tasks:
gro check -- gro test --coverageforwards to test - Forwarded args override direct args:
gro test --a 1 -- gro test --a 2results in{a: 2} invoke_taskautomatically forwards CLI args to invoked tasks
- Multiple sections:
- Error handling -
TaskErrorsuppresses stack trace,SilentErrorexits silently
Task composition patterns:
invoke_task('test', args)- respects user overrides, better logging, auto-forwards args (recommended)- Direct import -
import {task} from './test.task.js'; await task.run(ctx)- faster, tight coupling
Task context:
interface TaskContext<TArgs = object> {
args: TArgs;
config: GroConfig;
svelte_config: ParsedSvelteConfig;
filer: Filer;
log: Logger;
timings: Timings;
invoke_task: InvokeTask;
}Example task:
import type {Task} from '@fuzdev/gro';
import {z} from 'zod';
export const Args = z.strictObject({
name: z.string().default('world'),
});
export const task: Task<typeof Args> = {
summary: 'greets someone',
Args,
run: async ({args, log}) => {
log.info(`Hello, ${args.name}!`);
},
};Run with: gro mytask or gro mytask --name Alice
Overriding builtin tasks:
export const task: Task = {
run: async ({invoke_task, args}) => {
await setupCustomEnvironment();
await invoke_task('gro/test', args); // call builtin
await teardownCustomEnvironment();
},
};Implementation: src/lib/loader.ts, src/lib/register.ts
Custom Node module loader enabling direct execution of TypeScript and Svelte
files without compilation step. Registered via gro run foo.ts or
node --import @fuzdev/gro/register.js foo.ts.
Capabilities:
- TypeScript files (
.ts) via Node's experimental type stripping (--experimental-strip-types) - Svelte components (
.svelte) with SSR compilation usingcompile() - Svelte runes in TypeScript (
.svelte.ts) usingcompileModule() - JSON imports (any extension with
type: 'json'import attribute) - Raw text imports (
.css,.svg, or?rawsuffix)
SvelteKit module shims: Best-effort shims for tasks/tests/servers, not identical to actual SvelteKit modules:
$lib/*→ resolved via svelte.config.js alias tosrc/lib/$env/static/public→ readsPUBLIC_*vars from.env$env/static/private→ reads all vars from.env$env/dynamic/public→process.envwithPUBLIC_*filtering$env/dynamic/private→ fullprocess.env$app/environment→{dev: true, browser: false, building: false, version: ''}$app/paths→{base: '', assets: ''}from svelte.config.js
Full documentation: src/docs/gen.md
Convention-based codegen system where files containing .gen. in their name
export a gen function or config object. The gro gen task finds these files,
runs them, and writes output to the filesystem.
Naming convention:
foo.gen.ts→ outputsfoo.tsfoo.gen.html.ts→ outputsfoo.html- Multiple extensions stripped:
.gen.is removed, last.tsdropped - Custom output via
{content: '...', filename: 'custom.ts'} - Can return array for multiple output files
Return values:
- String: default filename, auto-formatted with Prettier
- Object:
{content, filename?, format?}for control - Array: multiple files from one genfile
null: no-op
Dependencies: By default, genfiles regenerate when they or their imports change.
Customize via dependencies property:
'all'- regenerate on any file change{patterns: [/\.json$/], files: ['package.json']}- static patterns/files- Function returning config - dynamic based on which file changed
When it runs:
gro gen- manual triggergro dev- watch mode, throttled queue viagro_plugin_gengro build- one-time generation (assumes fresh, verified by CI)gro sync- before syncing package.jsongro gen --check- verify no drift (used bygro checkand CI)
Gen context:
interface GenContext {
config: GroConfig;
svelte_config: ParsedSvelteConfig;
filer: Filer;
log: Logger;
timings: Timings;
invoke_task: InvokeTask;
origin_id: PathId; // Same as import.meta.url in path form
origin_path: string; // origin_id relative to root dir
changed_file_id: PathId | undefined; // Only during dependency checking
}Example genfile:
import type {Gen} from '@fuzdev/gro';
export const gen: Gen = async () => {
const routes = await findRoutes();
return `export const ROUTES = ${JSON.stringify(routes)};`;
};
// Multiple outputs:
export const gen: Gen = async () => [
{content: 'export const foo = 1;', filename: 'foo.ts'},
{content: '{"version": 1}', filename: 'data.json'},
];
// With dependencies:
export const gen: GenConfig = {
generate: () => 'generated content',
dependencies: {
patterns: [/\.json$/],
files: ['package.json'],
},
};Full documentation: src/docs/plugin.md
Plugins customize gro dev and gro build workflows with three lifecycle
hooks: setup, adapt, and teardown. They're objects with a name and
optional async functions for each phase.
Lifecycle:
setup- runs first in both dev and buildadapt- runs second, build only (for SvelteKit adapters)teardown- runs last, build only or dev with--no-watch
Dev vs build:
gro dev- creates plugins with{dev: true, watch: true}, runs setup, keeps process alivegro dev --no-watch- runs setup → teardown, exitsgro build- creates plugins with{dev: false, watch: false}, runs setup → adapt → teardown
Plugin interface:
interface Plugin<TPluginContext extends PluginContext = PluginContext> {
name: string;
setup?: (ctx: TPluginContext) => void | Promise<void>;
adapt?: (ctx: TPluginContext) => void | Promise<void>;
teardown?: (ctx: TPluginContext) => void | Promise<void>;
}
interface PluginContext<TArgs = object> extends TaskContext<TArgs> {
dev: boolean;
watch: boolean;
}Builtin plugins:
gro_plugin_gen- watches files, queues genfiles when they or dependencies changegro_plugin_sveltekit_app- runsvite devorvite buildfor SvelteKit frontends (docs)gro_plugin_sveltekit_library- runssvelte-packageto publish fromsrc/lib/(docs)gro_plugin_server- runs Node servers with auto-restart on changes
Full documentation: src/docs/config.md
Optional gro.config.ts at project root exports CreateGroConfig function or
config object. If absent, uses default config from
src/lib/gro.config.default.ts.
Default config behavior: Auto-detects project type by checking filesystem:
svelte.config.js→ enablesgro_plugin_sveltekit_appsvelte.config.js+@sveltejs/packagein package.json +src/lib/→ enablesgro_plugin_sveltekit_librarysrc/lib/server/server.ts→ enablesgro_plugin_server- Always enables
gro_plugin_gen
Config interface:
interface GroConfig {
plugins: PluginsCreateConfig; // Function returning array of plugins
map_package_json: PackageJsonMapper | null; // Hook for package.json automations
task_root_dirs: Array<PathId>; // Where to search for tasks
search_filters: Array<PathFilter>; // Exclude patterns for discovery
js_cli: string; // Node-compatible CLI (default: 'node')
pm_cli: string; // npm-compatible CLI (default: 'npm')
}map_package_json: Runs during gro sync to auto-generate "exports" field in
package.json using wildcard patterns for files in src/lib/. Return null
to opt out.
Example config:
import type {CreateGroConfig} from '@fuzdev/gro';
const config: CreateGroConfig = async (base_config) => {
// Extend default plugins
const base_plugins = base_config.plugins;
base_config.plugins = async (ctx) => {
const plugins = await base_plugins(ctx);
return [...plugins, myCustomPlugin()];
};
// Customize package.json automation
base_config.map_package_json = (pkg) => {
pkg.exports = {'.': './dist/index.js'};
return pkg;
};
return base_config;
};
export default config;- Task files:
*.task.tsinsrc/lib/(or configuredtask_root_dirs) - Gen files:
*.gen.*anywhere insrc/(pattern:.gen.substring) - Test files:
*.test.tsanywhere (run by Vitest) - Config:
gro.config.tsat project root - SvelteKit config:
svelte.config.jsat project root
Exclusions (configurable via search_filters):
- Dot-prefixed directories (
.git,.svelte-kit,.gro) node_modules/(exceptnode_modules/@*/gro/dist/)- Build directories:
.svelte-kit/,build/,dist/(except in Gro's own directory)
Complete list: src/docs/tasks.md
- Development:
dev,test,gen,format,lint,typecheck - Production:
build,check,publish,deploy,release - Utilities:
clean,sync,run,changeset,commit,reinstall,resolve,upgrade
Key tasks:
dev- start dev server with watch mode (SvelteKit + Vite via plugins) (docs)test- run Vitest tests matching.test.pattern (docs)gen- run code generation (docs)build- production build with intelligent caching (runs plugin lifecycle: setup → adapt → teardown) (docs)- Build caching - skips expensive rebuilds using git commit + optional config hash (docs)
- Conservative correctness: dirty workspace forces rebuild and cleans outputs
- Outputs validated via parallel hashing (implementation)
- Cache survives manual
build/deletion (stored in.gro/) - Force rebuild:
gro build --force_build
check- run all checks (test, gen --check, format --check, lint, typecheck)sync- run gen, update package.json exports, optionally install packagespublish- version with Changesets, publish to npm, push to git (docs)deploy- build and force push to git branch (default:deploy) (docs)run- execute TypeScript file with Gro's loader
Filesystem as API - tasks, genfiles, and tests discovered by naming convention
(.task.ts, .gen.*, .test.ts). No registration needed - creating a file
makes it available.
Lazy loading - only invoked tasks and their dependencies get imported. Running
gro lists all tasks but doesn't execute their code.
Conventions over configuration - file naming patterns define behavior. Minimal
config files (optional gro.config.ts). Default config auto-detects project
type by inspecting filesystem.
User overrides - local src/lib/foo.task.ts takes precedence over
gro/dist/foo.task.js. Call builtin explicitly with gro gro/foo. All of Gro's
internals exported from $lib for reuse.
Plugin lifecycle - setup initializes, adapt handles production finalization (SvelteKit adapters), teardown cleans up. Watch mode skips teardown to keep processes alive.
Filer - central filesystem tracker in task/plugin context. Watches files in dev mode, tracks dependencies between modules, used by gen plugin to trigger regeneration.
Timings - performance tracking API.
const timing = timings.start('name'); await work(); timing(); logs duration.
Minimal abstraction - thin layer over tools (Vite, esbuild, Vitest), forwarding args and exposing internals. TypeScript everywhere (tasks, config, genfiles).
Core systems (src/lib/):
- CLI and task invocation:
gro.ts,invoke.ts,task.ts,invoke_task.ts,run_task.ts,input_path.ts - Code generation:
gen.ts,gen.task.ts,run_gen.ts,gen_helpers.ts - Plugins:
plugin.ts,gro_plugin_*.ts(gen, sveltekit_app, sveltekit_library, server) - Config:
gro_config.ts,gro.config.default.ts - Loader:
loader.ts,register.ts
Key utilities:
- Filesystem:
filer.ts,paths.ts - Module handling:
modules.ts,format_file.ts,package_json.ts,args.ts - SvelteKit integration:
svelte_config.ts,sveltekit_shim_*.ts,esbuild_plugin_sveltekit_*.ts - Build tools:
esbuild_helpers.ts,esbuild_plugin_svelte.ts,build_cache.ts
Documentation: Complete index at src/docs/README.md - Core topics: task, gen, plugin, config, test | Workflows: dev, build, deploy, publish