Skip to content
Merged
Show file tree
Hide file tree
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
120 changes: 120 additions & 0 deletions skills/feature-arch/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
---
name: feature-arch
description: >
A skill for feature-based development using the js-plugin package in Node.js or frontend
applications. Use this whenever the user is working with features under a src/features/
directory — creating features, wiring them via extension points, updating specs, or
managing feature boundaries.
---

# Feature-Based Development Skill

This skill describes a feature-based development approach powered by the [js-plugin](./references/js-plugin.md) package. It applies to both Node.js backend and frontend UI applications.

**Before proceeding with any feature work, read `./references/js-plugin.md`** to understand the plugin API and extension point patterns. The entire approach depends on it.

---

## Folder Structure

Each feature lives in its own subdirectory under `src/features/`:

```
src/features/
├── feature1/
│ ├── ext/
│ │ └── index.js # Extension point contributions from this feature
│ ├── index.js # Feature plugin definition
│ └── FEATURE_SPEC.md # Feature specification
├── feature2/
│ ├── ext/
│ │ └── index.js
│ ├── index.js
│ └── FEATURE_SPEC.md
└── ...
```

Tip: you can use `.ts[x]` , `.js[x]` for index files, not limited to `.js`.

---

## What Is a Feature?

A feature is a cohesive group of related capabilities. All features work together to form the complete application, but each one is independently removable.

**Core rules:**

- Every feature lives as a subdirectory under `src/features/`.
- Every feature is implemented as a js-plugin plugin.
- A feature must be removable without breaking the rest of the application — its absence degrades functionality gracefully rather than causing crashes or errors.
- Features communicate with each other exclusively through **extension points** and **exports** — never through direct imports across feature boundaries.
- Add extension points or exports to a feature only when integration with another feature actually requires it.

---

## FEATURE_SPEC.md

Every feature folder must contain a `FEATURE_SPEC.md` file. This file defines the feature's boundary and serves as the source of truth for its capabilities.

**Purpose:** The spec establishes what the feature does and how it does it — not why it exists. Keep it precise and current.

**When to update:**

- Add an entry whenever a new capability is added to the feature.
- When the user manually edits the spec, treat it as an implementation directive and apply the changes in code.

**Boundary enforcement:** If a proposed change seems to exceed the feature's defined scope, pause and ask the user whether a new feature is needed rather than expanding the current one silently.

**Suggested sections** — include only what's relevant, and add any other sections that help describe the feature clearly:

| Section | Content |
| ---------------------------- | ----------------------------------------------------------------- |
| **Overview** | A short, accurate description of what the feature does |
| **UI Requirements** | Layout, components, interactions, and visual behavior |
| **Performance Requirements** | Loading targets, caching strategy, optimization constraints |
| **Security Requirements** | Auth checks, data access rules, input validation |
| **Extensibility** | Extension points exposed, global shared modal IDs, plugin exports |

Keep the spec concise. Avoid rationale or background — focus on _what_ and _how_.

---

## Workflow

### Creating a new feature

When the user asks to create a new feature:

1. Create a subdirectory under `src/features/` with a name that reflects the feature's purpose.
2. Set up the standard folder structure (`ext/index.js`, `index.js`, `FEATURE_SPEC.md`).
3. Write an initial `FEATURE_SPEC.md` with an accurate overview and placeholder sections for UI, performance, security, and extensibility.
4. Scaffold the js-plugin plugin in `index.js`.

### Implementing or modifying a feature

When adding or changing capabilities:

1. Review the feature's `FEATURE_SPEC.md` to confirm the change is in scope.
2. If the change is in scope, implement it and update the spec to reflect the new capability.
3. If the change appears out of scope, ask the user whether to expand the spec or create a new feature.
4. After applying any code changes, verify that the implementation matches the spec.

### Wiring features together

Use extension points for inter-feature communication:

- A feature that needs to expose behavior for others defines an extension point in its plugin.
- A feature that contributes to another's extension point does so in its `ext/index.js`.
- Never import directly from another feature's internal modules — always go through the plugin's public interface.

---

## Quick Reference

| Question | Answer |
| ---------------------------------------------- | ------------------------------------------------------------- |
| Where do new features go? | `src/features/<feature-name>/` |
| How do features share behavior? | Extension points and plugin exports |
| What happens if I remove a feature? | App still runs; that feature's capabilities are simply absent |
| Where is a feature's contract defined? | `FEATURE_SPEC.md` |
| When do I create a new feature vs. extend one? | When the capability is outside the existing spec's boundary |
270 changes: 270 additions & 0 deletions skills/feature-arch/references/js-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
# js-plugin Reference

`js-plugin` is a lightweight, general-purpose plugin engine for building extensible JavaScript applications. It provides the **extension point** pattern that lets independent features discover and integrate with each other without direct coupling.

Works in both browser and Node.js environments. If you need to understand internals (caching, registration ordering, invocation mechanics), read the source directly — it's ~150 lines.

---

## The Problem It Solves

Without a plugin system, adding features requires modifying shared files that every feature touches:

```javascript
// menu.js — WITHOUT js-plugin
function Menu() {
return (
<ul>
<li>Profile</li>
<li>Account</li>
{/* Every new feature must edit this file */}
<li>Blocked Users</li>
<li>API Keys</li>
</ul>
);
}
```

**With js-plugin**, each feature registers its own contributions, and shared components collect them dynamically:

```javascript
// menu.js — WITH js-plugin
import plugin from 'js-plugin';

function Menu() {
const items = plugin.invoke('menu.getItems');
return <ul>{items.map(item => <li key={item.key}>{item.label}</li>)}</ul>;
}

// blocked-users/index.js
plugin.register({
name: 'blocked-users',
menu: { getItems: () => ({ key: 'blocked', label: 'Blocked Users' }) }
});

// api-keys/index.js
plugin.register({
name: 'api-keys',
menu: { getItems: () => ({ key: 'api-keys', label: 'API Keys' }) }
});
```

`menu.js` never changes when features are added or removed. Each feature's code stays self-contained.

---

## API Reference

### `plugin.register(pluginObject)`

Register a feature plugin. Call this at module top-level in the feature's entry file, before the app renders.

```javascript
import plugin from 'js-plugin';

plugin.register({
name: 'notifications', // Required: unique identifier
deps: ['auth'], // Optional: plugins that must be registered first
initialize() { // Optional: called immediately after registration
console.log('notifications ready');
},
// Everything else is an extension point contribution:
menu: {
getItems: () => [{ key: 'notif', label: 'Notifications', order: 20 }]
},
route: { path: '/notifications', component: NotificationsPage }
});
```

- If a declared `deps` entry is missing from the registry, the plugin is excluded from all invocations and a console warning is logged.
- Never register conditionally — plugins should be statically registered.

---

### `plugin.invoke(extPoint, ...args)`

Collect contributions from all plugins that implement an extension point. Returns an array — one entry per contributing plugin.

| Syntax | Behavior |
|---|---|
| `plugin.invoke('a.b')` | Calls `a.b` as a function if it is one; otherwise returns the value |
| `plugin.invoke('!a.b')` | Always returns the value without calling, even if it's a function |
| `plugin.invoke('a.b!')` | Same as default, but throws errors instead of swallowing them |

```javascript
// Call a lifecycle hook on every feature
plugin.invoke('onInit');

// Collect plain values
const routes = plugin.invoke('!route');
// → [{ path: '/a', component: A }, { path: '/b', component: B }]

// Call with arguments
const items = plugin.invoke('menu.getItems', currentUser);
// → calls menu.getItems(currentUser) on each plugin that defines it

// Collect functions to call later
const getters = plugin.invoke('!getHeaderWidget');
// → [fn1, fn2] — call when ready: getters.map(fn => fn())
```

---

### `plugin.getPlugin(name)`

Look up a specific plugin by name. Returns the plugin object or `undefined`.

```javascript
const auth = plugin.getPlugin('auth');
if (auth) {
const user = auth.exports.getCurrentUser();
}
```

**Never call at module top-level** — the registry populates as modules load, so other plugins may not be registered yet:

```javascript
// ❌ Too early
const auth = plugin.getPlugin('auth');

// ✅ Inside a function, called after all plugins are loaded
function handleLogin() {
const auth = plugin.getPlugin('auth');
auth?.exports.login();
}
```

---

### `plugin.getPlugins(extPoint?)`

Get all plugins contributing to an extension point. Omit the argument to get all registered plugins.

```javascript
const all = plugin.getPlugins();
const routed = plugin.getPlugins('route');
const contributors = plugin.getPlugins('menu.getItems');
```

Plugins with unresolved dependencies are automatically excluded.

---

### `plugin.sort(array, sortProp?)`

Sort an array of objects by a numeric property (default: `'order'`), in-place. Objects missing the property go to the end.

```javascript
const items = plugin.invoke('menu.getItems').flat();
plugin.sort(items); // sorts by item.order
```

---

### `plugin.unregister(name)`

Remove a plugin from the registry. Mainly useful in tests and hot-reload scenarios.

---

### `plugin.config`

```javascript
plugin.config.throws = true; // Make all invocations throw on error
```

---

## Exports Pattern

`exports` is a js-plugin convention for sharing APIs, utilities, components, or services between features. Define an `exports` property on your plugin registration — other features access it via `plugin.getPlugin()`.

```javascript
// feature-a/index.js
import plugin from 'js-plugin';
import * as hooks from './hooks';
import * as utils from './utils';
import apiClient from './apiClient';

plugin.register({
name: 'feature-a',
exports: {
hooks, // e.g. useFeatureAData, useFeatureAAuth
utils, // e.g. formatItem, parseConfig
apiClient // configured axios instance or similar
}
});
```

```javascript
// feature-b/SomeComponent.jsx
import plugin from 'js-plugin';

function SomeComponent() {
const { hooks, utils } = plugin.getPlugin('feature-a')?.exports || {};
const data = hooks?.useFeatureAData();
// ...
}
```

**When to use exports vs extension points:**

- Use **exports** when one feature needs to *consume* APIs or code owned by another — hooks, utilities, configured service clients, shared components.
- Use **extension points** when a feature wants to let others *contribute* capabilities to it. Exports create direct coupling; extension points stay loose.

**Avoid calling `plugin.getPlugin()` at module top level** — the registry may not be fully populated yet when the module first evaluates. Always call it inside a function, component, or hook.

```javascript
// ❌ Too early — feature-a may not be registered yet
const { hooks } = plugin.getPlugin('feature-a').exports;

// ✅ Inside a function — safe
function MyComponent() {
const { hooks } = plugin.getPlugin('feature-a')?.exports || {};
}
```

Document your feature's exports in its `FEATURE_SPEC.md` so other features know what's available and how to access it.

---

## Extension Point Conventions

### Use nested objects, not string keys

Extension points are resolved by traversing object properties, not by parsing dot-notation strings:

```javascript
// ✅ Correct
plugin.register({
name: 'my-feature',
layout: { sidebar: { getItems: () => [...] } }
});

// ❌ Wrong — string path keys are not traversed
plugin.register({
name: 'my-feature',
'layout.sidebar.getItems': () => [...]
});
```

### Extension points are implicitly defined

There is no central registry of extension points. A point exists when a consumer calls `plugin.invoke('some.point')` and contributors register a matching property path. Document your feature's extension points in `FEATURE_SPEC.md` so other features know how to contribute.

### Two roles for every extension point

```javascript
// Provider — defines the extension point by consuming it
function Sidebar() {
const items = plugin.invoke('sidebar.getItems').flat();
plugin.sort(items);
return <nav>{items.map(renderItem)}</nav>;
}

// Contributor — any feature that wants to appear in the sidebar
plugin.register({
name: 'reports',
sidebar: { getItems: () => [{ key: 'reports', label: 'Reports', order: 40 }] }
});
```