create-liferay-react-cx 1.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/LICENSE +21 -0
- package/README.md +288 -0
- package/bin/index.js +279 -0
- package/package.json +56 -0
- package/templates/custom-element/client-extension.yaml +17 -0
- package/templates/custom-element/eslint.config.js +1 -0
- package/templates/custom-element/index.html +12 -0
- package/templates/custom-element/src/App.jsx +19 -0
- package/templates/custom-element/src/assets/style.css +47 -0
- package/templates/custom-element/src/index.css +5 -0
- package/templates/custom-element/src/main.jsx +27 -0
- package/templates/custom-element/vite.config.js +14 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Laxit Khanpara
|
|
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
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# create-liferay-react-cx
|
|
2
|
+
|
|
3
|
+
> ⚡ Zero-config CLI scaffold for **Liferay React Client Extensions** (custom element type).
|
|
4
|
+
> Powered by [Vite](https://vitejs.dev/) · Compatible with **Liferay 7.4 / DXP 2024.Q1+**
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/create-liferay-react-cx)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
[](https://nodejs.org)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Table of Contents
|
|
13
|
+
|
|
14
|
+
- [What is a Liferay Client Extension?](#what-is-a-liferay-client-extension)
|
|
15
|
+
- [Requirements](#requirements)
|
|
16
|
+
- [Quick Start](#quick-start)
|
|
17
|
+
- [All Ways to Use This CLI](#all-ways-to-use-this-cli)
|
|
18
|
+
- [Method 1 — npm create (Recommended)](#method-1--npm-create-recommended)
|
|
19
|
+
- [Method 2 — npx (No Install)](#method-2--npx-no-install)
|
|
20
|
+
- [Method 3 — Global Install](#method-3--global-install)
|
|
21
|
+
- [Options & Flags](#options--flags)
|
|
22
|
+
- [Interactive Mode](#interactive-mode)
|
|
23
|
+
- [Generated Project Structure](#generated-project-structure)
|
|
24
|
+
- [Development Workflow](#development-workflow)
|
|
25
|
+
- [React Version Compatibility](#react-version-compatibility)
|
|
26
|
+
- [Contributing](#contributing)
|
|
27
|
+
- [License](#license)
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## What is a Liferay Client Extension?
|
|
32
|
+
|
|
33
|
+
A **Client Extension (CE)** is the modern, decoupled way to extend Liferay DXP / Community Edition — no Java, no OSGi, no portal restarts required.
|
|
34
|
+
|
|
35
|
+
A **Custom Element** CE wraps your React app as a standard Web Component (`<my-widget />`), which Liferay embeds directly on any page. This CLI scaffolds everything you need to build, preview, and deploy one in seconds.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Requirements
|
|
40
|
+
|
|
41
|
+
Before you start, make sure you have:
|
|
42
|
+
|
|
43
|
+
| Requirement | Version |
|
|
44
|
+
|---|---|
|
|
45
|
+
| Node.js | ≥ 18.0.0 |
|
|
46
|
+
| npm | ≥ 9.0.0 |
|
|
47
|
+
| [Liferay Workspace](https://learn.liferay.com/dxp/latest/en/building-applications/tooling/liferay-workspace/what-is-liferay-workspace.html) | For `gradlew deploy` |
|
|
48
|
+
|
|
49
|
+
> **Note:** The generated project must sit inside a Liferay Workspace at `[workspace]/client-extensions/my-widget/` for Gradle deployment to work.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm create liferay-react-cx my-widget
|
|
57
|
+
cd my-widget
|
|
58
|
+
../../gradlew deploy
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
That's it. Your custom element is live in Liferay. 🚀
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## All Ways to Use This CLI
|
|
66
|
+
|
|
67
|
+
### Method 1 — `npm create` (Recommended)
|
|
68
|
+
|
|
69
|
+
`npm create` is the standard npm convention for scaffolding tools (like `npm create vite`, `npm create react-app`). It automatically resolves `create-liferay-react-cx` under the hood.
|
|
70
|
+
|
|
71
|
+
**With a project name (fastest):**
|
|
72
|
+
```bash
|
|
73
|
+
npm create liferay-react-cx my-widget
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**With a specific React version:**
|
|
77
|
+
```bash
|
|
78
|
+
npm create liferay-react-cx -- --name my-widget --react-version 18.2.0
|
|
79
|
+
```
|
|
80
|
+
> ⚠️ Note the `--` before flags — this is required when using `npm create` with named options, so npm passes them through to the CLI correctly.
|
|
81
|
+
|
|
82
|
+
**Interactive wizard (no arguments):**
|
|
83
|
+
```bash
|
|
84
|
+
npm create liferay-react-cx
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
### Method 2 — `npx` (No Install)
|
|
90
|
+
|
|
91
|
+
Use `npx` to run the CLI directly without installing anything globally. The full package name `create-liferay-react-cx` is used here.
|
|
92
|
+
|
|
93
|
+
**With a project name:**
|
|
94
|
+
```bash
|
|
95
|
+
npx create-liferay-react-cx my-widget
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**With a specific React version:**
|
|
99
|
+
```bash
|
|
100
|
+
npx create-liferay-react-cx --name my-widget --react-version 18.2.0
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Interactive wizard:**
|
|
104
|
+
```bash
|
|
105
|
+
npx create-liferay-react-cx
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
### Method 3 — Global Install
|
|
111
|
+
|
|
112
|
+
Install once, use anywhere. After a global install, you can use the short alias `liferay-react-cx` from any directory.
|
|
113
|
+
|
|
114
|
+
**Step 1 — Install globally:**
|
|
115
|
+
```bash
|
|
116
|
+
npm install -g create-liferay-react-cx
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Step 2 — Use it:**
|
|
120
|
+
```bash
|
|
121
|
+
liferay-react-cx my-widget
|
|
122
|
+
liferay-react-cx --name my-widget --react-version 18.2.0
|
|
123
|
+
liferay-react-cx --help
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
> **Tip:** To update a globally installed version later, run `npm update -g create-liferay-react-cx`.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Options & Flags
|
|
131
|
+
|
|
132
|
+
| Flag | Short | Description | Default |
|
|
133
|
+
|------|-------|-------------|---------|
|
|
134
|
+
| `--name` | `-n` | App name in **kebab-case** | _(prompted)_ |
|
|
135
|
+
| `--react-version` | `-r` | React version to scaffold with | `16.12.0` |
|
|
136
|
+
| `--help` | `-h` | Print help and exit | — |
|
|
137
|
+
| `--version` | `-v` | Print version and exit | — |
|
|
138
|
+
|
|
139
|
+
**App name rules:** lowercase letters, numbers, and hyphens only. Must start with a letter. Examples: `my-widget`, `employee-portal`, `news-feed`.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Interactive Mode
|
|
144
|
+
|
|
145
|
+
Run the CLI with no arguments to enter the interactive wizard:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npm create liferay-react-cx
|
|
149
|
+
# or
|
|
150
|
+
npx create-liferay-react-cx
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
You will be asked:
|
|
154
|
+
|
|
155
|
+
1. **App name** — enter a kebab-case name for your widget
|
|
156
|
+
2. **React version** — choose from a list or enter a custom version:
|
|
157
|
+
- `16.12.0` — Liferay 7.4 / DXP classic
|
|
158
|
+
- `18.2.0` — Liferay 7.4 U45+ / DXP 2024.Q1+
|
|
159
|
+
- `Custom` — enter any valid semver (e.g. `17.0.2`)
|
|
160
|
+
3. **Install dependencies now?** — yes/no confirm
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Generated Project Structure
|
|
165
|
+
|
|
166
|
+
Running the CLI creates the following layout:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
my-widget/
|
|
170
|
+
├── client-extension.yaml ← Liferay CE descriptor (auto-configured)
|
|
171
|
+
├── index.html ← Vite dev-server entry point
|
|
172
|
+
├── package.json ← Project dependencies & scripts
|
|
173
|
+
├── vite.config.js ← Vite build config
|
|
174
|
+
├── eslint.config.js ← ESLint flat config
|
|
175
|
+
└── src/
|
|
176
|
+
├── main.jsx ← Web Component registration (HTMLElement)
|
|
177
|
+
├── App.jsx ← Your React component — start editing here
|
|
178
|
+
├── index.css ← Global reset (shadow DOM scoped)
|
|
179
|
+
└── assets/
|
|
180
|
+
└── style.css ← Component styles (shadow DOM isolated)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### What `client-extension.yaml` does
|
|
184
|
+
|
|
185
|
+
This file tells Liferay how to register and display your widget:
|
|
186
|
+
|
|
187
|
+
```yaml
|
|
188
|
+
my-widget:
|
|
189
|
+
type: customElement # renders as <my-widget> on Liferay pages
|
|
190
|
+
htmlElementName: my-widget # the HTML tag name
|
|
191
|
+
urls:
|
|
192
|
+
- assets/*.js # built JS bundle from vite-build/
|
|
193
|
+
cssURLs:
|
|
194
|
+
- assets/*.css # built CSS bundle from vite-build/
|
|
195
|
+
useESM: true # ES module support
|
|
196
|
+
instanceable: false # one instance per page
|
|
197
|
+
portletCategoryName: category.client-extensions
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
All values are automatically replaced with your app name during scaffolding.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Development Workflow
|
|
205
|
+
|
|
206
|
+
### ✅ Deploying to Liferay (Standard Workflow)
|
|
207
|
+
|
|
208
|
+
When working inside a Liferay Workspace, **you only need one command**. Gradle handles the npm install, Vite build, and deployment automatically — no manual `npm run build` needed.
|
|
209
|
+
|
|
210
|
+
**1. Navigate into your project:**
|
|
211
|
+
```bash
|
|
212
|
+
cd my-widget
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**2. Deploy directly to Liferay** (from the Liferay Workspace root):
|
|
216
|
+
```bash
|
|
217
|
+
../../gradlew deploy
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
That's it. Under the hood, Gradle will:
|
|
221
|
+
- Run `npm install` (if `node_modules` is missing)
|
|
222
|
+
- Run `npm run build` (Vite compiles to `vite-build/`)
|
|
223
|
+
- Copy the output into Liferay's deploy folder
|
|
224
|
+
- Hot-reload the bundle in your running Liferay instance
|
|
225
|
+
|
|
226
|
+
> After deployment, go to **Liferay Admin → Fragments and Resources → Client Extensions** to find and add your widget to a page.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
### 🖥️ Local Preview (Optional)
|
|
231
|
+
|
|
232
|
+
If you want to preview your React component in a browser **outside of Liferay** during development (faster feedback, hot reload), you can use Vite's dev server:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
npm install # only needed once
|
|
236
|
+
npm run dev # starts at http://localhost:5173
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
> This is purely optional and useful for rapid UI development. The component will render standalone — not inside Liferay's portal context.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## React Version Compatibility
|
|
244
|
+
|
|
245
|
+
| React Version | Liferay Compatibility | Notes |
|
|
246
|
+
|:---:|:---|:---|
|
|
247
|
+
| `16.12.0` | Liferay 7.4 GA / DXP classic | Uses Liferay's bundled React. Smaller bundle. |
|
|
248
|
+
| `18.2.0` | Liferay 7.4 U45+ / DXP 2024.Q1+ | Fully isolated in shadow DOM. Concurrent features. |
|
|
249
|
+
|
|
250
|
+
Not sure which to pick? Use `16.12.0` for maximum compatibility with older Liferay instances, or `18.2.0` for newer deployments that support isolated client extensions.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Contributing
|
|
255
|
+
|
|
256
|
+
Contributions, issues, and feature requests are welcome!
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
# 1. Clone the repo
|
|
260
|
+
git clone https://github.com/laxitkhanpara/liferay-react-ce.git
|
|
261
|
+
cd liferay-react-cx
|
|
262
|
+
|
|
263
|
+
# 2. Install dependencies
|
|
264
|
+
npm install
|
|
265
|
+
|
|
266
|
+
# 3. Test locally
|
|
267
|
+
node bin/index.js my-test-app
|
|
268
|
+
|
|
269
|
+
# 4. Clean up
|
|
270
|
+
rm -rf my-test-app
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Please open an [issue](https://github.com/laxitkhanpara/liferay-react-ce/issues) first to discuss any significant changes.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## License
|
|
278
|
+
|
|
279
|
+
[MIT](./LICENSE) © [Laxit Khanpara](https://github.com/laxitkhanpara)
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Links
|
|
284
|
+
|
|
285
|
+
- 📦 [npm package](https://www.npmjs.com/package/create-liferay-react-cx)
|
|
286
|
+
- 🐛 [Report a bug](https://github.com/laxitkhanpara/liferay-react-ce/issues)
|
|
287
|
+
- 📖 [Liferay Client Extensions docs](https://learn.liferay.com/dxp/latest/en/building-applications/client-extensions.html)
|
|
288
|
+
- 🏗️ [Liferay Workspace docs](https://learn.liferay.com/dxp/latest/en/building-applications/tooling/liferay-workspace/what-is-liferay-workspace.html)
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { execa } from 'execa';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
const commandName = 'liferay-react-cx';
|
|
14
|
+
|
|
15
|
+
const PKG_VERSION = '1.0.1';
|
|
16
|
+
|
|
17
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
|
|
21
|
+
const hasFlag = (flags) => flags.some((f) => args.includes(f));
|
|
22
|
+
|
|
23
|
+
const getArgValue = (names) => {
|
|
24
|
+
const i = args.findIndex((arg) => names.includes(arg));
|
|
25
|
+
return i >= 0 && args[i + 1] ? args[i + 1] : undefined;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const positionalArgs = args.filter((a) => !a.startsWith('-'));
|
|
29
|
+
|
|
30
|
+
// ─── Banner ──────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
function printBanner() {
|
|
33
|
+
console.log('');
|
|
34
|
+
|
|
35
|
+
console.log(
|
|
36
|
+
chalk.bold.hex('#0B5FFF')(
|
|
37
|
+
' ██╗ ██╗███████╗███████╗██████╗ █████╗ ██╗ ██╗'
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
console.log(
|
|
41
|
+
chalk.bold.hex('#0B5FFF')(
|
|
42
|
+
' ██║ ██║██╔════╝██╔════╝██╔══██╗██╔══██╗╚██╗ ██╔╝'
|
|
43
|
+
)
|
|
44
|
+
);
|
|
45
|
+
console.log(
|
|
46
|
+
chalk.bold.hex('#0B5FFF')(
|
|
47
|
+
' ██║ ██║█████╗ █████╗ ██████╔╝███████║ ╚████╔╝ '
|
|
48
|
+
)
|
|
49
|
+
);
|
|
50
|
+
console.log(
|
|
51
|
+
chalk.bold.hex('#0B5FFF')(
|
|
52
|
+
' ██║ ██║██╔══╝ ██╔══╝ ██╔══██╗██╔══██║ ╚██╔╝ '
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
console.log(
|
|
56
|
+
chalk.bold.hex('#0B5FFF')(
|
|
57
|
+
' ███████╗██║██║ ███████╗██║ ██║██║ ██║ ██║ '
|
|
58
|
+
)
|
|
59
|
+
);
|
|
60
|
+
console.log(
|
|
61
|
+
chalk.bold.hex('#0B5FFF')(
|
|
62
|
+
' ╚══════╝╚═╝╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ '
|
|
63
|
+
)
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
console.log('');
|
|
67
|
+
|
|
68
|
+
console.log(
|
|
69
|
+
chalk.bold(' Liferay React Client Extension')
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
console.log(
|
|
73
|
+
chalk.dim(' Official-style scaffolding tool for Liferay DXP')
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
console.log(
|
|
77
|
+
chalk.dim(` Version: ${PKG_VERSION}`)
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
console.log(
|
|
81
|
+
chalk.dim(' ─────────────────────────────────────────────')
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
console.log('');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── Version / Help ──────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
if (hasFlag(['--version', '-v'])) {
|
|
90
|
+
console.log(`liferay-react-cx v${PKG_VERSION}`);
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (hasFlag(['--help', '-h'])) {
|
|
95
|
+
console.log(`
|
|
96
|
+
${chalk.bold('liferay-react-cx')} — Scaffold a Liferay React Client Extension
|
|
97
|
+
|
|
98
|
+
${chalk.bold('USAGE')}
|
|
99
|
+
liferay-react-cx [app-name] [react-version]
|
|
100
|
+
liferay-react-cx --name <app-name> [--react-version <version>]
|
|
101
|
+
-n, --name App name in kebab-case (e.g. my-widget)
|
|
102
|
+
-r, --react-version React version to use (default: 16.12.0)
|
|
103
|
+
-v, --version Print version
|
|
104
|
+
-h, --help Show this help
|
|
105
|
+
|
|
106
|
+
${chalk.bold('EXAMPLES')}
|
|
107
|
+
npm create liferay-react-cx my-widget
|
|
108
|
+
npm create liferay-react-cx -- --name my-widget --react-version 18.2.0
|
|
109
|
+
npm create liferay-react-cx # interactive mode
|
|
110
|
+
|
|
111
|
+
${chalk.bold('AFTER SCAFFOLDING')}
|
|
112
|
+
cd <app-name>
|
|
113
|
+
../../gradlew deploy # deploy to Liferay
|
|
114
|
+
|
|
115
|
+
${chalk.dim('https://github.com/laxitkhanpara/liferay-react-ce')}
|
|
116
|
+
`);
|
|
117
|
+
process.exit(0);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
(async () => {
|
|
123
|
+
printBanner();
|
|
124
|
+
|
|
125
|
+
// Resolve inputs
|
|
126
|
+
const appNameArg = getArgValue(['--name', '-n']) || positionalArgs[0];
|
|
127
|
+
const reactVersionArg =
|
|
128
|
+
getArgValue(['--react-version', '-r']) || positionalArgs[1];
|
|
129
|
+
|
|
130
|
+
// Interactive prompts only for missing fields
|
|
131
|
+
let answers;
|
|
132
|
+
|
|
133
|
+
if (appNameArg) {
|
|
134
|
+
answers = {
|
|
135
|
+
appName: appNameArg,
|
|
136
|
+
reactVersion: reactVersionArg || '16.12.0',
|
|
137
|
+
installDeps: true,
|
|
138
|
+
};
|
|
139
|
+
} else {
|
|
140
|
+
answers = await inquirer.prompt([
|
|
141
|
+
{
|
|
142
|
+
type: 'input',
|
|
143
|
+
name: 'appName',
|
|
144
|
+
message: chalk.cyan('App name') + chalk.dim(' (kebab-case):'),
|
|
145
|
+
validate: (input) =>
|
|
146
|
+
/^[a-z][a-z0-9-]*$/.test(input.trim())
|
|
147
|
+
? true
|
|
148
|
+
: chalk.red('Use lowercase kebab-case — e.g. my-widget'),
|
|
149
|
+
filter: (v) => v.trim(),
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
type: 'list',
|
|
153
|
+
name: 'reactVersion',
|
|
154
|
+
message: chalk.cyan('React version:'),
|
|
155
|
+
choices: [
|
|
156
|
+
{ name: '16.12.0 (Liferay 7.4 / DXP classic)', value: '16.12.0' },
|
|
157
|
+
{ name: '18.2.0 (Liferay 7.4 U45+ / DXP 2024.Q1+)', value: '18.2.0' },
|
|
158
|
+
{ name: 'Custom (enter manually)', value: '__custom__' },
|
|
159
|
+
],
|
|
160
|
+
default: '16.12.0',
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
type: 'input',
|
|
164
|
+
name: 'reactVersionCustom',
|
|
165
|
+
message: chalk.cyan('Enter React version:'),
|
|
166
|
+
when: (prev) => prev.reactVersion === '__custom__',
|
|
167
|
+
validate: (v) =>
|
|
168
|
+
/^\d+\.\d+\.\d+/.test(v.trim()) ? true : 'Enter a valid semver e.g. 17.0.2',
|
|
169
|
+
filter: (v) => v.trim(),
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
type: 'confirm',
|
|
173
|
+
name: 'installDeps',
|
|
174
|
+
message: chalk.cyan('Install npm dependencies now?'),
|
|
175
|
+
default: true,
|
|
176
|
+
},
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
if (answers.reactVersion === '__custom__') {
|
|
180
|
+
answers.reactVersion = answers.reactVersionCustom;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const { appName, reactVersion, installDeps } = answers;
|
|
185
|
+
const projectPath = path.resolve(process.cwd(), appName);
|
|
186
|
+
const templatePath = path.join(__dirname, '..', 'templates', 'custom-element');
|
|
187
|
+
|
|
188
|
+
// Guard: existing directory
|
|
189
|
+
if (fs.existsSync(projectPath)) {
|
|
190
|
+
console.error(
|
|
191
|
+
chalk.red(`\n ✖ Directory "${appName}" already exists. Choose a different name.\n`)
|
|
192
|
+
);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── Step 1: Copy template ──────────────────────────────────────────────────
|
|
197
|
+
process.stdout.write(chalk.blue('\n 📁 Scaffolding project…'));
|
|
198
|
+
await fs.copy(templatePath, projectPath);
|
|
199
|
+
console.log(chalk.green(' ✔'));
|
|
200
|
+
|
|
201
|
+
// ── Step 2: Replace __APP_NAME__ placeholders ─────────────────────────────
|
|
202
|
+
const replaceInFile = (filePath) => {
|
|
203
|
+
let content = fs.readFileSync(filePath, 'utf-8');
|
|
204
|
+
content = content.replace(/__APP_NAME__/g, appName);
|
|
205
|
+
fs.writeFileSync(filePath, content);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const walk = (dir) => {
|
|
209
|
+
for (const file of fs.readdirSync(dir)) {
|
|
210
|
+
const full = path.join(dir, file);
|
|
211
|
+
fs.statSync(full).isDirectory() ? walk(full) : replaceInFile(full);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
walk(projectPath);
|
|
216
|
+
|
|
217
|
+
// ── Step 3: Write package.json ────────────────────────────────────────────
|
|
218
|
+
process.stdout.write(chalk.blue(' 📦 Writing package.json…'));
|
|
219
|
+
|
|
220
|
+
const projectPackageJson = {
|
|
221
|
+
name: appName,
|
|
222
|
+
version: '1.0.0',
|
|
223
|
+
private: true,
|
|
224
|
+
type: 'module',
|
|
225
|
+
scripts: {
|
|
226
|
+
dev: 'vite',
|
|
227
|
+
build: 'vite build',
|
|
228
|
+
preview: 'vite preview',
|
|
229
|
+
},
|
|
230
|
+
dependencies: {
|
|
231
|
+
react: reactVersion,
|
|
232
|
+
'react-dom': reactVersion,
|
|
233
|
+
},
|
|
234
|
+
devDependencies: {
|
|
235
|
+
'@vitejs/plugin-react': '^4.3.3',
|
|
236
|
+
vite: '^4.4.9',
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
fs.writeJsonSync(path.join(projectPath, 'package.json'), projectPackageJson, {
|
|
241
|
+
spaces: 2,
|
|
242
|
+
});
|
|
243
|
+
console.log(chalk.green(' ✔'));
|
|
244
|
+
|
|
245
|
+
// ── Step 4: Install dependencies ──────────────────────────────────────────
|
|
246
|
+
if (installDeps !== false) {
|
|
247
|
+
console.log(chalk.blue('\n 📥 Installing dependencies…\n'));
|
|
248
|
+
try {
|
|
249
|
+
await execa('npm', ['install'], { cwd: projectPath, stdio: 'inherit' });
|
|
250
|
+
} catch {
|
|
251
|
+
console.warn(
|
|
252
|
+
chalk.yellow(
|
|
253
|
+
'\n ⚠ npm install failed. Run it manually inside the project folder.\n'
|
|
254
|
+
)
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── Done ──────────────────────────────────────────────────────────────────
|
|
260
|
+
console.log('');
|
|
261
|
+
console.log(
|
|
262
|
+
chalk.bold.green(' ✅ Project created successfully!\n')
|
|
263
|
+
);
|
|
264
|
+
console.log(chalk.bold(' Next steps:\n'));
|
|
265
|
+
console.log(chalk.cyan(` cd ${appName}`));
|
|
266
|
+
if (installDeps === false) {
|
|
267
|
+
console.log(chalk.cyan(' npm install'));
|
|
268
|
+
}
|
|
269
|
+
console.log(
|
|
270
|
+
chalk.cyan(' ../../gradlew deploy') + chalk.dim(' # deploy to Liferay')
|
|
271
|
+
);
|
|
272
|
+
console.log('');
|
|
273
|
+
console.log(
|
|
274
|
+
chalk.dim(
|
|
275
|
+
' Docs: https://learn.liferay.com/dxp/latest/en/building-applications/client-extensions.html'
|
|
276
|
+
)
|
|
277
|
+
);
|
|
278
|
+
console.log('');
|
|
279
|
+
})();
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-liferay-react-cx",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "CLI scaffold tool to generate Liferay React Client Extension (custom element) projects — zero-config, Vite-powered, Liferay 7.4+ compatible.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./bin/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-liferay-react-cx": "./bin/index.js",
|
|
9
|
+
"liferay-react-cx": "./bin/index.js"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"liferay",
|
|
13
|
+
"client-extension",
|
|
14
|
+
"custom-element",
|
|
15
|
+
"react",
|
|
16
|
+
"vite",
|
|
17
|
+
"scaffold",
|
|
18
|
+
"cli",
|
|
19
|
+
"liferay-dxp",
|
|
20
|
+
"liferay-portal",
|
|
21
|
+
"web-component"
|
|
22
|
+
],
|
|
23
|
+
"author": {
|
|
24
|
+
"name": "Laxit Khanpara",
|
|
25
|
+
"email": "laxitkhanpara3646@gmail.com",
|
|
26
|
+
"url": "https://github.com/laxitkhanpara"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/laxitkhanpara/liferay-react-ce.git"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/laxitkhanpara/liferay-react-ce/issues"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/laxitkhanpara/liferay-react-ce#readme",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18.0.0",
|
|
39
|
+
"npm": ">=9.0.0"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"bin/",
|
|
43
|
+
"templates/",
|
|
44
|
+
"README.md",
|
|
45
|
+
"LICENSE"
|
|
46
|
+
],
|
|
47
|
+
"scripts": {
|
|
48
|
+
"test": "echo \"No tests yet\" && exit 0"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"chalk": "^5.3.0",
|
|
52
|
+
"execa": "^9.6.1",
|
|
53
|
+
"fs-extra": "^11.2.0",
|
|
54
|
+
"inquirer": "^9.2.12"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
assemble:
|
|
2
|
+
- from: vite-build
|
|
3
|
+
into: static
|
|
4
|
+
|
|
5
|
+
__APP_NAME__:
|
|
6
|
+
cssURLs:
|
|
7
|
+
- assets/*.css
|
|
8
|
+
friendlyURLMapping: __APP_NAME__
|
|
9
|
+
htmlElementName: __APP_NAME__
|
|
10
|
+
instanceable: false
|
|
11
|
+
name: __APP_NAME__
|
|
12
|
+
portletCategoryName: category.client-extensions
|
|
13
|
+
type: customElement
|
|
14
|
+
urls:
|
|
15
|
+
- assets/*.js
|
|
16
|
+
useESM: true
|
|
17
|
+
liferay.virtual.instance.id: default
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default [{ rules: { 'no-console': 'warn' } }];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>__APP_NAME__</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<__APP_NAME__></__APP_NAME__>
|
|
10
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import './assets/style.css';
|
|
3
|
+
|
|
4
|
+
function App() {
|
|
5
|
+
return (
|
|
6
|
+
<div className="lce-container">
|
|
7
|
+
<div className="lce-card">
|
|
8
|
+
<header className="lce-header">
|
|
9
|
+
<h1 className="lce-title">__APP_NAME__</h1>
|
|
10
|
+
<p className="lce-subtitle">
|
|
11
|
+
React {React.version}
|
|
12
|
+
</p>
|
|
13
|
+
</header>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default App;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/* ── Shadow DOM reset ─────────────────────────────────────────────────────── */
|
|
2
|
+
|
|
3
|
+
:host {
|
|
4
|
+
all: initial;
|
|
5
|
+
display: block;
|
|
6
|
+
font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
|
|
7
|
+
color: #1e1b4b;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* ── Layout ───────────────────────────────────────────────────────────────── */
|
|
11
|
+
|
|
12
|
+
.lce-container {
|
|
13
|
+
width: 100%;
|
|
14
|
+
min-height: 100px;
|
|
15
|
+
padding: 24px 16px;
|
|
16
|
+
display: flex;
|
|
17
|
+
justify-content: center;
|
|
18
|
+
align-items: center;
|
|
19
|
+
background: linear-gradient(135deg, #f5f3ff 0%, #ede9fe 100%);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.lce-card {
|
|
23
|
+
width: min(100%, 500px);
|
|
24
|
+
padding: 36px 40px;
|
|
25
|
+
border-radius: 20px;
|
|
26
|
+
background: #ffffff;
|
|
27
|
+
box-shadow: 0 8px 32px rgba(109, 40, 217, 0.12);
|
|
28
|
+
text-align: center;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* ── Header ───────────────────────────────────────────────────────────────── */
|
|
32
|
+
|
|
33
|
+
.lce-title {
|
|
34
|
+
font-size: 28px;
|
|
35
|
+
font-weight: 800;
|
|
36
|
+
letter-spacing: -0.5px;
|
|
37
|
+
background: linear-gradient(135deg, #4c1d95, #7c3aed);
|
|
38
|
+
-webkit-background-clip: text;
|
|
39
|
+
-webkit-text-fill-color: transparent;
|
|
40
|
+
background-clip: text;
|
|
41
|
+
margin-bottom: 10px;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.lce-subtitle {
|
|
45
|
+
color: #6b7280;
|
|
46
|
+
font-size: 14px;
|
|
47
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, unmountComponentAtNode } from 'react-dom';
|
|
3
|
+
import App from './App';
|
|
4
|
+
|
|
5
|
+
class WebComponent extends HTMLElement {
|
|
6
|
+
connectedCallback() {
|
|
7
|
+
render(
|
|
8
|
+
<React.StrictMode>
|
|
9
|
+
<App />
|
|
10
|
+
</React.StrictMode>,
|
|
11
|
+
this
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
disconnectedCallback() {
|
|
16
|
+
unmountComponentAtNode(this);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ELEMENT_NAME = '__APP_NAME__';
|
|
21
|
+
|
|
22
|
+
if (customElements.get(ELEMENT_NAME)) {
|
|
23
|
+
// eslint-disable-next-line no-console
|
|
24
|
+
console.log(`Skipping registration for <${ELEMENT_NAME}> (already registered)`);
|
|
25
|
+
} else {
|
|
26
|
+
customElements.define(ELEMENT_NAME, WebComponent);
|
|
27
|
+
}
|