@veap/plugins 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +131 -2
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/actions/plugin.actions.d.ts +6 -0
- package/dist/actions/plugin.actions.js +60 -0
- package/dist/actions.d.ts +1 -0
- package/dist/actions.js +1 -0
- package/dist/cli/add.d.ts +4 -0
- package/dist/cli/add.js +191 -0
- package/dist/cli/eject.d.ts +2 -0
- package/dist/cli/eject.js +136 -0
- package/dist/cli/generate-plugin.d.ts +3 -0
- package/dist/cli/generate-plugin.js +48 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.js +6 -0
- package/dist/cli/package-manager.d.ts +14 -0
- package/dist/cli/package-manager.js +62 -0
- package/dist/cli/stubs.d.ts +16 -0
- package/dist/cli/stubs.js +42 -0
- package/dist/cli/utils.d.ts +3 -0
- package/dist/cli/utils.js +164 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.js +8 -0
- package/dist/common/context.d.ts +26 -0
- package/dist/common/context.js +26 -0
- package/dist/common/events.d.ts +19 -0
- package/dist/common/events.js +26 -0
- package/dist/common/index.d.ts +3 -0
- package/dist/common/index.js +3 -0
- package/dist/common/types.d.ts +195 -0
- package/dist/common/types.js +40 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/migrations/0001_create_plugins_tables.d.ts +4 -0
- package/dist/migrations/0001_create_plugins_tables.js +29 -0
- package/dist/migrations/index.d.ts +4 -0
- package/dist/migrations/index.js +7 -0
- package/dist/models/SystemPlugin.d.ts +19 -0
- package/dist/models/SystemPlugin.js +14 -0
- package/dist/models/SystemUserWidget.d.ts +19 -0
- package/dist/models/SystemUserWidget.js +17 -0
- package/dist/models/index.d.ts +2 -0
- package/dist/models/index.js +2 -0
- package/dist/models.d.ts +1 -0
- package/dist/models.js +1 -0
- package/dist/navigation/breadcrumbs.d.ts +2 -0
- package/dist/navigation/breadcrumbs.js +84 -0
- package/dist/navigation/index.d.ts +2 -0
- package/dist/navigation/index.js +2 -0
- package/dist/navigation/navigation.service.d.ts +27 -0
- package/dist/navigation/navigation.service.js +183 -0
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.js +1 -0
- package/dist/providers/plugins.provider.d.ts +20 -0
- package/dist/providers/plugins.provider.js +142 -0
- package/dist/registry/contracts.d.ts +72 -0
- package/dist/registry/contracts.js +4 -0
- package/dist/registry/facade.d.ts +45 -0
- package/dist/registry/facade.js +49 -0
- package/dist/registry/index.d.ts +5 -0
- package/dist/registry/index.js +5 -0
- package/dist/registry/migration-runner.d.ts +8 -0
- package/dist/registry/migration-runner.js +12 -0
- package/dist/registry/plugin.repository.d.ts +19 -0
- package/dist/registry/plugin.repository.js +74 -0
- package/dist/registry/registry.service.d.ts +69 -0
- package/dist/registry/registry.service.js +535 -0
- package/dist/router/discovery.d.ts +27 -0
- package/dist/router/discovery.js +37 -0
- package/dist/router/handler.d.ts +6 -0
- package/dist/router/handler.js +40 -0
- package/dist/router/index.d.ts +13 -0
- package/dist/router/index.js +13 -0
- package/dist/router/matcher.d.ts +5 -0
- package/dist/router/matcher.js +32 -0
- package/dist/router/middlewares.d.ts +25 -0
- package/dist/router/middlewares.js +119 -0
- package/dist/router/react/error-boundary.d.ts +30 -0
- package/dist/router/react/error-boundary.js +68 -0
- package/dist/router/react/index.d.ts +4 -0
- package/dist/router/react/index.js +4 -0
- package/dist/router/react/renderer.d.ts +13 -0
- package/dist/router/react/renderer.js +159 -0
- package/dist/router/react/soft-navigation.d.ts +4 -0
- package/dist/router/react/soft-navigation.js +34 -0
- package/dist/router/react/with-router.d.ts +22 -0
- package/dist/router/react/with-router.js +19 -0
- package/dist/router/route-tree.d.ts +177 -0
- package/dist/router/route-tree.js +633 -0
- package/dist/router/router.service.d.ts +25 -0
- package/dist/router/router.service.js +127 -0
- package/dist/router/scanner.d.ts +4 -0
- package/dist/router/scanner.js +337 -0
- package/dist/router/utils.d.ts +10 -0
- package/dist/router/utils.js +48 -0
- package/dist/router.d.ts +6 -0
- package/dist/router.js +6 -0
- package/dist/server.d.ts +11 -0
- package/dist/server.js +17 -0
- package/dist/widgets/index.d.ts +8 -0
- package/dist/widgets/index.js +8 -0
- package/dist/widgets/path-prefix-context.d.ts +5 -0
- package/dist/widgets/path-prefix-context.js +9 -0
- package/dist/widgets/plugin-extension-point-client.d.ts +15 -0
- package/dist/widgets/plugin-extension-point-client.js +44 -0
- package/dist/widgets/plugin-extension-point.d.ts +14 -0
- package/dist/widgets/plugin-extension-point.js +51 -0
- package/dist/widgets/plugin-widget-area-client.d.ts +11 -0
- package/dist/widgets/plugin-widget-area-client.js +31 -0
- package/dist/widgets/plugin-widget-area.d.ts +10 -0
- package/dist/widgets/plugin-widget-area.js +19 -0
- package/dist/widgets/use-confirm-action.d.ts +19 -0
- package/dist/widgets/use-confirm-action.js +68 -0
- package/dist/widgets/widget-composer-client.d.ts +21 -0
- package/dist/widgets/widget-composer-client.js +281 -0
- package/dist/widgets/widget-composer.d.ts +8 -0
- package/dist/widgets/widget-composer.js +28 -0
- package/package.json +115 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Damian Miller
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,132 @@
|
|
|
1
|
-
|
|
1
|
+
[](https://github.com/veapjs/veap-plugins/actions/workflows/ci.yml)
|
|
2
|
+
[](https://github.com/veapjs/veap-plugins/actions/workflows/release.yml)
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
# @veap/plugins
|
|
5
|
+
|
|
6
|
+
Plugin architecture, dynamic extension points, virtual routing, and widget
|
|
7
|
+
customization for Veap applications.
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
`@veap/plugins` provides an enterprise-grade modular plugin system. It enables
|
|
12
|
+
developers to extend Veap applications dynamically with UI slots, custom
|
|
13
|
+
dashboard widgets, virtual routing, navigation items, and event listeners
|
|
14
|
+
without modifying core application code.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **Standard plugin lifecycle**: Declarative plugin manifests defining
|
|
19
|
+
dependencies, configuration schemas (Zod), migrations, and activation hooks.
|
|
20
|
+
- **Extension slots & widgets**: React extension points allowing plugins to
|
|
21
|
+
inject custom UI components into existing pages, sidebars, and headers.
|
|
22
|
+
- **Drag-and-drop dashboard widgets**: Built-in support for user-customizable
|
|
23
|
+
widgets with `@dnd-kit` persistence.
|
|
24
|
+
- **Virtual router**: Dynamic route interception and virtual page injection
|
|
25
|
+
via the integrated router service.
|
|
26
|
+
- **Database persistence**: Storing plugin activation states and user widget
|
|
27
|
+
layouts in database tables managed by `@veap/database`.
|
|
28
|
+
- **IoC integration**: `PluginServiceProvider` boots and registers plugin
|
|
29
|
+
capabilities cleanly into the application container.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Using bun
|
|
35
|
+
bun add @veap/plugins @veap/kernel @veap/database @veap/auth
|
|
36
|
+
|
|
37
|
+
# Using pnpm
|
|
38
|
+
pnpm add @veap/plugins @veap/kernel @veap/database @veap/auth
|
|
39
|
+
|
|
40
|
+
# Using npm
|
|
41
|
+
npm install @veap/plugins @veap/kernel @veap/database @veap/auth
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
### Define a Custom Plugin
|
|
47
|
+
|
|
48
|
+
Create a plugin definition implementing the Veap plugin contract:
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import type { PluginDefinition } from "@veap/plugins";
|
|
52
|
+
|
|
53
|
+
export const AnalyticsPlugin: PluginDefinition = {
|
|
54
|
+
id: "com.example.analytics",
|
|
55
|
+
name: "Analytics Tracker",
|
|
56
|
+
version: "1.0.0",
|
|
57
|
+
description: "Injects telemetry script and admin dashboard widgets",
|
|
58
|
+
|
|
59
|
+
widgets: [
|
|
60
|
+
{
|
|
61
|
+
id: "analytics-overview",
|
|
62
|
+
title: "Visitor Traffic",
|
|
63
|
+
component: () => (
|
|
64
|
+
<div className="p-4 border rounded">
|
|
65
|
+
<p className="text-sm font-semibold">Active Visitors: 42</p>
|
|
66
|
+
</div>
|
|
67
|
+
),
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
|
|
71
|
+
async onActivate(container) {
|
|
72
|
+
console.log("Analytics plugin activated!");
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Register Plugins in Application Builder
|
|
78
|
+
|
|
79
|
+
Pass plugins to `Application.configure()`:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { Application } from "@veap/kernel";
|
|
83
|
+
import { PluginServiceProvider } from "@veap/plugins";
|
|
84
|
+
import { AnalyticsPlugin } from "./plugins/analytics";
|
|
85
|
+
|
|
86
|
+
export const app = Application.configure()
|
|
87
|
+
.use(PluginServiceProvider)
|
|
88
|
+
.withPlugins([AnalyticsPlugin])
|
|
89
|
+
.create();
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Render Extension Points in React
|
|
93
|
+
|
|
94
|
+
Use extension components from `@veap/plugins/client`:
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
"use client";
|
|
98
|
+
|
|
99
|
+
import { ExtensionSlot } from "@veap/plugins/client";
|
|
100
|
+
|
|
101
|
+
export function DashboardHeader() {
|
|
102
|
+
return (
|
|
103
|
+
<header className="flex justify-between items-center p-4">
|
|
104
|
+
<h1>Admin Dashboard</h1>
|
|
105
|
+
{/* External plugins can inject actions here */}
|
|
106
|
+
<ExtensionSlot name="dashboard:header:actions" />
|
|
107
|
+
</header>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Subpath Exports
|
|
113
|
+
|
|
114
|
+
| Export Path | Content |
|
|
115
|
+
| -------------------------- | -------------------------------------------------------------------------- |
|
|
116
|
+
| `@veap/plugins` | Core plugin definitions, lifecycle management, and `PluginServiceProvider` |
|
|
117
|
+
| `@veap/plugins/client` | React extension slots (`ExtensionSlot`) and widget renderers |
|
|
118
|
+
| `@veap/plugins/router` | Virtual router infrastructure and route rewriting |
|
|
119
|
+
| `@veap/plugins/models` | Active Record models (`PluginModel`, `UserWidgetModel`) |
|
|
120
|
+
| `@veap/plugins/migrations` | Knex migrations for plugin persistence |
|
|
121
|
+
|
|
122
|
+
## Documentation
|
|
123
|
+
|
|
124
|
+
For full guides and plugin development patterns, visit:
|
|
125
|
+
|
|
126
|
+
- **Official Website**: [https://veap.pl](https://veap.pl)
|
|
127
|
+
- **Documentation**: [https://veap.pl/docs](https://veap.pl/docs)
|
|
128
|
+
- **GitHub Repository**: [https://github.com/veapjs/veap-plugins](https://github.com/veapjs/veap-plugins)
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { getPluginExtensionsAction, getPluginWidgetsAction, getUserWidgetsState, saveUserWidgetsState, } from "./plugin.actions";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { getPluginExtensionsAction, getPluginWidgetsAction, getUserWidgetsState, saveUserWidgetsState, } from "./plugin.actions";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function getPluginExtensionsAction(target: string, point: string, includeDisabled?: boolean): Promise<import("..").PluginExtension[]>;
|
|
2
|
+
export declare function getPluginWidgetsAction(area: string): Promise<import("..").PluginWidget[]>;
|
|
3
|
+
export declare function getUserWidgetsState(slot: string): Promise<any>;
|
|
4
|
+
export declare function saveUserWidgetsState(slot: string, state: any): Promise<{
|
|
5
|
+
success: boolean;
|
|
6
|
+
}>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
import { getCurrentSession } from "@veap/auth";
|
|
3
|
+
import { AppError } from "@veap/kernel";
|
|
4
|
+
import { pluginsContext } from "../common/context";
|
|
5
|
+
import { SystemUserWidget } from "../models/SystemUserWidget";
|
|
6
|
+
export async function getPluginExtensionsAction(target, point, includeDisabled = false) {
|
|
7
|
+
const { user } = await getCurrentSession();
|
|
8
|
+
const userRoles = user?.roles || [];
|
|
9
|
+
const userPermissions = user?.permissions || [];
|
|
10
|
+
return pluginsContext().registry.getExtensions(target, point, includeDisabled, {
|
|
11
|
+
roles: userRoles,
|
|
12
|
+
permissions: userPermissions,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export async function getPluginWidgetsAction(area) {
|
|
16
|
+
const { user } = await getCurrentSession();
|
|
17
|
+
const userRoles = user?.roles || [];
|
|
18
|
+
const userPermissions = user?.permissions || [];
|
|
19
|
+
return pluginsContext().registry.getWidgets(area, {
|
|
20
|
+
roles: userRoles,
|
|
21
|
+
permissions: userPermissions,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export async function getUserWidgetsState(slot) {
|
|
25
|
+
const { user } = await getCurrentSession();
|
|
26
|
+
if (!user) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const widget = await SystemUserWidget.where({
|
|
30
|
+
user_id: user.id,
|
|
31
|
+
slot: slot,
|
|
32
|
+
}).first();
|
|
33
|
+
return widget?.state ?? null;
|
|
34
|
+
}
|
|
35
|
+
export async function saveUserWidgetsState(slot, state) {
|
|
36
|
+
const { user } = await getCurrentSession();
|
|
37
|
+
if (!user) {
|
|
38
|
+
throw AppError.Unauthorized();
|
|
39
|
+
}
|
|
40
|
+
const existing = await SystemUserWidget.where({
|
|
41
|
+
user_id: user.id,
|
|
42
|
+
slot: slot,
|
|
43
|
+
}).first();
|
|
44
|
+
if (existing) {
|
|
45
|
+
await SystemUserWidget.where({
|
|
46
|
+
user_id: user.id,
|
|
47
|
+
slot: slot,
|
|
48
|
+
}).update({
|
|
49
|
+
state: state,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
await SystemUserWidget.create({
|
|
54
|
+
userId: user.id,
|
|
55
|
+
slot: slot,
|
|
56
|
+
state: state,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return { success: true };
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./actions/index";
|
package/dist/actions.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./actions/index";
|
package/dist/cli/add.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import readline from "node:readline";
|
|
5
|
+
import ora from "ora";
|
|
6
|
+
import { findProjectRoot, regeneratePluginsRegistry } from "./utils.js";
|
|
7
|
+
async function askQuestion(query) {
|
|
8
|
+
const rl = readline.createInterface({
|
|
9
|
+
input: process.stdin,
|
|
10
|
+
output: process.stdout,
|
|
11
|
+
});
|
|
12
|
+
return new Promise((resolve) => rl.question(query, (ans) => {
|
|
13
|
+
rl.close();
|
|
14
|
+
resolve(ans);
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
export async function addPlugin(pluginArg, options) {
|
|
18
|
+
const rootDir = findProjectRoot(process.cwd());
|
|
19
|
+
console.log(`\n📦 Adding plugin: ${pluginArg}...`);
|
|
20
|
+
let pluginDir = "";
|
|
21
|
+
let pluginPkgJsonPath = "";
|
|
22
|
+
if (options?.local) {
|
|
23
|
+
// Local installation from Git URL or npm package
|
|
24
|
+
let gitUrl = pluginArg;
|
|
25
|
+
const isGitUrl = /^(git|http|https|github):/.test(pluginArg) || pluginArg.endsWith(".git");
|
|
26
|
+
if (!isGitUrl) {
|
|
27
|
+
const spinUrl = ora(`Fetching Git repository URL for package ${pluginArg}...`).start();
|
|
28
|
+
try {
|
|
29
|
+
const output = execSync(`npm view ${pluginArg} repository.url`, {
|
|
30
|
+
stdio: "pipe",
|
|
31
|
+
})
|
|
32
|
+
.toString()
|
|
33
|
+
.trim();
|
|
34
|
+
if (output) {
|
|
35
|
+
gitUrl = output;
|
|
36
|
+
spinUrl.succeed(`Found repository URL: ${gitUrl}`);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
spinUrl.fail(`Could not find repository URL for ${pluginArg}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (_err) {
|
|
44
|
+
spinUrl.fail(`Failed to fetch package details for ${pluginArg}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Normalize git URL
|
|
49
|
+
if (gitUrl.startsWith("git+")) {
|
|
50
|
+
gitUrl = gitUrl.slice(4);
|
|
51
|
+
}
|
|
52
|
+
if (gitUrl.startsWith("git://")) {
|
|
53
|
+
gitUrl = `https://${gitUrl.slice(6)}`;
|
|
54
|
+
}
|
|
55
|
+
const repoName = gitUrl
|
|
56
|
+
.split("/")
|
|
57
|
+
.pop()
|
|
58
|
+
?.replace(/\.git$/, "") || "custom-plugin";
|
|
59
|
+
pluginDir = path.join(rootDir, "plugins", repoName);
|
|
60
|
+
if (fs.existsSync(pluginDir)) {
|
|
61
|
+
console.error(`Error: Local plugin already exists in ${pluginDir}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
const spin = ora(`Cloning repository ${gitUrl} to plugins/${repoName}...`).start();
|
|
65
|
+
try {
|
|
66
|
+
execSync(`git clone ${gitUrl} "${pluginDir}"`, { stdio: "ignore" });
|
|
67
|
+
spin.succeed(`Cloned repository to plugins/${repoName}`);
|
|
68
|
+
}
|
|
69
|
+
catch (_err) {
|
|
70
|
+
spin.fail(`Failed to clone repository ${gitUrl}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
pluginPkgJsonPath = path.join(pluginDir, "package.json");
|
|
74
|
+
if (fs.existsSync(pluginPkgJsonPath)) {
|
|
75
|
+
try {
|
|
76
|
+
const pluginPkg = JSON.parse(fs.readFileSync(pluginPkgJsonPath, "utf-8"));
|
|
77
|
+
const pluginName = pluginPkg.name || repoName;
|
|
78
|
+
// Add to root package.json as workspace dependency
|
|
79
|
+
const rootPkgPath = path.join(rootDir, "package.json");
|
|
80
|
+
if (fs.existsSync(rootPkgPath)) {
|
|
81
|
+
const rootPkg = JSON.parse(fs.readFileSync(rootPkgPath, "utf-8"));
|
|
82
|
+
rootPkg.dependencies = rootPkg.dependencies || {};
|
|
83
|
+
rootPkg.dependencies[pluginName] = "workspace:*";
|
|
84
|
+
fs.writeFileSync(rootPkgPath, `${JSON.stringify(rootPkg, null, 2)}\n`, "utf-8");
|
|
85
|
+
}
|
|
86
|
+
// Run install to link the workspace package
|
|
87
|
+
if (!options?.skipInstall) {
|
|
88
|
+
const { detectPackageManager } = await import("./package-manager.js");
|
|
89
|
+
const pm = detectPackageManager(rootDir);
|
|
90
|
+
const installSpin = ora(`Linking local plugin with ${pm} install...`).start();
|
|
91
|
+
try {
|
|
92
|
+
execSync(`${pm} install`, { cwd: rootDir, stdio: "ignore" });
|
|
93
|
+
installSpin.succeed("Plugin linked successfully.");
|
|
94
|
+
}
|
|
95
|
+
catch (_err) {
|
|
96
|
+
installSpin.fail(`Failed to run ${pm} install automatically.`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (_) { }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// NPM installation via package manager
|
|
105
|
+
if (!options?.skipInstall) {
|
|
106
|
+
const { detectPackageManager } = await import("./package-manager.js");
|
|
107
|
+
const pm = detectPackageManager(rootDir);
|
|
108
|
+
const installCmd = pm === "npm" ? "install" : "add";
|
|
109
|
+
const workspaceFlag = pm === "npm" ? "" : "-w";
|
|
110
|
+
const spin = ora(`Installing npm package ${pluginArg}...`).start();
|
|
111
|
+
try {
|
|
112
|
+
execSync(`${pm} ${installCmd} ${pluginArg} ${workspaceFlag}`.trim(), {
|
|
113
|
+
cwd: rootDir,
|
|
114
|
+
stdio: "inherit",
|
|
115
|
+
});
|
|
116
|
+
spin.succeed(`Package ${pluginArg} installed.`);
|
|
117
|
+
}
|
|
118
|
+
catch (_err) {
|
|
119
|
+
spin.fail(`Failed to install package ${pluginArg}`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Resolve where it is installed in node_modules
|
|
124
|
+
const projectPkgPath = path.join(rootDir, "package.json");
|
|
125
|
+
let resolvedPkgName = pluginArg;
|
|
126
|
+
if (fs.existsSync(projectPkgPath)) {
|
|
127
|
+
const projectPkg = JSON.parse(fs.readFileSync(projectPkgPath, "utf-8"));
|
|
128
|
+
const deps = {
|
|
129
|
+
...(projectPkg.dependencies || {}),
|
|
130
|
+
...(projectPkg.devDependencies || {}),
|
|
131
|
+
};
|
|
132
|
+
for (const [name, val] of Object.entries(deps)) {
|
|
133
|
+
if (val === pluginArg ||
|
|
134
|
+
(typeof val === "string" && val.includes(pluginArg)) ||
|
|
135
|
+
name === pluginArg) {
|
|
136
|
+
resolvedPkgName = name;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
pluginDir = path.join(rootDir, "node_modules", resolvedPkgName);
|
|
142
|
+
pluginPkgJsonPath = path.join(pluginDir, "package.json");
|
|
143
|
+
}
|
|
144
|
+
if (!fs.existsSync(pluginPkgJsonPath)) {
|
|
145
|
+
console.error(`Error: Cannot find package.json for plugin at ${pluginPkgJsonPath}`);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
const pluginPkg = JSON.parse(fs.readFileSync(pluginPkgJsonPath, "utf-8"));
|
|
149
|
+
const VeapConfig = pluginPkg.veap || pluginPkg.veap || {};
|
|
150
|
+
// 1. Configure environment variables (.env)
|
|
151
|
+
if (VeapConfig.env) {
|
|
152
|
+
const envPath = path.join(rootDir, ".env");
|
|
153
|
+
let envContent = "";
|
|
154
|
+
if (fs.existsSync(envPath)) {
|
|
155
|
+
envContent = fs.readFileSync(envPath, "utf-8");
|
|
156
|
+
}
|
|
157
|
+
console.log(`\n ⚙️ Configuring environment variables for ${pluginPkg.name || pluginArg}:`);
|
|
158
|
+
// biome-ignore lint/suspicious/noExplicitAny: <ignore>
|
|
159
|
+
for (const [varName, varConfig] of Object.entries(VeapConfig.env)) {
|
|
160
|
+
const regex = new RegExp(`^${varName}=`, "m");
|
|
161
|
+
if (regex.test(envContent)) {
|
|
162
|
+
console.log(` - ${varName} is already configured in .env`);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const desc = varConfig.description || "No description provided";
|
|
166
|
+
const reqStr = varConfig.required ? " (required)" : " (optional)";
|
|
167
|
+
let value = "";
|
|
168
|
+
while (true) {
|
|
169
|
+
value = await askQuestion(`? Enter value for ${varName} - ${desc}${reqStr}: `);
|
|
170
|
+
if (varConfig.required && !value.trim()) {
|
|
171
|
+
console.log(` Error: ${varName} is required.`);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
envContent += `\n# Added by Veap CLI for ${pluginPkg.name}\n${varName}="${value}"\n`;
|
|
177
|
+
}
|
|
178
|
+
fs.writeFileSync(envPath, envContent, "utf-8");
|
|
179
|
+
console.log(`✅ Environment variables saved to .env`);
|
|
180
|
+
}
|
|
181
|
+
// 2. Install dependencies recursively
|
|
182
|
+
if (VeapConfig.dependencies && Array.isArray(VeapConfig.dependencies)) {
|
|
183
|
+
console.log(`\n📦 Installing dependencies for ${pluginPkg.name || pluginArg}...`);
|
|
184
|
+
for (const dep of VeapConfig.dependencies) {
|
|
185
|
+
await addPlugin(dep, options);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// 3. Regenerate plugins.gen.ts
|
|
189
|
+
regeneratePluginsRegistry(rootDir);
|
|
190
|
+
console.log(`\n✨ Plugin "${pluginPkg.name || pluginArg}" is ready!`);
|
|
191
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import { findProjectRoot, regeneratePluginsRegistry } from "./utils.js";
|
|
6
|
+
function normalizeGitUrl(url) {
|
|
7
|
+
if (url.startsWith("git+")) {
|
|
8
|
+
url = url.slice(4);
|
|
9
|
+
}
|
|
10
|
+
if (url.startsWith("git://")) {
|
|
11
|
+
url = `https://${url.slice(6)}`;
|
|
12
|
+
}
|
|
13
|
+
return url;
|
|
14
|
+
}
|
|
15
|
+
export async function ejectPackage(packageName) {
|
|
16
|
+
const rootDir = findProjectRoot(process.cwd());
|
|
17
|
+
// Check if root package.json has it
|
|
18
|
+
const rootPkgPath = path.join(rootDir, "package.json");
|
|
19
|
+
if (!fs.existsSync(rootPkgPath)) {
|
|
20
|
+
console.error("Error: Cannot find root package.json.");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const rootPkg = JSON.parse(fs.readFileSync(rootPkgPath, "utf-8"));
|
|
25
|
+
const deps = {
|
|
26
|
+
...(rootPkg.dependencies || {}),
|
|
27
|
+
...(rootPkg.devDependencies || {}),
|
|
28
|
+
};
|
|
29
|
+
if (!deps[packageName]) {
|
|
30
|
+
console.error(`Error: Package "${packageName}" is not installed in dependencies.`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
return;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// Find installed folder in node_modules
|
|
36
|
+
const nodeModulesDir = path.join(rootDir, "node_modules", packageName);
|
|
37
|
+
const pkgJsonPath = path.join(nodeModulesDir, "package.json");
|
|
38
|
+
if (!fs.existsSync(pkgJsonPath)) {
|
|
39
|
+
console.error(`Error: Cannot find package.json for installed package at ${pkgJsonPath}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
return;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const installedPkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
45
|
+
const isTemplate = installedPkg.veap?.type === "template" ||
|
|
46
|
+
packageName.endsWith("-template") ||
|
|
47
|
+
packageName.includes("template");
|
|
48
|
+
const itemType = isTemplate ? "template" : "plugin";
|
|
49
|
+
const targetSubdir = isTemplate ? "templates" : "plugins";
|
|
50
|
+
console.log(`\n🚀 Ejecting ${itemType}: ${packageName}...`);
|
|
51
|
+
const repo = installedPkg.repository;
|
|
52
|
+
let gitUrl = "";
|
|
53
|
+
if (typeof repo === "string") {
|
|
54
|
+
gitUrl = repo;
|
|
55
|
+
}
|
|
56
|
+
else if (repo && typeof repo === "object") {
|
|
57
|
+
gitUrl = repo.url || "";
|
|
58
|
+
}
|
|
59
|
+
if (!gitUrl) {
|
|
60
|
+
// If not found in package.json, try querying registry
|
|
61
|
+
const spinUrl = ora(`Querying registry for Git repository of ${packageName}...`).start();
|
|
62
|
+
try {
|
|
63
|
+
const output = execSync(`npm view ${packageName} repository.url`, {
|
|
64
|
+
stdio: "pipe",
|
|
65
|
+
})
|
|
66
|
+
.toString()
|
|
67
|
+
.trim();
|
|
68
|
+
if (output) {
|
|
69
|
+
gitUrl = output;
|
|
70
|
+
spinUrl.succeed(`Found repository URL: ${gitUrl}`);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
spinUrl.fail();
|
|
74
|
+
console.error(`Error: Package package.json does not specify repository URL, and registry query returned nothing.`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (_err) {
|
|
79
|
+
spinUrl.fail();
|
|
80
|
+
console.error(`Error: Could not retrieve repository URL from package or registry.`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const normalizedUrl = normalizeGitUrl(gitUrl);
|
|
85
|
+
const folderName = packageName.replace(/^@.*\//, "");
|
|
86
|
+
const destDir = path.join(rootDir, targetSubdir, folderName);
|
|
87
|
+
if (fs.existsSync(destDir)) {
|
|
88
|
+
console.error(`Error: Local ${itemType} directory already exists at ${destDir}`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
return;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Ensure target parent directory exists (e.g. templates/ or plugins/)
|
|
94
|
+
const targetBaseDir = path.join(rootDir, targetSubdir);
|
|
95
|
+
if (!fs.existsSync(targetBaseDir)) {
|
|
96
|
+
fs.mkdirSync(targetBaseDir, { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
const cloneSpin = ora(`Cloning repository ${normalizedUrl} to ${targetSubdir}/${folderName}...`).start();
|
|
99
|
+
try {
|
|
100
|
+
execSync(`git clone ${normalizedUrl} "${destDir}"`, { stdio: "ignore" });
|
|
101
|
+
cloneSpin.succeed(`Cloned repository to ${targetSubdir}/${folderName}`);
|
|
102
|
+
}
|
|
103
|
+
catch (_err) {
|
|
104
|
+
cloneSpin.fail(`Failed to clone repository ${normalizedUrl}`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
// Ensure workspace pattern exists in root package.json if workspaces array is defined
|
|
108
|
+
if (Array.isArray(rootPkg.workspaces)) {
|
|
109
|
+
const workspacePattern = `${targetSubdir}/*`;
|
|
110
|
+
if (!rootPkg.workspaces.includes(workspacePattern)) {
|
|
111
|
+
rootPkg.workspaces.push(workspacePattern);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Update root package.json dependency to workspace:*
|
|
115
|
+
rootPkg.dependencies = rootPkg.dependencies || {};
|
|
116
|
+
rootPkg.dependencies[packageName] = "workspace:*";
|
|
117
|
+
fs.writeFileSync(rootPkgPath, `${JSON.stringify(rootPkg, null, 2)}\n`, "utf-8");
|
|
118
|
+
console.log(`✅ Updated root package.json dependencies.`);
|
|
119
|
+
// Link workspace packages
|
|
120
|
+
const { detectPackageManager } = await import("./package-manager.js");
|
|
121
|
+
const pm = detectPackageManager(rootDir);
|
|
122
|
+
const linkSpin = ora(`Running ${pm} install to link ejected ${itemType}...`).start();
|
|
123
|
+
try {
|
|
124
|
+
execSync(`${pm} install`, { cwd: rootDir, stdio: "ignore" });
|
|
125
|
+
linkSpin.succeed(`${isTemplate ? "Template" : "Plugin"} linked successfully.`);
|
|
126
|
+
}
|
|
127
|
+
catch (_err) {
|
|
128
|
+
linkSpin.fail(`Failed to run ${pm} install automatically.`);
|
|
129
|
+
}
|
|
130
|
+
// Regenerate registry only for plugins
|
|
131
|
+
if (!isTemplate) {
|
|
132
|
+
regeneratePluginsRegistry(rootDir);
|
|
133
|
+
}
|
|
134
|
+
console.log(`\n✨ ${isTemplate ? "Template" : "Plugin"} "${packageName}" ejected successfully to local folder ${targetSubdir}/${folderName}!`);
|
|
135
|
+
}
|
|
136
|
+
export const ejectPlugin = ejectPackage;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import { stubPath } from "./stubs.js";
|
|
6
|
+
import { findProjectRoot, processStubs, } from "./utils.js";
|
|
7
|
+
export async function generatePlugin(name, options) {
|
|
8
|
+
const rootDir = findProjectRoot(process.cwd());
|
|
9
|
+
const pluginDir = path.join(rootDir, "plugins", `${name}-plugin`);
|
|
10
|
+
if (fs.existsSync(pluginDir)) {
|
|
11
|
+
console.error(`Error: Plugin "${name}" already exists in ${pluginDir}`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
const rootPluginsDir = path.join(rootDir, "plugins");
|
|
15
|
+
if (!fs.existsSync(rootPluginsDir)) {
|
|
16
|
+
fs.mkdirSync(rootPluginsDir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
console.log(`🚀 Generating a plugin: ${name} in ${pluginDir}...`);
|
|
19
|
+
try {
|
|
20
|
+
const titleName = name
|
|
21
|
+
.split("-")
|
|
22
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
23
|
+
.join(" ");
|
|
24
|
+
const pascalName = name.replace(/-/g, "");
|
|
25
|
+
const date = new Date().toDateString();
|
|
26
|
+
const variables = {
|
|
27
|
+
name,
|
|
28
|
+
titleName,
|
|
29
|
+
pascalName,
|
|
30
|
+
date,
|
|
31
|
+
};
|
|
32
|
+
// Stubs live at the package root of the installed @veap/framework.
|
|
33
|
+
const stubsDir = stubPath("plugin");
|
|
34
|
+
const spin = ora("Generating plugin files from stubs...").start();
|
|
35
|
+
processStubs(stubsDir, pluginDir, variables);
|
|
36
|
+
spin.succeed("Plugin files generated.");
|
|
37
|
+
if (!options?.skipInstall) {
|
|
38
|
+
const spinner = ora("📦 Installing dependencies (pnpm install)...").start();
|
|
39
|
+
execSync("pnpm install", { cwd: rootDir, stdio: "inherit" });
|
|
40
|
+
spinner.succeed("Dependencies installed.");
|
|
41
|
+
}
|
|
42
|
+
console.log(`\n✨ Plugin "${name}" is ready!`);
|
|
43
|
+
console.log(`💡 Run "pnpm veap generate:plugins" to register it.`);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
console.error("Error generating plugin:", err);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { addPlugin } from "./add";
|
|
2
|
+
export { ejectPackage, ejectPlugin } from "./eject";
|
|
3
|
+
export { generatePlugin } from "./generate-plugin";
|
|
4
|
+
export { applyDockerfileForPackageManager, detectPackageManager, type PackageManager, } from "./package-manager";
|
|
5
|
+
export { findStubsDir, stubPath } from "./stubs";
|
|
6
|
+
export { findProjectRoot, processStubs, regeneratePluginsRegistry, } from "./utils";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { addPlugin } from "./add";
|
|
2
|
+
export { ejectPackage, ejectPlugin } from "./eject";
|
|
3
|
+
export { generatePlugin } from "./generate-plugin";
|
|
4
|
+
export { applyDockerfileForPackageManager, detectPackageManager, } from "./package-manager";
|
|
5
|
+
export { findStubsDir, stubPath } from "./stubs";
|
|
6
|
+
export { findProjectRoot, processStubs, regeneratePluginsRegistry, } from "./utils";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type PackageManager = "pnpm" | "npm" | "yarn" | "bun";
|
|
2
|
+
/**
|
|
3
|
+
* Detects the package manager used by the project in `dir`.
|
|
4
|
+
*
|
|
5
|
+
* Priority: `packageManager` field in package.json > lockfiles > default (pnpm).
|
|
6
|
+
* On conflicting lockfiles pnpm wins (framework default).
|
|
7
|
+
*/
|
|
8
|
+
export declare function detectPackageManager(dir: string): PackageManager;
|
|
9
|
+
/**
|
|
10
|
+
* Overwrites the generated pnpm Dockerfile with the variant matching `pm`.
|
|
11
|
+
* No-op for pnpm (baseline stub), missing variants or missing Dockerfile -
|
|
12
|
+
* best effort, Docker config is optional.
|
|
13
|
+
*/
|
|
14
|
+
export declare function applyDockerfileForPackageManager(projectDir: string, pm: PackageManager): void;
|