Skip to content

Commit dba2d3e

Browse files
authored
Merge pull request #47 from supnate/skill-dev
add project scopped skill
2 parents f33c7db + 78b3ae6 commit dba2d3e

2 files changed

Lines changed: 390 additions & 0 deletions

File tree

skills/feature-arch/SKILL.md

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
---
2+
name: feature-arch
3+
description: >
4+
A skill for feature-based development using the js-plugin package in Node.js or frontend
5+
applications. Use this whenever the user is working with features under a src/features/
6+
directory — creating features, wiring them via extension points, updating specs, or
7+
managing feature boundaries.
8+
---
9+
10+
# Feature-Based Development Skill
11+
12+
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.
13+
14+
**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.
15+
16+
---
17+
18+
## Folder Structure
19+
20+
Each feature lives in its own subdirectory under `src/features/`:
21+
22+
```
23+
src/features/
24+
├── feature1/
25+
│ ├── ext/
26+
│ │ └── index.js # Extension point contributions from this feature
27+
│ ├── index.js # Feature plugin definition
28+
│ └── FEATURE_SPEC.md # Feature specification
29+
├── feature2/
30+
│ ├── ext/
31+
│ │ └── index.js
32+
│ ├── index.js
33+
│ └── FEATURE_SPEC.md
34+
└── ...
35+
```
36+
37+
Tip: you can use `.ts[x]` , `.js[x]` for index files, not limited to `.js`.
38+
39+
---
40+
41+
## What Is a Feature?
42+
43+
A feature is a cohesive group of related capabilities. All features work together to form the complete application, but each one is independently removable.
44+
45+
**Core rules:**
46+
47+
- Every feature lives as a subdirectory under `src/features/`.
48+
- Every feature is implemented as a js-plugin plugin.
49+
- A feature must be removable without breaking the rest of the application — its absence degrades functionality gracefully rather than causing crashes or errors.
50+
- Features communicate with each other exclusively through **extension points** and **exports** — never through direct imports across feature boundaries.
51+
- Add extension points or exports to a feature only when integration with another feature actually requires it.
52+
53+
---
54+
55+
## FEATURE_SPEC.md
56+
57+
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.
58+
59+
**Purpose:** The spec establishes what the feature does and how it does it — not why it exists. Keep it precise and current.
60+
61+
**When to update:**
62+
63+
- Add an entry whenever a new capability is added to the feature.
64+
- When the user manually edits the spec, treat it as an implementation directive and apply the changes in code.
65+
66+
**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.
67+
68+
**Suggested sections** — include only what's relevant, and add any other sections that help describe the feature clearly:
69+
70+
| Section | Content |
71+
| ---------------------------- | ----------------------------------------------------------------- |
72+
| **Overview** | A short, accurate description of what the feature does |
73+
| **UI Requirements** | Layout, components, interactions, and visual behavior |
74+
| **Performance Requirements** | Loading targets, caching strategy, optimization constraints |
75+
| **Security Requirements** | Auth checks, data access rules, input validation |
76+
| **Extensibility** | Extension points exposed, global shared modal IDs, plugin exports |
77+
78+
Keep the spec concise. Avoid rationale or background — focus on _what_ and _how_.
79+
80+
---
81+
82+
## Workflow
83+
84+
### Creating a new feature
85+
86+
When the user asks to create a new feature:
87+
88+
1. Create a subdirectory under `src/features/` with a name that reflects the feature's purpose.
89+
2. Set up the standard folder structure (`ext/index.js`, `index.js`, `FEATURE_SPEC.md`).
90+
3. Write an initial `FEATURE_SPEC.md` with an accurate overview and placeholder sections for UI, performance, security, and extensibility.
91+
4. Scaffold the js-plugin plugin in `index.js`.
92+
93+
### Implementing or modifying a feature
94+
95+
When adding or changing capabilities:
96+
97+
1. Review the feature's `FEATURE_SPEC.md` to confirm the change is in scope.
98+
2. If the change is in scope, implement it and update the spec to reflect the new capability.
99+
3. If the change appears out of scope, ask the user whether to expand the spec or create a new feature.
100+
4. After applying any code changes, verify that the implementation matches the spec.
101+
102+
### Wiring features together
103+
104+
Use extension points for inter-feature communication:
105+
106+
- A feature that needs to expose behavior for others defines an extension point in its plugin.
107+
- A feature that contributes to another's extension point does so in its `ext/index.js`.
108+
- Never import directly from another feature's internal modules — always go through the plugin's public interface.
109+
110+
---
111+
112+
## Quick Reference
113+
114+
| Question | Answer |
115+
| ---------------------------------------------- | ------------------------------------------------------------- |
116+
| Where do new features go? | `src/features/<feature-name>/` |
117+
| How do features share behavior? | Extension points and plugin exports |
118+
| What happens if I remove a feature? | App still runs; that feature's capabilities are simply absent |
119+
| Where is a feature's contract defined? | `FEATURE_SPEC.md` |
120+
| When do I create a new feature vs. extend one? | When the capability is outside the existing spec's boundary |
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
# js-plugin Reference
2+
3+
`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.
4+
5+
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.
6+
7+
---
8+
9+
## The Problem It Solves
10+
11+
Without a plugin system, adding features requires modifying shared files that every feature touches:
12+
13+
```javascript
14+
// menu.js — WITHOUT js-plugin
15+
function Menu() {
16+
return (
17+
<ul>
18+
<li>Profile</li>
19+
<li>Account</li>
20+
{/* Every new feature must edit this file */}
21+
<li>Blocked Users</li>
22+
<li>API Keys</li>
23+
</ul>
24+
);
25+
}
26+
```
27+
28+
**With js-plugin**, each feature registers its own contributions, and shared components collect them dynamically:
29+
30+
```javascript
31+
// menu.js — WITH js-plugin
32+
import plugin from 'js-plugin';
33+
34+
function Menu() {
35+
const items = plugin.invoke('menu.getItems');
36+
return <ul>{items.map(item => <li key={item.key}>{item.label}</li>)}</ul>;
37+
}
38+
39+
// blocked-users/index.js
40+
plugin.register({
41+
name: 'blocked-users',
42+
menu: { getItems: () => ({ key: 'blocked', label: 'Blocked Users' }) }
43+
});
44+
45+
// api-keys/index.js
46+
plugin.register({
47+
name: 'api-keys',
48+
menu: { getItems: () => ({ key: 'api-keys', label: 'API Keys' }) }
49+
});
50+
```
51+
52+
`menu.js` never changes when features are added or removed. Each feature's code stays self-contained.
53+
54+
---
55+
56+
## API Reference
57+
58+
### `plugin.register(pluginObject)`
59+
60+
Register a feature plugin. Call this at module top-level in the feature's entry file, before the app renders.
61+
62+
```javascript
63+
import plugin from 'js-plugin';
64+
65+
plugin.register({
66+
name: 'notifications', // Required: unique identifier
67+
deps: ['auth'], // Optional: plugins that must be registered first
68+
initialize() { // Optional: called immediately after registration
69+
console.log('notifications ready');
70+
},
71+
// Everything else is an extension point contribution:
72+
menu: {
73+
getItems: () => [{ key: 'notif', label: 'Notifications', order: 20 }]
74+
},
75+
route: { path: '/notifications', component: NotificationsPage }
76+
});
77+
```
78+
79+
- If a declared `deps` entry is missing from the registry, the plugin is excluded from all invocations and a console warning is logged.
80+
- Never register conditionally — plugins should be statically registered.
81+
82+
---
83+
84+
### `plugin.invoke(extPoint, ...args)`
85+
86+
Collect contributions from all plugins that implement an extension point. Returns an array — one entry per contributing plugin.
87+
88+
| Syntax | Behavior |
89+
|---|---|
90+
| `plugin.invoke('a.b')` | Calls `a.b` as a function if it is one; otherwise returns the value |
91+
| `plugin.invoke('!a.b')` | Always returns the value without calling, even if it's a function |
92+
| `plugin.invoke('a.b!')` | Same as default, but throws errors instead of swallowing them |
93+
94+
```javascript
95+
// Call a lifecycle hook on every feature
96+
plugin.invoke('onInit');
97+
98+
// Collect plain values
99+
const routes = plugin.invoke('!route');
100+
// → [{ path: '/a', component: A }, { path: '/b', component: B }]
101+
102+
// Call with arguments
103+
const items = plugin.invoke('menu.getItems', currentUser);
104+
// → calls menu.getItems(currentUser) on each plugin that defines it
105+
106+
// Collect functions to call later
107+
const getters = plugin.invoke('!getHeaderWidget');
108+
// → [fn1, fn2] — call when ready: getters.map(fn => fn())
109+
```
110+
111+
---
112+
113+
### `plugin.getPlugin(name)`
114+
115+
Look up a specific plugin by name. Returns the plugin object or `undefined`.
116+
117+
```javascript
118+
const auth = plugin.getPlugin('auth');
119+
if (auth) {
120+
const user = auth.exports.getCurrentUser();
121+
}
122+
```
123+
124+
**Never call at module top-level** — the registry populates as modules load, so other plugins may not be registered yet:
125+
126+
```javascript
127+
// ❌ Too early
128+
const auth = plugin.getPlugin('auth');
129+
130+
// ✅ Inside a function, called after all plugins are loaded
131+
function handleLogin() {
132+
const auth = plugin.getPlugin('auth');
133+
auth?.exports.login();
134+
}
135+
```
136+
137+
---
138+
139+
### `plugin.getPlugins(extPoint?)`
140+
141+
Get all plugins contributing to an extension point. Omit the argument to get all registered plugins.
142+
143+
```javascript
144+
const all = plugin.getPlugins();
145+
const routed = plugin.getPlugins('route');
146+
const contributors = plugin.getPlugins('menu.getItems');
147+
```
148+
149+
Plugins with unresolved dependencies are automatically excluded.
150+
151+
---
152+
153+
### `plugin.sort(array, sortProp?)`
154+
155+
Sort an array of objects by a numeric property (default: `'order'`), in-place. Objects missing the property go to the end.
156+
157+
```javascript
158+
const items = plugin.invoke('menu.getItems').flat();
159+
plugin.sort(items); // sorts by item.order
160+
```
161+
162+
---
163+
164+
### `plugin.unregister(name)`
165+
166+
Remove a plugin from the registry. Mainly useful in tests and hot-reload scenarios.
167+
168+
---
169+
170+
### `plugin.config`
171+
172+
```javascript
173+
plugin.config.throws = true; // Make all invocations throw on error
174+
```
175+
176+
---
177+
178+
## Exports Pattern
179+
180+
`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()`.
181+
182+
```javascript
183+
// feature-a/index.js
184+
import plugin from 'js-plugin';
185+
import * as hooks from './hooks';
186+
import * as utils from './utils';
187+
import apiClient from './apiClient';
188+
189+
plugin.register({
190+
name: 'feature-a',
191+
exports: {
192+
hooks, // e.g. useFeatureAData, useFeatureAAuth
193+
utils, // e.g. formatItem, parseConfig
194+
apiClient // configured axios instance or similar
195+
}
196+
});
197+
```
198+
199+
```javascript
200+
// feature-b/SomeComponent.jsx
201+
import plugin from 'js-plugin';
202+
203+
function SomeComponent() {
204+
const { hooks, utils } = plugin.getPlugin('feature-a')?.exports || {};
205+
const data = hooks?.useFeatureAData();
206+
// ...
207+
}
208+
```
209+
210+
**When to use exports vs extension points:**
211+
212+
- Use **exports** when one feature needs to *consume* APIs or code owned by another — hooks, utilities, configured service clients, shared components.
213+
- Use **extension points** when a feature wants to let others *contribute* capabilities to it. Exports create direct coupling; extension points stay loose.
214+
215+
**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.
216+
217+
```javascript
218+
// ❌ Too early — feature-a may not be registered yet
219+
const { hooks } = plugin.getPlugin('feature-a').exports;
220+
221+
// ✅ Inside a function — safe
222+
function MyComponent() {
223+
const { hooks } = plugin.getPlugin('feature-a')?.exports || {};
224+
}
225+
```
226+
227+
Document your feature's exports in its `FEATURE_SPEC.md` so other features know what's available and how to access it.
228+
229+
---
230+
231+
## Extension Point Conventions
232+
233+
### Use nested objects, not string keys
234+
235+
Extension points are resolved by traversing object properties, not by parsing dot-notation strings:
236+
237+
```javascript
238+
// ✅ Correct
239+
plugin.register({
240+
name: 'my-feature',
241+
layout: { sidebar: { getItems: () => [...] } }
242+
});
243+
244+
// ❌ Wrong — string path keys are not traversed
245+
plugin.register({
246+
name: 'my-feature',
247+
'layout.sidebar.getItems': () => [...]
248+
});
249+
```
250+
251+
### Extension points are implicitly defined
252+
253+
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.
254+
255+
### Two roles for every extension point
256+
257+
```javascript
258+
// Provider — defines the extension point by consuming it
259+
function Sidebar() {
260+
const items = plugin.invoke('sidebar.getItems').flat();
261+
plugin.sort(items);
262+
return <nav>{items.map(renderItem)}</nav>;
263+
}
264+
265+
// Contributor — any feature that wants to appear in the sidebar
266+
plugin.register({
267+
name: 'reports',
268+
sidebar: { getItems: () => [{ key: 'reports', label: 'Reports', order: 40 }] }
269+
});
270+
```

0 commit comments

Comments
 (0)