Safe, deterministic scaffolding and refactoring tool for Astro projects
npm install @oamm/textorA safe, deterministic scaffolding and refactoring CLI tool for Astro + modern frontend projects.
Textor enforces strict separation of concerns:
- Pages = Thin routing adapters
- Features = Self-contained implementation modules
- Components = Reusable UI primitives
The tool is designed to be production-grade, principal-engineer approved, and optimized for large teams and long-lived codebases.
1. Explicit over implicit β No magic behavior.
2. Safe by default β Never delete or overwrite hand-written code.
3. Configurable over hardcoded β Everything driven by configuration.
4. Reversible operations β Add, move, and remove operations are always supported.
5. Deterministic output β Same input always produces the same structure.
``bashIn your Astro project
pnpm add -D @oamm/textor
This creates .textor/config.json with default settings and initializes the state tracker.
ποΈ Scaffolding Presets
Control structural complexity using presets. Presets apply to both add-section and create-component.
- minimal: Lean defaults, minimal folders. Ideal for simple components and features.
- standard: Balanced structure. Includes hooks, tests, and types by default.
- senior: Full enterprise-grade layout including API, services, schemas, and documentation.
Select a preset via the --preset flag:
`bash
pnpm textor add-section /users users/catalog --preset senior
`πΊοΈ Routing Mode
Textor supports explicit routing strategies to avoid ambiguity in Astro projects.
- flat: /users β src/pages/users.astro (Default)
- nested: /users β src/pages/users/index.astro
Configure this in .textor/config.json:
`json
{
"routing": {
"mode": "nested",
"indexFile": "index.astro"
}
}
`Textor supports multiple frameworks (React, Astro). By default, components are created as React components (
.tsx), while features and pages are Astro components (.astro).Configure this in
.textor/config.json:
`json
{
"components": {
"framework": "react"
},
"features": {
"framework": "astro"
}
}
`π¦ Feature Entry Strategy
Avoid filename collisions for feature entry files.
- pascal: src/features/users/catalog/UsersCatalog.astro (Default)
- index: src/features/users/catalog/index.astro
Configure this in .textor/config.json:
`json
{
"features": {
"entry": "pascal"
}
}
`File Naming Patterns
You can override generated file names for feature and component sub-files (api, services, hooks, tests, etc.) using simple patterns. Patterns support
{{componentName}}, {{hookName}}, {{hookExtension}}, {{testExtension}}, {{componentExtension}}, and {{featureExtension}}.Example:
`json
{
"filePatterns": {
"features": {
"api": "{{componentName}}.route.ts"
},
"components": {
"api": "{{componentName}}.route.ts"
}
}
}
`More examples:
`json
{
"filePatterns": {
"features": {
"hook": "use{{componentName}}.ts",
"test": "{{componentName}}.spec{{testExtension}}",
"readme": "{{componentName}}.md"
},
"components": {
"api": "{{componentName}}.route{{testExtension}}",
"services": "{{componentName}}.service{{hookExtension}}",
"stories": "{{componentName}}.stories.tsx"
}
}
}
`π‘οΈ Safety & File Tracking
Textor uses a multi-layered safety approach to protect your codebase.
$3
Every file generated by Textor includes a signature (e.g., ). Textor will refuse to delete or move any file missing this signature unless --force is used.$3
Textor maintains a .textor/state.json file that tracks:
- File paths and kind (route, feature, component)
- Templates used
- Content hashes
- Creation timestamps$3
Safe deletion and moves require:
1. Valid Textor signature in the file.
2. Presence of the file in the state.
3. Hash match (verifying the file hasn't been manually edited).If you have manually edited a Textor-generated file and wish to remove or move it, use --accept-changes or --force.
π οΈ Commands
$3
Create a route + feature binding, or a standalone feature.`bash
pnpm textor add-section [route] [options]
`If
route is provided, Textor creates both a route adapter (e.g., in src/pages) and a feature module. If route is omitted, Textor scaffolds only the feature module. This is useful for features that are shared across multiple pages or used as sub-parts of other features.Examples:
`bash
Create a section with a route
pnpm textor add-section /users users/catalogCreate a standalone feature (no route file)
pnpm textor add-section auth/login
`Options:
- --preset : scaffolding preset (minimal, standard, senior)
- --layout : Layout component name (use "none" for no layout)
- --name : Custom name for easier state lookup
- --endpoint: Create an API endpoint (.ts) instead of an Astro page
- --dry-run: Preview changes without writing to disk
$3
Create reusable UI components.`bash
pnpm textor create-component [options]
`Options:
- --preset : scaffolding preset (minimal, standard, senior)
- --dry-run: Preview changes without writing to disk
$3
Move and rename sections safely. Textor performs deep renaming: if the feature name changes, all internal files and component signatures are updated automatically.`bash
Using state lookup
pnpm textor move-section /old /new
`$3
Rename a route, feature, or component. Ideal for correcting typos. Supports deep renaming and optional repo-wide import updates.`bash
Rename a route (URL)
pnpm textor rename route /lgoin /loginRename a feature module (includes internal files and component names)
pnpm textor rename feature auth/lgoin auth/loginRename a shared component
pnpm textor rename component Header SiteHeader
`Use
--scan to update imports across the entire project.$3
Add a new sub-item (api, hook, test, etc.) to an existing feature or component. This command is additive and will not overwrite existing files unless --force is used.`bash
Add API to an existing feature
pnpm textor add api auth/loginAdd hooks to an existing component
pnpm textor add hook ButtonAdd tests and readme to a feature
pnpm textor add test auth/login
pnpm textor add readme auth/login
`Textor automatically detects if the target is a feature or a component based on its state.
You can also use the original
add-section or create-component commands with the same flags to add items; they now also support additive mode.$3
Safely remove Textor-managed modules.`bash
Remove by route
pnpm textor remove-section /usersRemove a standalone feature by its name or path
pnpm textor remove-section auth/login
`$3
List all Textor-managed modules, including their architectural capabilities (API, Hooks, etc.).$3
Show drift between state and disk. This is a read-only command used to build trust and identify manual changes or missing files.`bash
pnpm textor status
`Categories:
- SYNCED: File content matches state exactly.
- MODIFIED: File exists but content differs from state hash.
- MISSING: File is registered in state but missing on disk.
- UNTRACKED: File has a Textor signature but is not in state.
- ORPHANED: File is in a managed directory but has no Textor signature and is not in state.
$3
Validate that the state file matches the project files.$3
Synchronize the state with the actual files in managed directories. This is useful for including existing files into Textor's state or updating hashes after manual edits.`bash
pnpm textor sync [options]
`Options:
-
--include-all: Include all files in managed directories, even without the Textor signature.
- --force: Update hashes for modified files even if they don't have the Textor signature.
- --dry-run: Preview what would be synchronized without making changes.$3
Adopt untracked files into Textor's state and add the Textor signature to them. This is the recommended way to bring manually created components or sections under Textor's management.`bash
pnpm textor adopt [options]
`Options:
-
--all: Scan all managed directories for untracked files and adopt them.
- --dry-run: Preview which files would be adopted and modified.Examples:
`bash
Adopt a specific component directory
pnpm textor adopt src/components/MyNewButtonAdopt a section by its route
pnpm textor adopt /usersAdopt all untracked files in the project
pnpm textor adopt --all
`$3
Upgrade .textor/config.json to the latest schema version without recreating it.`bash
pnpm textor upgrade-config
`Options:
-
--dry-run: Print the upgraded config without writing it.$3
Normalize .textor/state.json to use project-relative paths (helpful when moving between machines).`bash
pnpm textor normalize-state
`Options:
-
--dry-run: Print the normalized state without writing it.$3
Remove missing references from Textor state. This command identifies files that are tracked in the state but are no longer present on disk and removes them from .textor/state.json. Safe-by-default: This command never deletes any files from your disk; it only updates the state tracker.
`bash
pnpm textor prune-missing
`Options:
-
--dry-run: Preview what would be removed from the state without making changes.
- --yes: Skip confirmation prompt.
- --no-interactive: Disable interactive prompts (useful for CI).ποΈ Technical Architecture
Textor is designed with enterprise-grade robustness, moving beyond simple scaffolding to provide a reliable refactoring engine.
$3
State updates to .textor/state.json are atomic. Textor writes to a temporary file (state.json.tmp) and performs a cross-platform rename to the final destination. This prevents state corruption during crashes or interrupted operations.$3
Textor uses SHA-256 for file integrity. It supports multiple normalization strategies:
- normalizeEOL (Default): Converts \r\n to \n before hashing. Ensures Git "autocrlf" settings don't trigger false alerts.
- stripGeneratedRegions: Hashes only the content within @generated by Textor:begin and @generated by Textor:end markers. This allows developers to add hand-written code outside these regions without breaking Textor's integrity checks.
- none: Strict hashing of the entire file.$3
Every file in the state has an owner.
- A Section owns its route adapter and its associated feature directory.
- A Component owns its specific component directory.
Textor enforces ownership boundaries to prevent accidental deletion of shared resources and to ensure deep refactors only touch relevant files. Use --force to override ownership checks.$3
Textor can infer the "kind" of a file (e.g., route, feature, component-file) during synchronization. You can define custom rules in .textor/config.json:
`json
{
"kindRules": [
{ "match": "src/features/custom/**", "kind": "custom-logic" },
{ "match": "**/special.ts", "kind": "special-file" }
]
}
`$3
Textor integrates with Git to provide a "safety net":
- requireCleanRepo: When enabled, Textor refuses to perform destructive operations (remove/move) if the repository has uncommitted changes.
- stageChanges: When enabled, Textor automatically stages (git add) all created or modified files after a successful command.$3
When moving or renaming sections, Textor performs scoped AST-like updates:
- Scope: Updates imports in Textor-managed files and route adapters by default.
- Repo-wide: Use the --scan flag to scan the entire repository for imports that need to be updated.
- Exclusions: String literals, markdown documentation (unless registered), and complex dynamic imports are preserved to avoid breaking hand-written logic.---
βοΈ Configuration
The .textor/config.json file allows full control over the tool's behavior.
configVersion tracks schema changes and is updated by textor upgrade-config.`json
{
"configVersion": 2,
"paths": {
"pages": "src/pages",
"features": "src/features",
"components": "src/components",
"layouts": "src/layouts"
},
"routing": {
"mode": "flat",
"indexFile": "index.astro"
},
"importAliases": {
"layouts": "@/layouts",
"features": "@/features"
},
"naming": {
"routeExtension": ".astro",
"featureExtension": ".astro",
"componentExtension": ".tsx",
"hookExtension": ".ts",
"testExtension": ".test.tsx"
},
"signatures": {
"astro": "",
"typescript": "// @generated by Textor",
"javascript": "// @generated by Textor",
"tsx": "// @generated by Textor"
},
"features": {
"framework": "astro",
"entry": "pascal",
"createSubComponentsDir": true,
"createScriptsDir": true,
"scriptsIndexFile": "scripts/index.ts",
"createApi": false,
"createServices": false,
"createSchemas": false,
"createHooks": false,
"createContext": false,
"createTests": false,
"createTypes": false,
"createReadme": false,
"createStories": false,
"createIndex": false,
"layout": "Main"
},
"components": {
"framework": "react",
"createSubComponentsDir": true,
"createContext": true,
"createHook": true,
"createTests": true,
"createConfig": true,
"createConstants": true,
"createTypes": true,
"createApi": false,
"createServices": false,
"createSchemas": false,
"createReadme": false,
"createStories": false
},
"formatting": {
"tool": "none"
},
"hashing": {
"normalization": "normalizeEOL"
},
"git": {
"requireCleanRepo": false,
"stageChanges": false
},
"defaultPreset": "standard",
"presets": {
"minimal": {
"features": { "createSubComponentsDir": false, "createScriptsDir": false },
"components": {
"createSubComponentsDir": false,
"createContext": false,
"createHook": false,
"createTests": false,
"createConfig": false,
"createConstants": false,
"createTypes": false
}
},
"standard": {
"features": { "createSubComponentsDir": true, "createScriptsDir": true },
"components": {
"createSubComponentsDir": true,
"createContext": true,
"createHook": true,
"createTests": true,
"createConfig": true,
"createConstants": true,
"createTypes": true
}
},
"senior": {
"features": {
"createSubComponentsDir": true,
"createScriptsDir": true,
"createApi": true,
"createServices": true,
"createSchemas": true,
"createHooks": true,
"createContext": true,
"createTests": true,
"createTypes": true,
"createReadme": true,
"createStories": true,
"createIndex": true
},
"components": {
"createSubComponentsDir": true,
"createContext": true,
"createHook": true,
"createTests": true,
"createConfig": true,
"createConstants": true,
"createTypes": true,
"createApi": true,
"createServices": true,
"createSchemas": true,
"createReadme": true,
"createStories": true
}
}
}
}
`
Supported formatting tools: prettier, biome, none.$3
You can pass parameters to your layout component by defining
layoutProps in .textor/config.json. These props support variable substitution.`json
{
"features": {
"layout": "AppLayout",
"layoutProps": {
"title": "{{componentName}}",
"description": "Description for {{componentName}}"
}
}
}
`You can also override these props via the CLI using the
--prop flag:
`bash
pnpm textor add-section /users users/roles --prop title="Custom Title" --prop breadcrumbs='{[{ label: "Users" }]}'
`Properties that start and end with curly braces
{} are passed as JavaScript expressions, others as strings.---
π Template Overrides
You can customize the code generated by Textor by providing your own templates. Textor looks for override files in the
.textor/templates/ directory at your project root.$3
1. Create the
.textor/templates/ directory if it doesn't exist.
2. Create a file named according to the table below (e.g., feature.astro or component.tsx).
3. Use {{variable}} or __variable__ placeholders in your template. Textor will automatically replace them when generating files. Using __variable__ (e.g., __componentName__) is recommended for TypeScript/JavaScript templates as it is a valid identifier and avoids "broken code" warnings in your IDE.$3
| Template Name | File to create in
.textor/templates/ | Available Variables |
| :--- | :--- | :--- |
| Route | route.astro | {{layoutName}}, {{layoutImportPath}}, {{featureImportPath}}, {{featureComponentName}}, plus any layoutProps |
| Feature | feature.astro or feature.tsx | {{componentName}}, {{scriptImportPath}} |
| Component | component.astro or component.tsx | {{componentName}} |
| Hook | hook.ts | {{componentName}}, {{hookName}} |
| Context | context.tsx | {{componentName}} |
| Test | test.tsx | {{componentName}}, {{componentPath}} |
| Index | index.ts | {{componentName}}, {{componentExtension}} |
| Types | types.ts | {{componentName}} |
| API | api.ts | {{componentName}} |
| Endpoint | endpoint.ts | {{componentName}} |
| Service | service.ts | {{componentName}} |
| Schema | schema.ts | {{componentName}} |
| Readme | readme.md | {{componentName}} |
| Stories | stories.tsx | {{componentName}}, {{componentPath}} |
| Config | config.ts | {{componentName}} |
| Constants | constants.ts | {{componentName}} |
| Scripts Index| scripts-index.ts | (none) |> Note: For
feature and component templates, use the extension that matches your configured framework (.astro for Astro, .tsx for React). Other templates have fixed extensions for the override file, regardless of your project's configuration.$3
-
{{componentName}}: The PascalCase name of the feature or component (e.g., UserCatalog).
- {{componentNameCamel}}: camelCase version of the name (e.g., userCatalog).
- {{componentNameKebab}}: kebab-case version of the name (e.g., user-catalog).
- {{componentNameSnake}}: snake_case version of the name (e.g., user_catalog).
- {{componentNameUpper}}: SCREAMING_SNAKE_CASE version of the name (e.g., USER_CATALOG).
- {{hookName}}: The camelCase name of the generated hook (e.g., useUserCatalog).
- {{componentPath}}: Relative path to the component file (useful for imports in tests or stories).
- {{featureComponentName}}: The name of the feature component as imported in a route.
- {{featureComponentNameCamel}}, {{featureComponentNameKebab}}, {{featureComponentNameSnake}}, {{featureComponentNameUpper}}, {{featureComponentNamePascal}}: Case variations for the feature component name.
- {{layoutName}}: The name of the layout component being used.
- {{layoutNameCamel}}, {{layoutNameKebab}}, {{layoutNameSnake}}, {{layoutNameUpper}}, {{layoutNamePascal}}: Case variations for the layout name.
- {{layoutImportPath}}: The import path for the layout component.
- {{scriptImportPath}}: Relative path to the client-side script entry point.
- {{componentExtension}}: The file extension of the component (e.g., .astro or .tsx).$3
If you are using a custom routing library and want your templates to be valid TypeScript:
`typescript
/**
* @generated by Textor
* Route: {{featureComponentName}}
*/
import { defineRoute } from "my-router";
import __featureComponentName__ from "{{featureImportPath}}";export const __featureComponentName__Route = defineRoute({
path: "/__featureComponentNameKebab__",
component: __featureComponentName__
});
`$3
`astro
---
/**
* @generated by Textor
* Feature: {{componentName}}
*/interface Props {
title?: string;
}
const { title = "{{componentName}}" } = Astro.props;
---
{title}
``---
Textor is designed to be a tool you trust to refactor a 3-year-old production codebase without fear.