-
Notifications
You must be signed in to change notification settings - Fork 42
docs: add standalone mode documentation to CLAUDE.md #393
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -736,3 +736,241 @@ Main Egg.js plugin that integrates tegg into an Egg application. | |||||||||||||
| 2. `configDidLoad`: Setup EggContextHandler, ModuleHandler | ||||||||||||||
| 3. `didLoad`: Register hooks, call `moduleHandler.init()` to load all modules | ||||||||||||||
| 4. `beforeClose`: Cleanup and destroy | ||||||||||||||
|
|
||||||||||||||
| #### @eggjs/tegg-standalone (`standalone/standalone`) | ||||||||||||||
|
|
||||||||||||||
| Provides a standalone runtime for tegg that works without Egg.js. This enables using tegg's dependency injection in CLI tools, background workers, or any Node.js application. | ||||||||||||||
|
|
||||||||||||||
| ``` | ||||||||||||||
| ├── src/ | ||||||||||||||
| │ ├── Runner.ts # Main orchestrator: loads modules, manages lifecycle | ||||||||||||||
| │ ├── main.ts # Simple entry point: main() function | ||||||||||||||
| │ ├── EggModuleLoader.ts # Loads modules using tegg-loader | ||||||||||||||
| │ ├── StandaloneContext.ts # EggContext implementation for standalone | ||||||||||||||
| │ ├── StandaloneContextHandler.ts # AsyncLocalStorage-based context management | ||||||||||||||
| │ ├── StandaloneLoadUnit.ts # LoadUnit for inner objects (logger, config, etc.) | ||||||||||||||
| │ └── StandaloneInnerObjectProto.ts # EggPrototype for inner objects | ||||||||||||||
| └── index.ts # Exports all public APIs | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| ### Standalone Mode: How tegg Runs Without Egg.js | ||||||||||||||
|
|
||||||||||||||
| The standalone package replicates Egg.js plugin functionality using pure Node.js primitives: | ||||||||||||||
|
|
||||||||||||||
| **Key Differences from Egg.js Mode:** | ||||||||||||||
|
|
||||||||||||||
| | Aspect | Egg.js Mode | Standalone Mode | | ||||||||||||||
| |--------|------------|-----------------| | ||||||||||||||
| | Context storage | Egg.js request context | `AsyncLocalStorage` via `StandaloneContextHandler` | | ||||||||||||||
| | Context object | `EggContextImpl` (wraps Egg ctx) | `StandaloneContext` (extends `AbstractEggContext`) | | ||||||||||||||
| | Inner objects | From `app` and `ctx` | Manually provided via `innerObjectHandlers` | | ||||||||||||||
| | HTTP handling | Egg.js middleware | Not provided (bring your own) | | ||||||||||||||
| | Config loading | Egg.js config system | `ModuleConfigUtil` reads `module.yml` files | | ||||||||||||||
|
|
||||||||||||||
| **Initialization Flow:** | ||||||||||||||
|
|
||||||||||||||
| ``` | ||||||||||||||
| 1. new Runner(cwd, options) | ||||||||||||||
| ├── Read module references from cwd | ||||||||||||||
| ├── Load module configs (module.default.yml, module.{env}.yml) | ||||||||||||||
| ├── Register lifecycle hooks (AOP, DAL, etc.) | ||||||||||||||
| └── Create GlobalGraph instance | ||||||||||||||
|
|
||||||||||||||
| 2. runner.init() | ||||||||||||||
| ├── Register StandaloneContextHandler (AsyncLocalStorage) | ||||||||||||||
| ├── Register StandaloneLoadUnit factory (for inner objects) | ||||||||||||||
| ├── Load all modules via EggModuleLoader | ||||||||||||||
| │ ├── LoaderFactory.loadApp() → ModuleDescriptors | ||||||||||||||
| │ ├── GlobalGraph.create() → build dependency graph | ||||||||||||||
| │ ├── GlobalGraph.build() → resolve all injections | ||||||||||||||
| │ └── GlobalGraph.sort() → topological sort for init order | ||||||||||||||
| ├── Create LoadUnitInstance for each LoadUnit | ||||||||||||||
| └── Find @Runner decorated class | ||||||||||||||
|
|
||||||||||||||
| 3. runner.run() | ||||||||||||||
| ├── Create StandaloneContext | ||||||||||||||
| ├── ContextHandler.run(ctx, async () => {...}) | ||||||||||||||
| │ └── Uses AsyncLocalStorage.run() internally | ||||||||||||||
| ├── Get @Runner class instance via EggContainerFactory | ||||||||||||||
| ├── Call runner.main() | ||||||||||||||
| └── Cleanup context on completion | ||||||||||||||
|
|
||||||||||||||
| 4. runner.destroy() | ||||||||||||||
| ├── Destroy all LoadUnitInstances | ||||||||||||||
| ├── Destroy all LoadUnits | ||||||||||||||
| └── Unregister lifecycle hooks | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| **Context Management Without Egg.js:** | ||||||||||||||
|
|
||||||||||||||
| In Egg.js, each HTTP request has a context object (`ctx`). Standalone mode simulates this using `AsyncLocalStorage`: | ||||||||||||||
|
|
||||||||||||||
| ```typescript | ||||||||||||||
| // StandaloneContextHandler.ts | ||||||||||||||
| export class StandaloneContextHandler { | ||||||||||||||
| static storage = new AsyncLocalStorage<EggContext>(); | ||||||||||||||
|
|
||||||||||||||
| static register() { | ||||||||||||||
| // Tell ContextHandler how to get/run context | ||||||||||||||
| ContextHandler.getContextCallback = () => { | ||||||||||||||
| return StandaloneContextHandler.storage.getStore(); | ||||||||||||||
| }; | ||||||||||||||
| ContextHandler.runInContextCallback = (context, fn) => { | ||||||||||||||
| return StandaloneContextHandler.storage.run(context, fn); | ||||||||||||||
| }; | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| This allows `@ContextProto` objects to work correctly - they are stored per-context in the `AsyncLocalStorage`. | ||||||||||||||
|
|
||||||||||||||
| **Inner Objects (Replacing Egg.js app/ctx):** | ||||||||||||||
|
|
||||||||||||||
| In Egg.js, objects like `logger`, `config` come from `app` or `ctx`. Standalone mode uses `innerObjectHandlers`: | ||||||||||||||
|
|
||||||||||||||
| ```typescript | ||||||||||||||
| // Runner creates these automatically: | ||||||||||||||
| this.innerObjects = { | ||||||||||||||
| moduleConfigs: [{ obj: new ModuleConfigs(...) }], | ||||||||||||||
| moduleConfig: [...], // per-module configs with qualifiers | ||||||||||||||
| runtimeConfig: [{ obj: { baseDir, name, env } }], | ||||||||||||||
| mysqlDataSourceManager: [{ obj: MysqlDataSourceManager.instance }], | ||||||||||||||
| }; | ||||||||||||||
|
|
||||||||||||||
| // Users can add custom inner objects: | ||||||||||||||
| const runner = new Runner(cwd, { | ||||||||||||||
| innerObjectHandlers: { | ||||||||||||||
| logger: [{ obj: myLogger }], | ||||||||||||||
| customService: [{ | ||||||||||||||
| obj: myService, | ||||||||||||||
| qualifiers: [{ attribute: 'type', value: 'production' }], | ||||||||||||||
| }], | ||||||||||||||
| }, | ||||||||||||||
| }); | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| These inner objects are wrapped as `StandaloneInnerObjectProto` and registered in a special `StandaloneLoadUnit`, making them injectable via `@Inject()`. | ||||||||||||||
|
|
||||||||||||||
| ### Using Standalone Mode | ||||||||||||||
|
|
||||||||||||||
| **Basic Usage:** | ||||||||||||||
|
|
||||||||||||||
| ```typescript | ||||||||||||||
| // main.ts | ||||||||||||||
| import { ContextProto, Inject, SingletonProto } from '@eggjs/tegg'; | ||||||||||||||
| import { Runner, MainRunner } from '@eggjs/tegg/standalone'; | ||||||||||||||
|
|
||||||||||||||
| @SingletonProto() | ||||||||||||||
| export class MyService { | ||||||||||||||
| async doSomething() { | ||||||||||||||
| return 'hello'; | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| @ContextProto() | ||||||||||||||
| @Runner() // Mark as entry point | ||||||||||||||
| export class MyApp implements MainRunner<string> { | ||||||||||||||
| @Inject() | ||||||||||||||
| myService: MyService; | ||||||||||||||
|
|
||||||||||||||
| async main(): Promise<string> { | ||||||||||||||
| return await this.myService.doSomething(); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Run it | ||||||||||||||
| import { main } from '@eggjs/tegg-standalone'; | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check the main tegg package.json for exports configuration
find . -path "*/core/tegg/package.json" -o -path "*/tegg/package.json" | head -5 | xargs cat 2>/dev/null | grep -A 30 '"exports"' | head -40Repository: eggjs/tegg Length of output: 36 🏁 Script executed: #!/bin/bash
# Also check if there's a standalone package directory
find . -type d -name "standalone" | grep -v node_modulesRepository: eggjs/tegg Length of output: 90 🏁 Script executed: #!/bin/bash
# Check CLAUDE.md content around lines 882 and other import examples
sed -n '875,895p' CLAUDE.md
echo "---"
sed -n '855,870p' CLAUDE.md
echo "---"
sed -n '920,950p' CLAUDE.mdRepository: eggjs/tegg Length of output: 1643 🏁 Script executed: #!/bin/bash
# Search for package.json files and check their exports
find . -name "package.json" -type f | grep -E "(core/tegg|root)" | head -10 | xargs cat | grep -B5 -A50 '"exports"' | head -100Repository: eggjs/tegg Length of output: 36 🏁 Script executed: #!/bin/bash
# Try looking at the root package.json specifically
cat package.json | grep -A 50 '"exports"'Repository: eggjs/tegg Length of output: 36 🏁 Script executed: #!/bin/bash
# Check if there's a package.json in the standalone directory
if [ -f "./standalone/package.json" ]; then
cat ./standalone/package.json | head -20
fiRepository: eggjs/tegg Length of output: 36 🏁 Script executed: #!/bin/bash
# List root directory and common package locations
ls -la | head -20Repository: eggjs/tegg Length of output: 1231 🏁 Script executed: #!/bin/bash
# Find all package.json files
find . -name "package.json" -type f 2>/dev/null | head -20Repository: eggjs/tegg Length of output: 1239 🏁 Script executed: #!/bin/bash
# Check the standalone package.json if it exists
cat ./standalone/package.json 2>/dev/null || echo "File not found"Repository: eggjs/tegg Length of output: 68 🏁 Script executed: #!/bin/bash
# Search for actual imports in the codebase to see which pattern is used
rg '@eggjs/tegg-standalone|@eggjs/tegg/standalone' --type ts --type tsx -c | head -20Repository: eggjs/tegg Length of output: 81 🏁 Script executed: #!/bin/bash
# Find where standalone is located
find core plugin -type d -name "*standalone*" 2>/dev/nullRepository: eggjs/tegg Length of output: 79 🏁 Script executed: #!/bin/bash
# Check if standalone is a core package
ls -la core/ | grep standaloneRepository: eggjs/tegg Length of output: 123 🏁 Script executed: #!/bin/bash
# Look at CLAUDE.md line 121 area to see the subpaths list
sed -n '115,135p' CLAUDE.mdRepository: eggjs/tegg Length of output: 606 🏁 Script executed: #!/bin/bash
# Search for actual imports of both patterns in .ts files
rg '@eggjs/tegg-standalone|@eggjs/tegg/standalone' --type ts -A 2 | head -50Repository: eggjs/tegg Length of output: 3676 🏁 Script executed: #!/bin/bash
# Find all instances of both import patterns in CLAUDE.md
echo "=== Imports with SLASH (correct) ==="
rg '@eggjs/tegg/standalone' CLAUDE.md -n
echo ""
echo "=== Imports with HYPHEN (incorrect) ==="
rg '@eggjs/tegg-standalone' CLAUDE.md -nRepository: eggjs/tegg Length of output: 496 🏁 Script executed: #!/bin/bash
# Show the context around the examples to understand the scope
sed -n '850,860p' CLAUDE.md
echo "---"
sed -n '878,895p' CLAUDE.md
echo "---"
sed -n '918,932p' CLAUDE.md
echo "---"
sed -n '940,955p' CLAUDE.mdRepository: eggjs/tegg Length of output: 1540 Fix import paths in standalone mode examples. Lines 882, 891, 923, and 945 incorrectly import from 🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| const result = await main<string>(__dirname); | ||||||||||||||
| console.log(result); // 'hello' | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| **With Dependencies and Custom Objects:** | ||||||||||||||
|
|
||||||||||||||
| ```typescript | ||||||||||||||
| import { main, Runner } from '@eggjs/tegg-standalone'; | ||||||||||||||
|
|
||||||||||||||
| const result = await main(__dirname, { | ||||||||||||||
| // Environment name (affects config loading) | ||||||||||||||
| env: 'production', | ||||||||||||||
|
|
||||||||||||||
| // Application name | ||||||||||||||
| name: 'my-app', | ||||||||||||||
|
|
||||||||||||||
| // Additional module paths to load | ||||||||||||||
| dependencies: [ | ||||||||||||||
| '/path/to/other/module', | ||||||||||||||
| { baseDir: '/path/to/module', extraFilePattern: ['!**/test'] }, | ||||||||||||||
| ], | ||||||||||||||
|
|
||||||||||||||
| // Custom injectable objects | ||||||||||||||
| innerObjectHandlers: { | ||||||||||||||
| logger: [{ obj: myLogger }], | ||||||||||||||
| config: [{ | ||||||||||||||
| obj: myConfig, | ||||||||||||||
| qualifiers: [{ attribute: 'env', value: 'production' }], | ||||||||||||||
| }], | ||||||||||||||
| }, | ||||||||||||||
|
|
||||||||||||||
| // Disable module descriptor dump (default: true) | ||||||||||||||
| dump: false, | ||||||||||||||
| }); | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| **Advanced: Manual Runner Control:** | ||||||||||||||
|
|
||||||||||||||
| ```typescript | ||||||||||||||
| import { Runner, StandaloneContext } from '@eggjs/tegg-standalone'; | ||||||||||||||
|
|
||||||||||||||
| const runner = new Runner(__dirname, options); | ||||||||||||||
| await runner.init(); | ||||||||||||||
|
|
||||||||||||||
| // Custom context with pre-set values | ||||||||||||||
| const ctx = new StandaloneContext(); | ||||||||||||||
| ctx.set('requestId', '12345'); | ||||||||||||||
|
|
||||||||||||||
| // Run multiple times with different contexts | ||||||||||||||
| const result1 = await runner.run(ctx); | ||||||||||||||
| const result2 = await runner.run(new StandaloneContext()); | ||||||||||||||
|
|
||||||||||||||
| // Cleanup when done | ||||||||||||||
| await runner.destroy(); | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| **PreLoad for Faster Startup:** | ||||||||||||||
|
|
||||||||||||||
| Use `preLoad()` to run lifecycle `preLoad` hooks before `main()`. This is useful for warming up caches or validating configuration: | ||||||||||||||
|
|
||||||||||||||
| ```typescript | ||||||||||||||
| import { preLoad, main } from '@eggjs/tegg-standalone'; | ||||||||||||||
|
|
||||||||||||||
| // Run preLoad hooks (e.g., validate config, warm caches) | ||||||||||||||
| await preLoad(__dirname, dependencies); | ||||||||||||||
|
|
||||||||||||||
| // Then run main | ||||||||||||||
| const result = await main(__dirname, { dependencies }); | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| ### Creating Standalone-Compatible Plugins | ||||||||||||||
|
|
||||||||||||||
| Plugins that support standalone mode need an `eggModule` field in `package.json`: | ||||||||||||||
|
|
||||||||||||||
| ```json | ||||||||||||||
| { | ||||||||||||||
| "name": "@eggjs/tegg-my-plugin", | ||||||||||||||
| "eggPlugin": { | ||||||||||||||
| "name": "myPlugin", | ||||||||||||||
| "dependencies": ["tegg"] | ||||||||||||||
| }, | ||||||||||||||
| "eggModule": { | ||||||||||||||
| "name": "teggMyPlugin" | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| The `eggModule.name` is used by standalone mode to identify the module. The plugin's prototypes will be loaded automatically when the plugin path is included in `dependencies`. | ||||||||||||||
|
|
||||||||||||||
| Plugins should avoid depending on Egg.js-specific APIs (`app`, `ctx`) directly. Instead: | ||||||||||||||
| - Use `@Inject()` for dependencies | ||||||||||||||
| - Use lifecycle hooks (`init()`, `destroy()`) instead of Egg.js lifecycle | ||||||||||||||
| - Provide factories that accept configuration via injection | ||||||||||||||
|
Comment on lines
+974
to
+976
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For consistency and improved readability, it's good practice to end each item in a list with a period, especially when they function as complete sentences or imperative clauses.
Suggested change
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In an
asyncfunction, you can directly return a promise withoutawait-ing it. The function will automatically adopt the state of the returned promise. Removing the redundantawaitmakes the code slightly more concise and is a good practice to demonstrate in documentation.