Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 238 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In an async function, you can directly return a promise without await-ing it. The function will automatically adopt the state of the returned promise. Removing the redundant await makes the code slightly more concise and is a good practice to demonstrate in documentation.

Suggested change
return await this.myService.doSomething();
return this.myService.doSomething();

}
}

// Run it
import { main } from '@eggjs/tegg-standalone';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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 -40

Repository: 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_modules

Repository: 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.md

Repository: 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 -100

Repository: 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
fi

Repository: eggjs/tegg

Length of output: 36


🏁 Script executed:

#!/bin/bash
# List root directory and common package locations
ls -la | head -20

Repository: 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 -20

Repository: 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 -20

Repository: eggjs/tegg

Length of output: 81


🏁 Script executed:

#!/bin/bash
# Find where standalone is located
find core plugin -type d -name "*standalone*" 2>/dev/null

Repository: eggjs/tegg

Length of output: 79


🏁 Script executed:

#!/bin/bash
# Check if standalone is a core package
ls -la core/ | grep standalone

Repository: 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.md

Repository: 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 -50

Repository: 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 -n

Repository: 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.md

Repository: eggjs/tegg

Length of output: 1540


Fix import paths in standalone mode examples.

Lines 882, 891, 923, and 945 incorrectly import from '@eggjs/tegg-standalone' (with hyphen). The correct subpath format is '@eggjs/tegg/standalone' (with slash), which aligns with the documented subpaths list and the correct example at line 861. Update all four examples to use the slash format.

🤖 Prompt for AI Agents
In @CLAUDE.md at line 882, Update the incorrect imports that use the hyphenated
package name '@eggjs/tegg-standalone' to the correct subpath
'@eggjs/tegg/standalone' in all standalone-mode examples; specifically replace
occurrences that import symbols like "main" (and any other imports) from
'@eggjs/tegg-standalone' so they import from '@eggjs/tegg/standalone' instead.


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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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
- Use `@Inject()` for dependencies
- Use lifecycle hooks (`init()`, `destroy()`) instead of Egg.js lifecycle
- Provide factories that accept configuration via injection
- Use `@Inject()` for dependencies.
- Use lifecycle hooks (`init()`, `destroy()`) instead of Egg.js lifecycle.
- Provide factories that accept configuration via injection.

Loading