@unifyapps/custom-component-cli 0.0.1
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/CREATING_CUSTOM_BLOCK.md +111 -0
- package/cli.js +183 -0
- package/entryMountTemplate.js +16 -0
- package/init.js +65 -0
- package/manifestPlugin.js +36 -0
- package/package.json +43 -0
- package/templates/init/README.md.mustache +137 -0
- package/templates/init/manifest.json.mustache +7 -0
- package/templates/init/package.json.mustache +15 -0
- package/templates/init/src/index.jsx.mustache +19 -0
- package/templates/init/src/style.css.mustache +3 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Creating a Custom Block with the Block CLI
|
|
2
|
+
|
|
3
|
+
This guide explains how to build and register a Custom Block using the block CLI. Blocks can be served locally for development and then exported for use in the Unify application.
|
|
4
|
+
|
|
5
|
+
## 0. Initialize a new Block
|
|
6
|
+
|
|
7
|
+
To scaffold a new block with a simple React component, package.json, and a manifest.json, run:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpx @unifyapps/block-cli init <BlockName>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This creates a new `<BlockName>/` folder containing:
|
|
14
|
+
- `package.json`
|
|
15
|
+
- `manifest.json`
|
|
16
|
+
- `src/index.jsx`
|
|
17
|
+
- `src/style.css`
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
cd <BlockName>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 1. Set up your workspace
|
|
24
|
+
|
|
25
|
+
Navigate to the directory containing the React component you want to expose as a Block. The CLI commands should be executed from this path so that relative imports resolve correctly.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
cd path/to/project
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 2. Start the Block in development mode
|
|
32
|
+
|
|
33
|
+
With a `manifest.json` in your project folder, you can simply run:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pnpx @unifyapps/block-cli dev
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
You can still override using CLI arguments:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pnpx @unifyapps/block-cli dev <BlockName> <component-path>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Example (using manifest):
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pnpx @unifyapps/block-cli dev
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The CLI serves the block manifest at `http://localhost:5001/super-button.json`. Note this URL for the next step.
|
|
52
|
+
|
|
53
|
+
## 3. Register the Block in the Unify app
|
|
54
|
+
|
|
55
|
+
1. Visit `qa.unifyapps.com/p/0/object/custom_blocks/records`.
|
|
56
|
+
2. Click **Add Record** and fill out the form:
|
|
57
|
+
- **BlockName** – the name used in the CLI, e.g. `SuperButton`.
|
|
58
|
+
- **Manifest URL** – the local URL from the previous step.
|
|
59
|
+
3. Save the record.
|
|
60
|
+
|
|
61
|
+
Once saved, the Block appears in the Application Builder under the **components** section. You can now test it in your interfaces.
|
|
62
|
+
|
|
63
|
+
## 4. Iterate on the Block
|
|
64
|
+
|
|
65
|
+
While running in `dev` mode, any changes to the component are reflected automatically. Refresh the Application Builder to load the latest version.
|
|
66
|
+
|
|
67
|
+
## 5. Build the Block for production
|
|
68
|
+
|
|
69
|
+
After testing, build a production bundle. From your project folder with a `manifest.json`, run:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pnpx @unifyapps/block-cli build
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Or override using CLI arguments:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pnpx @unifyapps/block-cli build <BlockName> <component-path>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Example (using manifest):
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pnpx @unifyapps/block-cli build
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The CLI outputs a `dist` directory containing the compiled block files, an `out.json` manifest, and a `<BlockName>.zip` archive of the `dist/` folder.
|
|
88
|
+
|
|
89
|
+
## 6. Upload the build output
|
|
90
|
+
|
|
91
|
+
1. Upload the contents of the `dist` directory to the Unify app.
|
|
92
|
+
2. Update the record created earlier with the new **Manifest URL** pointing to your uploaded `out.json`.
|
|
93
|
+
|
|
94
|
+
## 7. Removing a Block
|
|
95
|
+
|
|
96
|
+
To remove a Block from the system, delete its record from the custom blocks page in the Unify app.
|
|
97
|
+
|
|
98
|
+
## 8. Additional CLI examples
|
|
99
|
+
|
|
100
|
+
While the CLI will eventually be published, you can run it directly from this repository:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pnpx /path/to/www/packages/block-cli dev
|
|
104
|
+
pnpx /path/to/www/packages/block-cli build
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Replace `/path/to/www` with the absolute path on your machine.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
With these steps you can develop, test, and deploy custom Blocks to extend the Unify platform.
|
package/cli.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createWriteStream,
|
|
4
|
+
existsSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
unlinkSync,
|
|
7
|
+
watch,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from 'node:fs';
|
|
10
|
+
import http from 'node:http';
|
|
11
|
+
import { dirname, resolve } from 'node:path';
|
|
12
|
+
import { optimizeLodashImports } from '@optimize-lodash/rollup-plugin';
|
|
13
|
+
import react from '@vitejs/plugin-react-swc';
|
|
14
|
+
import archiver from 'archiver';
|
|
15
|
+
import _kebabCase from 'lodash/kebabCase.js';
|
|
16
|
+
import preserveDirectives from 'rollup-preserve-directives';
|
|
17
|
+
import handler from 'serve-handler';
|
|
18
|
+
import { build } from 'vite';
|
|
19
|
+
import tsconfigPaths from 'vite-tsconfig-paths';
|
|
20
|
+
import { getEntryMountCode } from './entryMountTemplate.js';
|
|
21
|
+
import { initComponent } from './init.js';
|
|
22
|
+
import manifestPlugin from './manifestPlugin.js';
|
|
23
|
+
|
|
24
|
+
const mode = process.argv[2] ?? 'dev';
|
|
25
|
+
if (mode === 'init') {
|
|
26
|
+
initComponent(process.argv[3]);
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
// load user manifest if present
|
|
30
|
+
let manifest = {};
|
|
31
|
+
if (existsSync(resolve(process.cwd(), 'manifest.json'))) {
|
|
32
|
+
manifest = JSON.parse(readFileSync(resolve(process.cwd(), 'manifest.json'), 'utf-8'));
|
|
33
|
+
}
|
|
34
|
+
const PackageName = manifest.type ?? process.argv[3];
|
|
35
|
+
const userEntryRel = manifest.entry ?? process.argv[4];
|
|
36
|
+
if (!PackageName || !userEntryRel) {
|
|
37
|
+
console.error(
|
|
38
|
+
'\n❌ Error: Missing component name or entry. Provide via CLI arguments or manifest.json\n',
|
|
39
|
+
);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
const root = resolve(process.cwd(), userEntryRel);
|
|
43
|
+
const libName = _kebabCase(PackageName);
|
|
44
|
+
const distDir = resolve(process.cwd(), 'dist');
|
|
45
|
+
|
|
46
|
+
const WRAPPER_FILENAME = '.ua-entry-mount.js';
|
|
47
|
+
const wrapperPath = resolve(process.cwd(), WRAPPER_FILENAME);
|
|
48
|
+
|
|
49
|
+
function createWrapperFile() {
|
|
50
|
+
writeFileSync(wrapperPath, getEntryMountCode(userEntryRel));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function cleanupWrapperFile() {
|
|
54
|
+
try {
|
|
55
|
+
unlinkSync(wrapperPath);
|
|
56
|
+
} catch {
|
|
57
|
+
// ignore
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function patchViteConfigWithWrapper(config) {
|
|
62
|
+
config.build = {
|
|
63
|
+
...config.build,
|
|
64
|
+
lib: {
|
|
65
|
+
...config.build.lib,
|
|
66
|
+
entry: wrapperPath,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
return config;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getViteConfig() {
|
|
73
|
+
return {
|
|
74
|
+
plugins: [
|
|
75
|
+
tsconfigPaths(),
|
|
76
|
+
react(),
|
|
77
|
+
preserveDirectives(),
|
|
78
|
+
optimizeLodashImports(),
|
|
79
|
+
manifestPlugin(),
|
|
80
|
+
],
|
|
81
|
+
define: {
|
|
82
|
+
'process.env': { NODE_ENV: process.env.NODE_ENV ?? 'development' },
|
|
83
|
+
},
|
|
84
|
+
build: {
|
|
85
|
+
lib: {
|
|
86
|
+
entry: root,
|
|
87
|
+
name: PackageName,
|
|
88
|
+
formats: ['es'],
|
|
89
|
+
fileName: (format) => `${libName}.${format}.js`,
|
|
90
|
+
cssFileName: libName,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const DEV_SERVER_PORT = process.env.PORT || 5001;
|
|
97
|
+
|
|
98
|
+
async function runDevServer() {
|
|
99
|
+
createWrapperFile();
|
|
100
|
+
const config = patchViteConfigWithWrapper(getViteConfig());
|
|
101
|
+
let server;
|
|
102
|
+
|
|
103
|
+
async function startServer() {
|
|
104
|
+
if (server) await server.close();
|
|
105
|
+
server = http.createServer((request, response) =>
|
|
106
|
+
handler(request, response, {
|
|
107
|
+
public: distDir,
|
|
108
|
+
directoryListing: false,
|
|
109
|
+
headers: [
|
|
110
|
+
{
|
|
111
|
+
source: '**',
|
|
112
|
+
headers: [{ key: 'Access-Control-Allow-Origin', value: '*' }],
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
server.listen(DEV_SERVER_PORT, () => {
|
|
118
|
+
console.log(`\n✅ Development server started`);
|
|
119
|
+
console.log(`📡 Serving manifest at: http://localhost:${DEV_SERVER_PORT}/out.json`);
|
|
120
|
+
console.log(`\n💡 Add this URL in Unify's Custom Components Manager`);
|
|
121
|
+
console.log(`🔄 Watching for file changes...\n`);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await build({ ...config, build: { ...config.build } });
|
|
126
|
+
await startServer();
|
|
127
|
+
|
|
128
|
+
const watchDir = dirname(resolve(process.cwd(), userEntryRel));
|
|
129
|
+
console.log(`Watching directory: ${watchDir}`);
|
|
130
|
+
const watcher = watch(watchDir, { recursive: true }, async (_eventType, filename) => {
|
|
131
|
+
if (filename && /\.(ts|tsx|js|jsx|css)$/.test(filename)) {
|
|
132
|
+
console.log(`File ${filename} changed. Rebuilding...`);
|
|
133
|
+
try {
|
|
134
|
+
await build({ ...config, build: { ...config.build } });
|
|
135
|
+
console.log('Rebuild completed successfully');
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error('❌ Build failed:', error);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
process.on('SIGINT', async () => {
|
|
143
|
+
console.log('\n\n🛑 Stopping development server...');
|
|
144
|
+
watcher.close();
|
|
145
|
+
if (server) await server.close();
|
|
146
|
+
cleanupWrapperFile();
|
|
147
|
+
console.log('✅ Server stopped\n');
|
|
148
|
+
process.exit(0);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
switch (mode) {
|
|
153
|
+
case 'dev': {
|
|
154
|
+
await runDevServer();
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
case 'build': {
|
|
158
|
+
console.log('\n🔨 Building component for production...\n');
|
|
159
|
+
createWrapperFile();
|
|
160
|
+
const config = patchViteConfigWithWrapper(getViteConfig());
|
|
161
|
+
await build(config);
|
|
162
|
+
cleanupWrapperFile();
|
|
163
|
+
// Zip up the dist directory
|
|
164
|
+
const zipName = `${libName}.zip`;
|
|
165
|
+
const zipPath = resolve(process.cwd(), zipName);
|
|
166
|
+
await new Promise((resolveZip, rejectZip) => {
|
|
167
|
+
const output = createWriteStream(zipPath);
|
|
168
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
169
|
+
output.on('close', resolveZip);
|
|
170
|
+
archive.on('error', rejectZip);
|
|
171
|
+
archive.pipe(output);
|
|
172
|
+
archive.directory(distDir, false);
|
|
173
|
+
archive.finalize();
|
|
174
|
+
});
|
|
175
|
+
console.log(`\n✅ Production package created: ${zipName}`);
|
|
176
|
+
console.log(
|
|
177
|
+
`💡 Move the dev version to production and upload this file in Unify's Custom Components Manager\n`,
|
|
178
|
+
);
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
default:
|
|
182
|
+
throw new Error('Invalid mode');
|
|
183
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Generates the entry-mount wrapper code for the build
|
|
2
|
+
// userEntryRel: relative path to the user's entry file (string)
|
|
3
|
+
export function getEntryMountCode(userEntryRel) {
|
|
4
|
+
return `import React from "react";
|
|
5
|
+
import { createRoot } from "react-dom/client";
|
|
6
|
+
import Component from "${userEntryRel}";
|
|
7
|
+
const domNode = document.getElementById("ua-custom-block");
|
|
8
|
+
const reactRoot = domNode ? createRoot(domNode) : null;
|
|
9
|
+
window.renderComponent = (props) => {
|
|
10
|
+
if (reactRoot) {
|
|
11
|
+
reactRoot.render(React.createElement(Component, props));
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
export default Component;
|
|
15
|
+
`;
|
|
16
|
+
}
|
package/init.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import _capitalize from 'lodash/capitalize.js';
|
|
5
|
+
import _kebabCase from 'lodash/kebabCase.js';
|
|
6
|
+
import mustache from 'mustache';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Scaffold a new React Custom Component project in a folder named after componentName,
|
|
10
|
+
* using templates with Mustache placeholders.
|
|
11
|
+
*/
|
|
12
|
+
export function initComponent(componentName) {
|
|
13
|
+
if (!componentName) {
|
|
14
|
+
console.error('❌ Error: Component name is required');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
console.log(`🚀 Initializing custom component: ${componentName}`);
|
|
19
|
+
|
|
20
|
+
const projectDir = resolve(process.cwd(), componentName);
|
|
21
|
+
if (existsSync(projectDir)) {
|
|
22
|
+
console.error(`❌ Error: Directory '${componentName}' already exists`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const templatesDir = resolve(__dirname, 'templates', 'init');
|
|
28
|
+
const kebabName = _kebabCase(componentName);
|
|
29
|
+
const capitalName = _capitalize(componentName);
|
|
30
|
+
|
|
31
|
+
function copyTemplates(srcDir) {
|
|
32
|
+
for (const name of readdirSync(srcDir)) {
|
|
33
|
+
const srcPath = resolve(srcDir, name);
|
|
34
|
+
const relPath = srcPath.slice(templatesDir.length + 1);
|
|
35
|
+
const stats = statSync(srcPath);
|
|
36
|
+
if (stats.isDirectory()) {
|
|
37
|
+
copyTemplates(srcPath);
|
|
38
|
+
} else if (stats.isFile()) {
|
|
39
|
+
const outRel = relPath.replace(/\.mustache$/, '');
|
|
40
|
+
const destPath = resolve(projectDir, outRel);
|
|
41
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
42
|
+
const template = readFileSync(srcPath, 'utf-8');
|
|
43
|
+
const content = mustache.render(template, { componentName, kebabName, capitalName });
|
|
44
|
+
writeFileSync(destPath, content, 'utf-8');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
copyTemplates(templatesDir);
|
|
50
|
+
|
|
51
|
+
console.log(`
|
|
52
|
+
✅ Successfully initialized custom component in ./${componentName}
|
|
53
|
+
|
|
54
|
+
📝 Next steps:
|
|
55
|
+
1. cd ${componentName}
|
|
56
|
+
2. pnpm i
|
|
57
|
+
3. Open the project in your code editor and customize your component
|
|
58
|
+
4. pnpm run dev
|
|
59
|
+
5. Create a custom component and add the development URL in Unify's Custom Components Manager
|
|
60
|
+
6. pnpm run build
|
|
61
|
+
7. Move the dev version to production and upload the ${kebabName}.zip file in Unify's Custom Components Manager
|
|
62
|
+
|
|
63
|
+
📖 See README.md for detailed instructions
|
|
64
|
+
`);
|
|
65
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Rollup plugin that reads the user-defined manifest.json and updates
|
|
6
|
+
* the output manifest with actual build asset filenames.
|
|
7
|
+
*/
|
|
8
|
+
export default function manifestPlugin() {
|
|
9
|
+
return {
|
|
10
|
+
name: 'custom-component-cli-manifest',
|
|
11
|
+
enforce: 'post',
|
|
12
|
+
apply: 'build',
|
|
13
|
+
async writeBundle(options, bundle) {
|
|
14
|
+
const baseManifest = JSON.parse(
|
|
15
|
+
await readFile(resolve(process.cwd(), 'manifest.json'), 'utf-8'),
|
|
16
|
+
);
|
|
17
|
+
const entryAsset = Object.values(bundle).find((o) => o.isEntry);
|
|
18
|
+
if (!entryAsset || !entryAsset.exports?.includes('default')) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`Could not find default export in entry ${entryAsset?.facadeModuleId || 'unknown'}`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
const styleAsset = Object.values(bundle).find((o) => o.fileName.endsWith('.css'));
|
|
24
|
+
|
|
25
|
+
const { entry: _, ...staticFields } = baseManifest;
|
|
26
|
+
const manifest = {
|
|
27
|
+
...staticFields,
|
|
28
|
+
main: entryAsset.fileName,
|
|
29
|
+
style: styleAsset?.fileName,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const filePath = resolve(options.dir, 'out.json');
|
|
33
|
+
await writeFile(filePath, JSON.stringify(manifest, null, 2));
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unifyapps/custom-component-cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"bin": "./cli.js",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "cli.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"templates",
|
|
12
|
+
"init.js",
|
|
13
|
+
"cli.js",
|
|
14
|
+
"manifestPlugin.js",
|
|
15
|
+
"entryMountTemplate.js",
|
|
16
|
+
"CREATING_CUSTOM_BLOCK.md"
|
|
17
|
+
],
|
|
18
|
+
"lint-staged": {
|
|
19
|
+
"*.{md,css,gql,graphql,json}": "biome check --linter-enabled=false --write",
|
|
20
|
+
"*.{js,jsx,ts,tsx}": [
|
|
21
|
+
"oxlint --fix",
|
|
22
|
+
"biome check --linter-enabled=false --write"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@optimize-lodash/rollup-plugin": "^5.0.2",
|
|
27
|
+
"@rollup/plugin-commonjs": "^29.0.0",
|
|
28
|
+
"@vitejs/plugin-react-swc": "^4.2.0",
|
|
29
|
+
"archiver": "^5.3.1",
|
|
30
|
+
"lodash": "^4.17.21",
|
|
31
|
+
"mustache": "^4.2.0",
|
|
32
|
+
"rollup-plugin-visualizer": "^5.14.0",
|
|
33
|
+
"rollup-preserve-directives": "^1.1.3",
|
|
34
|
+
"serve-handler": "^6.1.6",
|
|
35
|
+
"vite": "^7.2.2",
|
|
36
|
+
"vite-plugin-dts": "^4.5.4",
|
|
37
|
+
"vite-tsconfig-paths": "^5.1.4"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"dev": "vite",
|
|
41
|
+
"build": "vite build"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# {{capitalName}} Custom Component
|
|
2
|
+
|
|
3
|
+
This is a custom component created for UnifyApps. You can develop and customize this component to extend your application's functionality.
|
|
4
|
+
|
|
5
|
+
## Project Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
{{componentName}}/
|
|
9
|
+
├── src/
|
|
10
|
+
│ ├── index.jsx # Main React component file
|
|
11
|
+
│ └── style.css # Component styles
|
|
12
|
+
├── manifest.json # Component configuration for Unify
|
|
13
|
+
├── package.json # Project dependencies and scripts
|
|
14
|
+
└── README.md # This file
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Manifest Configuration
|
|
18
|
+
|
|
19
|
+
The `manifest.json` file configures your component for Unify. You can add icon and modify the keywords before using the manifest in Unify:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"name": "{{capitalName}}",
|
|
24
|
+
"type": "{{componentName}}",
|
|
25
|
+
"entry": "./src/index.jsx",
|
|
26
|
+
"keywords": "{{componentName}}",
|
|
27
|
+
"style": "./src/style.css",
|
|
28
|
+
"icon": "https://assets.unifyapps.com/interface/migrated/theme/unify.svg"
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Fields
|
|
33
|
+
|
|
34
|
+
- **`name`** (required) - Display name of the component
|
|
35
|
+
- **`type`** (required) - Component type identifier
|
|
36
|
+
- **`entry`** (required) - Path to the main component file
|
|
37
|
+
- **`keywords`** (required) - Search keywords for the block picker
|
|
38
|
+
- **`style`** (optional) - Path to the component stylesheet
|
|
39
|
+
- **`icon`** (optional) - URL of the icon to display in the block picker
|
|
40
|
+
|
|
41
|
+
## Development
|
|
42
|
+
|
|
43
|
+
### Where to Develop
|
|
44
|
+
|
|
45
|
+
All component code should be developed in the `src/` directory:
|
|
46
|
+
|
|
47
|
+
- **`src/index.jsx`** - Your main React component. This is where you'll write your component logic.
|
|
48
|
+
- **`src/style.css`** - Component-specific styles. Add your CSS here.
|
|
49
|
+
|
|
50
|
+
The component receives the following props:
|
|
51
|
+
- `data` - Component data state
|
|
52
|
+
- `updateData` - Function to update component data
|
|
53
|
+
- `emitOnClick` - Function to emit click events
|
|
54
|
+
- `emitOnChange` - Function to emit change events
|
|
55
|
+
|
|
56
|
+
### Next Steps
|
|
57
|
+
|
|
58
|
+
#### Step 1: Install Dependencies
|
|
59
|
+
|
|
60
|
+
Install the required dependencies:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pnpm i
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
#### Step 2: Run in Development Mode
|
|
67
|
+
|
|
68
|
+
Start the development server to preview your component locally. Use the path shown during initialization:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pnpm run dev
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Once started, the CLI will host the manifest at a local URL like:
|
|
75
|
+
```
|
|
76
|
+
http://localhost:5001/out.json
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
You'll use this URL in the next step.
|
|
80
|
+
|
|
81
|
+
#### Step 3: Add the Component in Unify
|
|
82
|
+
|
|
83
|
+
To make your component available inside Unify:
|
|
84
|
+
|
|
85
|
+
1. Go to **Custom Components Manager** in your Unify dashboard
|
|
86
|
+
2. Click **Add Component** and fill in:
|
|
87
|
+
- **Component Name** - e.g., `{{capitalName}}`
|
|
88
|
+
- **Deployment Mode** - Select **development mode** for testing
|
|
89
|
+
- **Manifest URL** - The local URL from dev mode (e.g., `http://localhost:5001/out.json`)
|
|
90
|
+
|
|
91
|
+
On clicking **Add**, your component will be available in the Application Builder **block picker**.
|
|
92
|
+
|
|
93
|
+
#### Step 4: Instant Reload
|
|
94
|
+
|
|
95
|
+
While running in `dev` mode, any changes to your React component are instantly reflected. Just refresh the Application Builder to see updates.
|
|
96
|
+
|
|
97
|
+
#### Step 5: Build for Production
|
|
98
|
+
|
|
99
|
+
After finalizing and testing your component, generate a production-ready build. Use the same path as in step 2:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pnpm run build
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
This creates a `{{kebabName}}.zip` file containing:
|
|
106
|
+
- Compiled assets
|
|
107
|
+
- A production `out.json` manifest
|
|
108
|
+
|
|
109
|
+
#### Step 6: Deploy to Production
|
|
110
|
+
|
|
111
|
+
1. Open the **Component Detail** panel in Unify
|
|
112
|
+
2. Click the three-dot menu next to the manifest URL
|
|
113
|
+
3. Select **Move to Production** from the dropdown
|
|
114
|
+
4. Upload the `{{kebabName}}.zip` file
|
|
115
|
+
|
|
116
|
+
Your component is now live and ready to use across your Unify interfaces.
|
|
117
|
+
|
|
118
|
+
## Component Props
|
|
119
|
+
|
|
120
|
+
Your component receives the following props:
|
|
121
|
+
|
|
122
|
+
- `data` - Object containing component data state
|
|
123
|
+
- `updateData(newData)` - Function to update component data
|
|
124
|
+
- `emitOnClick()` - Function to emit click events
|
|
125
|
+
- `emitOnChange()` - Function to emit change events
|
|
126
|
+
|
|
127
|
+
## Removing a Component
|
|
128
|
+
|
|
129
|
+
To remove this component from your app:
|
|
130
|
+
|
|
131
|
+
1. Go to the **Custom Components** page in Unify
|
|
132
|
+
2. Delete the corresponding component you want to remove
|
|
133
|
+
|
|
134
|
+
## Resources
|
|
135
|
+
|
|
136
|
+
- [UnifyApps Custom Components Documentation](https://www.unifyapps.com/docs/unify-applications/add-custom-component)
|
|
137
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{kebabName}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"main": "src/index.jsx",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "pnpx @unifyapps/custom-component-cli dev {{kebabName}}",
|
|
8
|
+
"build": "pnpx @unifyapps/custom-component-cli build {{kebabName}}"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@unifyapps/custom-component-cli": "^0.1.1",
|
|
12
|
+
"react": "^19.2.3",
|
|
13
|
+
"react-dom": "^19.2.3"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
import './style.css';
|
|
3
|
+
|
|
4
|
+
export default function {{capitalName}}(props) {
|
|
5
|
+
const { updateData, data, emitOnClick, emitOnChange } = props;
|
|
6
|
+
const value = data?.value ?? 0;
|
|
7
|
+
|
|
8
|
+
const onClick = useCallback(() => {
|
|
9
|
+
updateData({ value: value + 1 });
|
|
10
|
+
emitOnClick();
|
|
11
|
+
}, [value, updateData]);
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<button className="{{kebabName}}" onClick={onClick}>
|
|
16
|
+
Button is {value}
|
|
17
|
+
</button>
|
|
18
|
+
);
|
|
19
|
+
}
|