@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.
@@ -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,7 @@
1
+ {
2
+ "name": "{{capitalName}}",
3
+ "type": "{{componentName}}",
4
+ "entry": "./src/index.jsx",
5
+ "keywords": "{{componentName}}",
6
+ "style": "./src/style.css"
7
+ }
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ .{{kebabName}} {
2
+ /* Add your styles here */
3
+ }