一键导入
create-plugin
Step-by-step guide to create a new Deenruv plugin with server and UI components
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Step-by-step guide to create a new Deenruv plugin with server and UI components
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
How to add, update, and manage i18n translations in Deenruv plugins and admin UI
Build admin UI components and pages using @deenruv/react-ui-devkit (shadcn/Tailwind-based)
Write E2E tests for Deenruv plugins using Vitest and @deenruv/testing utilities
How to extend the Deenruv GraphQL API with new types, queries, mutations, and resolvers
基于 SOC 职业分类
| name | create-plugin |
| description | Step-by-step guide to create a new Deenruv plugin with server and UI components |
plugins/<feature>-plugin/
├── package.json
├── tsconfig.json # Project references (server + ui)
├── src/
│ ├── plugin-server/
│ │ ├── tsconfig.json # Server-side TS config
│ │ ├── index.ts # Re-exports from plugin.ts
│ │ ├── plugin.ts # @DeenruvPlugin() class definition
│ │ ├── constants.ts # Injection tokens (Symbols)
│ │ ├── types.ts # Config interfaces
│ │ ├── entities/ # TypeORM entities
│ │ ├── services/ # NestJS injectable services
│ │ ├── resolvers/ # GraphQL resolvers (or api/)
│ │ └── extensions/ # GraphQL schema extensions (.ts with gql``)
│ └── plugin-ui/
│ ├── tsconfig.json # UI-side TS config
│ ├── index.tsx # createDeenruvUIPlugin() call
│ ├── translation-ns.ts # Symbol-based namespace (or constants.ts)
│ ├── locales/
│ │ ├── en/
│ │ │ ├── index.ts # Re-exports JSON files as array
│ │ │ └── feature.json # English translations
│ │ └── pl/
│ │ ├── index.ts
│ │ └── feature.json # Polish translations
│ ├── components/ # React components
│ └── graphql/ # Zeus-generated types (auto-generated)
└── e2e/ # E2E tests (*.e2e-spec.ts)
Replace <feature> with the plugin name (kebab-case, e.g. loyalty, wishlist).
FEATURE=<feature>
mkdir -p plugins/${FEATURE}-plugin/src/plugin-server/{entities,services,resolvers,extensions}
mkdir -p plugins/${FEATURE}-plugin/src/plugin-ui/{locales/en,locales/pl,components}
mkdir -p plugins/${FEATURE}-plugin/e2e
tsconfig.jsonFile: plugins/<feature>-plugin/tsconfig.json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true
},
"references": [
{ "path": "./src/plugin-server" },
{ "path": "./src/plugin-ui" }
],
"files": []
}
If the plugin has no UI, remove the
plugin-uireference.
tsconfig.jsonFile: plugins/<feature>-plugin/src/plugin-server/tsconfig.json
{
"compilerOptions": {
"jsx": "react",
"useDefineForClassFields": false,
"module": "NodeNext",
"moduleResolution": "NodeNext",
"noLib": false,
"declaration": true,
"skipLibCheck": true,
"noEmitOnError": false,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es2017",
"strict": true,
"strictPropertyInitialization": false,
"sourceMap": false,
"newLine": "LF",
"resolveJsonModule": true,
"outDir": "../../dist/plugin-server"
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", "dist", "ui"]
}
tsconfig.jsonFile: plugins/<feature>-plugin/src/plugin-ui/tsconfig.json
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "node",
"target": "ES2020",
"jsx": "react",
"outDir": "../../dist/plugin-ui",
"importHelpers": true,
"declaration": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"noImplicitAny": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"include": ["./**/*.tsx", "./**/*.json", "./**/*.ts"]
}
package.jsonFile: plugins/<feature>-plugin/package.json
{
"name": "@deenruv/<feature>-plugin",
"version": "1.0.0",
"license": "MIT",
"type": "module",
"files": ["dist/**/*"],
"main": "./dist/plugin-server/index.js",
"exports": {
".": "./dist/plugin-server/index.js",
"./plugin-server": "./dist/plugin-server/index.js",
"./plugin-ui": "./dist/plugin-ui/index.js"
},
"scripts": {
"build": "rimraf dist && tsc --build",
"watch": "tsc --build --watch",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"zeus": "zeus http://localhost:3000/admin-api ./src/plugin-server --td && zeus http://localhost:3000/admin-api ./src/plugin-ui --td"
},
"peerDependencies": {
"@deenruv/core": "^0.1.0"
},
"dependencies": {
"@deenruv/common": "workspace:^",
"@deenruv/admin-types": "workspace:^",
"@deenruv/react-ui-devkit": "workspace:^",
"@nestjs/common": "^10.3.10",
"@nestjs/graphql": "^12.2.0",
"graphql-tag": "^2.12.6",
"graphql-zeus": "^7.0.0",
"lucide-react": "^0.363.0",
"react": "^18.2.0",
"react-i18next": "^14.0.5",
"react-router-dom": "^6.22.1",
"sonner": "^1.4.41"
},
"devDependencies": {
"@deenruv/core": "workspace:^",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"rimraf": "^5.0.10",
"typescript": "5.3.3"
}
}
Remove UI-related deps (
react,lucide-react,react-ui-devkit, etc.) if the plugin is server-only.
File: plugins/<feature>-plugin/src/plugin-server/constants.ts
export const FEATURE_PLUGIN_OPTIONS = Symbol("<feature>-plugin-options");
File: plugins/<feature>-plugin/src/plugin-server/types.ts
export type FeaturePluginOptions = {
// Plugin configuration options
enabled?: boolean;
};
File: plugins/<feature>-plugin/src/plugin-server/index.ts
export * from "./plugin.js";
File: plugins/<feature>-plugin/src/plugin-server/plugin.ts
import { PluginCommonModule, DeenruvPlugin, LanguageCode } from "@deenruv/core";
@DeenruvPlugin({
compatibility: "^0.0.1",
imports: [PluginCommonModule],
configuration: (config) => {
config.customFields.Product.push({
name: "featureField",
type: "string",
nullable: true,
defaultValue: "",
label: [
{ languageCode: LanguageCode.en, value: "Feature Field" },
{ languageCode: LanguageCode.pl, value: "Pole funkcji" },
],
});
return config;
},
})
export class FeaturePlugin {}
File: plugins/<feature>-plugin/src/plugin-server/plugin.ts
import { PluginCommonModule, DeenruvPlugin } from "@deenruv/core";
import { ADMIN_API_EXTENSION } from "./extensions/admin.extension.js";
import type { FeaturePluginOptions } from "./types.js";
import { FEATURE_PLUGIN_OPTIONS } from "./constants.js";
import { FeatureEntity } from "./entities/feature.entity.js";
import { FeatureAdminResolver } from "./resolvers/admin.resolver.js";
import { FeatureService } from "./services/feature.service.js";
@DeenruvPlugin({
compatibility: "^0.0.1",
imports: [PluginCommonModule],
entities: [FeatureEntity],
adminApiExtensions: {
schema: ADMIN_API_EXTENSION,
resolvers: [FeatureAdminResolver],
},
providers: [
{ provide: FEATURE_PLUGIN_OPTIONS, useFactory: () => FeaturePlugin.options },
FeatureService,
],
})
export class FeaturePlugin {
private static options: FeaturePluginOptions;
static init(options: FeaturePluginOptions) {
this.options = options;
return this;
}
}
IMPORTANT: All relative imports in
plugin-server/must use.jsextensions (ESM).
File: plugins/<feature>-plugin/src/plugin-server/entities/feature.entity.ts
import { Column, Entity } from "typeorm";
import type { DeepPartial } from "@deenruv/core";
import { DeenruvEntity } from "@deenruv/core";
@Entity()
export class FeatureEntity extends DeenruvEntity {
constructor(input?: DeepPartial<FeatureEntity>) {
super(input);
}
@Column()
name: string;
@Column({ default: true })
enabled: boolean;
}
File: plugins/<feature>-plugin/src/plugin-server/extensions/admin.extension.ts
import gql from "graphql-tag";
export const ADMIN_API_EXTENSION = gql`
type Feature {
id: ID!
name: String!
enabled: Boolean!
createdAt: DateTime!
updatedAt: DateTime!
}
extend type Query {
feature(id: ID!): Feature
}
extend type Mutation {
createFeature(input: CreateFeatureInput!): Feature!
deleteFeature(id: ID!): DeletionResponse!
}
input CreateFeatureInput {
name: String!
enabled: Boolean
}
`;
File: plugins/<feature>-plugin/src/plugin-server/services/feature.service.ts
import { Inject, Injectable } from "@nestjs/common";
import { ID, RequestContext, TransactionalConnection } from "@deenruv/core";
import { FeatureEntity } from "../entities/feature.entity.js";
import { FEATURE_PLUGIN_OPTIONS } from "../constants.js";
import type { FeaturePluginOptions } from "../types.js";
@Injectable()
export class FeatureService {
constructor(
private connection: TransactionalConnection,
@Inject(FEATURE_PLUGIN_OPTIONS)
private options: FeaturePluginOptions,
) {}
async findOne(ctx: RequestContext, id: ID) {
return this.connection.getRepository(ctx, FeatureEntity).findOne({ where: { id } });
}
async create(ctx: RequestContext, input: { name: string; enabled?: boolean }) {
const entity = new FeatureEntity({ name: input.name, enabled: input.enabled ?? true });
return this.connection.getRepository(ctx, FeatureEntity).save(entity);
}
async delete(ctx: RequestContext, id: ID) {
await this.connection.getRepository(ctx, FeatureEntity).delete(id);
return { result: "DELETED" as const };
}
}
File: plugins/<feature>-plugin/src/plugin-server/resolvers/admin.resolver.ts
import { Mutation, Query, Args, Resolver } from "@nestjs/graphql";
import { Allow, Ctx, ID, Permission, RequestContext } from "@deenruv/core";
import { FeatureService } from "../services/feature.service.js";
@Resolver()
export class FeatureAdminResolver {
constructor(private service: FeatureService) {}
@Allow(Permission.SuperAdmin)
@Query()
async feature(@Ctx() ctx: RequestContext, @Args() args: { id: ID }) {
return this.service.findOne(ctx, args.id);
}
@Allow(Permission.SuperAdmin)
@Mutation()
async createFeature(
@Ctx() ctx: RequestContext,
@Args() args: { input: { name: string; enabled?: boolean } },
) {
return this.service.create(ctx, args.input);
}
@Allow(Permission.SuperAdmin)
@Mutation()
async deleteFeature(@Ctx() ctx: RequestContext, @Args() args: { id: ID }) {
return this.service.delete(ctx, args.id);
}
}
File: plugins/<feature>-plugin/src/plugin-ui/index.tsx
import { BASE_GROUP_ID, createDeenruvUIPlugin } from "@deenruv/react-ui-devkit";
import { LayoutGridIcon } from "lucide-react";
import { translationNS } from "./translation-ns";
import pl from "./locales/pl";
import en from "./locales/en";
import React from "react";
import { FeaturePage } from "./components/FeaturePage";
export const UIPlugin = createDeenruvUIPlugin({
version: "1.0.0",
name: "Feature Plugin",
pages: [
{ path: "", element: <FeaturePage /> },
],
translations: {
ns: translationNS,
data: { en, pl },
},
navMenuLinks: [
{
id: "feature",
href: "",
labelId: "nav.link",
groupId: BASE_GROUP_ID.SETTINGS,
icon: LayoutGridIcon,
},
],
});
Two patterns exist in the codebase. Pick one:
Pattern A — Symbol-based (file: translation-ns.ts):
export const translationNS = Symbol("<feature>-plugin").toString();
Pattern B — String constant (file: constants.ts):
export const TRANSLATION_NAMESPACE = "<feature>-plugin";
File: plugins/<feature>-plugin/src/plugin-ui/locales/en/feature.json
{
"nav": {
"link": "Feature"
},
"title": "Feature Management",
"description": "Manage features from here."
}
File: plugins/<feature>-plugin/src/plugin-ui/locales/en/index.ts
import feature from "./feature.json";
export default [feature];
File: plugins/<feature>-plugin/src/plugin-ui/locales/pl/feature.json
{
"nav": {
"link": "Funkcja"
},
"title": "Zarządzanie funkcjami",
"description": "Zarządzaj funkcjami stąd."
}
File: plugins/<feature>-plugin/src/plugin-ui/locales/pl/index.ts
import feature from "./feature.json";
export default [feature];
File: plugins/<feature>-plugin/src/plugin-ui/components/FeaturePage.tsx
import React from "react";
export const FeaturePage = () => {
return (
<div>
<h1>Feature Plugin</h1>
<p>Plugin page content goes here.</p>
</div>
);
};
Edit apps/server/dev-config.ts:
import { FeaturePlugin } from "@deenruv/<feature>-plugin/plugin-server";
// Add to the plugins array:
plugins: [
// ... existing plugins
FeaturePlugin.init({ /* options */ }),
],
For the UI plugin, register it in the admin panel's plugin list at apps/panel/.
After the server is running with the new GraphQL extensions:
cd plugins/<feature>-plugin
pnpm zeus
This generates Zeus client types in src/plugin-server/zeus/ and src/plugin-ui/zeus/ (or graphql/).
pnpm install # Install workspace deps
pnpm build # Build all packages (includes the new plugin)
pnpm start:server # Verify server starts without errors
createDeenruvUIPlugin Full InterfaceAll available options for the UI plugin:
| Option | Type | Description |
|---|---|---|
name | string | Plugin display name |
version | string | Plugin version |
config | T | Generic config object passed to components |
pages | PluginPage[] | Full pages at /admin-ui/extensions/<plugin>/ |
components | DeenruvUIDetailComponent[] | Injected into detail views (e.g. product sidebar) |
tables | DeenruvUITable[] | Custom columns, row/bulk actions on list views |
tabs | DeenruvTabs[] | Extra tabs on detail views (e.g. customer detail) |
actions | { inline?, dropdown? } | Action buttons on detail views |
notifications | Notification[] | Custom notification handlers |
inputs | PluginComponent[] | Override custom field input components |
modals | DeenruvUIModalComponent[] | Custom modals on specific locations |
widgets | Widget[] | Dashboard widgets |
navMenuGroups | PluginNavigationGroup[] | Navigation groups in sidebar |
navMenuLinks | PluginNavigationLink[] | Navigation links in sidebar |
topNavigationComponents | PluginComponent[] | Components in top nav bar |
topNavigationActionsMenu | NavigationAction[] | Actions in top nav dropdown |
translations | { ns, data } | i18n translations (ns = namespace string, data = { en, pl }) |
BASE_GROUP_ID ValuesUse these to place nav links/groups in the sidebar:
| ID | Value |
|---|---|
BASE_GROUP_ID.SHOP | "shop-group" |
BASE_GROUP_ID.ASSORTMENT | "assortment-group" |
BASE_GROUP_ID.USERS | "users-group" |
BASE_GROUP_ID.PROMOTIONS | "promotions-group" |
BASE_GROUP_ID.SHIPPING | "shipping-group" |
BASE_GROUP_ID.SETTINGS | "settings-group" |
<feature>-plugin. Package name is @deenruv/<feature>-plugin..ts imports in plugin-server/ must use .js extensions.static init(options) + static options + provider factory useFactory: () => Plugin.options.Symbol("<feature>-plugin-options") in constants.ts.en and pl locales.After creating a plugin, verify:
plugin-server/ and plugin-ui/ (if UI needed)tsconfig.json with project referencestsconfig.json files with correct outDirpackage.json with @deenruv/ scope, workspace deps, correct exports@DeenruvPlugin decorator, PluginCommonModule importconstants.ts with Symbol()-based injection tokentypes.ts with options interfaceindex.ts re-exporting from plugin.tscreateDeenruvUIPlugin() (if UI needed)en/ and pl/ with index.ts re-exporting JSON arraysapps/server/dev-config.tspnpm install succeedspnpm build succeeds