@pulse-editor/cli 0.1.1-beta.17 → 0.1.1-beta.19

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.
@@ -3,6 +3,7 @@ import { useEffect } from 'react';
3
3
  import { execa } from 'execa';
4
4
  import fs from 'fs';
5
5
  import { cleanDist } from '../../lib/execa-utils/clean.js';
6
+ import { webpackCompile } from '../../lib/webpack/compile.js';
6
7
  export default function Build({ cli }) {
7
8
  useEffect(() => {
8
9
  async function buildProd() {
@@ -48,40 +49,11 @@ export default function Build({ cli }) {
48
49
  }
49
50
  }
50
51
  await cleanDist();
51
- if (buildTarget === undefined) {
52
- // Start building
53
- await execa('npx webpack --mode production', {
54
- stdio: 'inherit',
55
- shell: true,
56
- env: {
57
- NODE_OPTIONS: '--import=tsx',
58
- },
59
- });
52
+ try {
53
+ await webpackCompile('production', buildTarget);
60
54
  }
61
- else if (buildTarget === 'client') {
62
- // Start building client only
63
- await execa('npx webpack --mode production', {
64
- stdio: 'inherit',
65
- shell: true,
66
- env: {
67
- NODE_OPTIONS: '--import=tsx',
68
- BUILD_TARGET: 'client',
69
- },
70
- });
71
- }
72
- else if (buildTarget === 'server') {
73
- // Start building server only
74
- await execa('npx webpack --mode production', {
75
- stdio: 'inherit',
76
- shell: true,
77
- env: {
78
- NODE_OPTIONS: '--import=tsx',
79
- BUILD_TARGET: 'server',
80
- },
81
- });
82
- }
83
- else {
84
- console.error(`❌ Unknown build target: ${buildTarget}`);
55
+ catch (err) {
56
+ console.error('❌ Webpack build failed', err);
85
57
  }
86
58
  }
87
59
  buildProd();
@@ -1,10 +1,10 @@
1
- import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Text } from 'ink';
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
3
2
  import { useEffect } from 'react';
4
3
  import { execa } from 'execa';
5
4
  import fs from 'fs';
6
5
  import { getDepsBinPath } from '../../lib/execa-utils/deps.js';
7
6
  import { cleanDist } from '../../lib/execa-utils/clean.js';
7
+ import { webpackCompile } from '../../lib/webpack/compile.js';
8
8
  export default function Dev({ cli }) {
9
9
  useEffect(() => {
10
10
  async function startDevServer() {
@@ -47,21 +47,34 @@ export default function Dev({ cli }) {
47
47
  }
48
48
  // Start dev server
49
49
  await cleanDist();
50
- await execa(getDepsBinPath('concurrently'), [
51
- '--prefix',
52
- 'none',
53
- '"npx webpack --mode development --watch"',
54
- '"tsx watch --clear-screen=false node_modules/@pulse-editor/cli/dist/lib/server/express.js"',
50
+ // Start webpack in dev watch mode and watch for changes
51
+ const compiler = await webpackCompile('development', undefined, true);
52
+ // Start server with tsx
53
+ await execa(getDepsBinPath('tsx'), [
54
+ 'watch',
55
+ '--clear-screen=false',
56
+ 'node_modules/@pulse-editor/cli/dist/lib/server/express.js',
55
57
  ], {
56
58
  stdio: 'inherit',
57
59
  shell: true,
58
60
  env: {
59
61
  NODE_OPTIONS: '--import=tsx',
60
- NODE_ENV: 'development',
61
62
  },
62
63
  });
64
+ // Handle process exit to close webpack compiler
65
+ process.on('SIGINT', () => {
66
+ if (compiler && typeof compiler.close === 'function') {
67
+ compiler.close(() => {
68
+ process.exit();
69
+ });
70
+ }
71
+ else {
72
+ process.exit();
73
+ }
74
+ });
63
75
  }
76
+ console.log('🚀 Starting development server...');
64
77
  startDevServer();
65
78
  }, []);
66
- return (_jsx(_Fragment, { children: _jsx(Text, { children: "Starting dev server..." }) }));
79
+ return _jsx(_Fragment, {});
67
80
  }
@@ -1,10 +1,10 @@
1
- import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Text } from 'ink';
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
3
2
  import { useEffect } from 'react';
4
3
  import { execa } from 'execa';
5
4
  import fs from 'fs';
6
5
  import { getDepsBinPath } from '../../lib/execa-utils/deps.js';
7
6
  import { cleanDist } from '../../lib/execa-utils/clean.js';
7
+ import { webpackCompile } from '../../lib/webpack/compile.js';
8
8
  export default function Preview({ cli }) {
9
9
  useEffect(() => {
10
10
  async function startPreviewServer() {
@@ -47,11 +47,28 @@ export default function Preview({ cli }) {
47
47
  }
48
48
  // Start preview server
49
49
  await cleanDist();
50
- await execa(getDepsBinPath('concurrently'), [
51
- '--prefix',
52
- 'none',
53
- '"npx webpack --mode development --watch"',
54
- '"tsx watch --clear-screen=false node_modules/@pulse-editor/cli/dist/lib/server/express.js"',
50
+ // await execa(
51
+ // getDepsBinPath('concurrently'),
52
+ // [
53
+ // '--prefix',
54
+ // 'none',
55
+ // '"npx webpack --mode development --watch"',
56
+ // '"tsx watch --clear-screen=false node_modules/@pulse-editor/cli/dist/lib/server/express.js"',
57
+ // ],
58
+ // {
59
+ // stdio: 'inherit',
60
+ // shell: true,
61
+ // env: {
62
+ // NODE_OPTIONS: '--import=tsx',
63
+ // PREVIEW: 'true',
64
+ // },
65
+ // },
66
+ // );
67
+ const compiler = await webpackCompile('preview', undefined, true);
68
+ await execa(getDepsBinPath('tsx'), [
69
+ 'watch',
70
+ '--clear-screen=false',
71
+ 'node_modules/@pulse-editor/cli/dist/lib/server/express.js',
55
72
  ], {
56
73
  stdio: 'inherit',
57
74
  shell: true,
@@ -60,8 +77,20 @@ export default function Preview({ cli }) {
60
77
  PREVIEW: 'true',
61
78
  },
62
79
  });
80
+ // Handle process exit to close webpack compiler
81
+ process.on('SIGINT', () => {
82
+ if (compiler && typeof compiler.close === 'function') {
83
+ compiler.close(() => {
84
+ process.exit();
85
+ });
86
+ }
87
+ else {
88
+ process.exit();
89
+ }
90
+ });
63
91
  }
92
+ console.log('🚀 Starting preview server...');
64
93
  startPreviewServer();
65
94
  }, []);
66
- return (_jsx(_Fragment, { children: _jsx(Text, { children: "Starting preview server..." }) }));
95
+ return _jsx(_Fragment, {});
67
96
  }
@@ -40,6 +40,13 @@ export default function Upgrade({ cli }) {
40
40
  await $ `npm install react@${React.version} react-dom@${React.version} --save-exact --silent --force`;
41
41
  await $ `npm install -D @pulse-editor/cli@${tag} --silent --force`;
42
42
  await $ `npm install @pulse-editor/shared-utils@${tag} @pulse-editor/react-api@${tag} --silent --force`;
43
+ // Remove webpack.config.ts if exists
44
+ const webpackConfigPath = `${process.cwd()}/webpack.config.ts`;
45
+ if (fs.existsSync(webpackConfigPath)) {
46
+ fs.unlinkSync(webpackConfigPath);
47
+ }
48
+ // Uninstall @module-federation/node html-webpack-plugin copy-webpack-plugin glob mini-css-extract-plugin webpack webpack-cli webpack-dev-server
49
+ await $ `npm uninstall @module-federation/node html-webpack-plugin copy-webpack-plugin glob mini-css-extract-plugin webpack webpack-cli webpack-dev-server --silent --force`;
43
50
  setStep('done');
44
51
  }
45
52
  return (_jsx(_Fragment, { children: isError ? (_jsxs(Text, { color: 'redBright', children: ["\u274C An error occurred: ", errorMessage || 'Unknown error'] })) : !isInProjectDir ? (_jsx(Text, { color: 'redBright', children: "\u26D4 The current directory does not contain a Pulse Editor project." })) : step === 'check-config' ? (_jsxs(Box, { children: [_jsx(Spinner, { type: "dots" }), _jsx(Text, { children: " Checking configuration..." })] })) : step === 'upgrade' ? (_jsx(_Fragment, { children: _jsxs(Box, { children: [_jsx(Spinner, { type: "dots" }), _jsx(Text, { children: " Upgrading packages..." })] }) })) : (_jsx(Box, { children: _jsx(Text, { color: 'greenBright', children: "\u2705 Upgrade completed successfully." }) })) }));
@@ -0,0 +1,2 @@
1
+ import webpack from 'webpack';
2
+ export declare function webpackCompile(mode: 'development' | 'production' | 'preview', buildTarget?: 'client' | 'server', isWatchMode?: boolean): Promise<void | webpack.MultiCompiler>;
@@ -0,0 +1,28 @@
1
+ import webpack from 'webpack';
2
+ import { createWebpackConfig } from '../../lib/webpack/webpack.config.js';
3
+ export async function webpackCompile(mode, buildTarget, isWatchMode = false) {
4
+ const configs = await createWebpackConfig(mode === 'preview', buildTarget ?? 'both', mode === 'development'
5
+ ? 'development'
6
+ : mode === 'preview'
7
+ ? 'development'
8
+ : 'production');
9
+ const compiler = webpack(configs);
10
+ if (isWatchMode) {
11
+ compiler.watch({}, (err, stats) => {
12
+ if (err) {
13
+ console.error('❌ Webpack build failed', err);
14
+ return;
15
+ }
16
+ });
17
+ return compiler;
18
+ }
19
+ return new Promise((resolve, reject) => {
20
+ compiler.run(err => {
21
+ if (err) {
22
+ reject(err);
23
+ return;
24
+ }
25
+ resolve();
26
+ });
27
+ });
28
+ }
@@ -0,0 +1,2 @@
1
+ import wp from 'webpack';
2
+ export declare function createWebpackConfig(isPreview: boolean, buildTarget: 'client' | 'server' | 'both', mode: 'development' | 'production'): Promise<wp.Configuration[]>;
@@ -0,0 +1,545 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { ModuleFederationPlugin } from '@module-federation/enhanced/webpack';
3
+ import MiniCssExtractPlugin from 'mini-css-extract-plugin';
4
+ import HtmlWebpackPlugin from 'html-webpack-plugin';
5
+ import { networkInterfaces } from 'os';
6
+ import path from 'path';
7
+ import { globSync } from 'glob';
8
+ import fs from 'fs';
9
+ import CopyWebpackPlugin from 'copy-webpack-plugin';
10
+ import ts from 'typescript';
11
+ import { pathToFileURL } from 'url';
12
+ import mfNode from '@module-federation/node';
13
+ const { NodeFederationPlugin } = mfNode;
14
+ import wp from 'webpack';
15
+ const { webpack } = wp;
16
+ export async function createWebpackConfig(isPreview, buildTarget, mode) {
17
+ const projectDirName = process.cwd();
18
+ async function loadPulseConfig() {
19
+ // compile to js file and import
20
+ const program = ts.createProgram({
21
+ rootNames: [path.join(projectDirName, 'pulse.config.ts')],
22
+ options: {
23
+ module: ts.ModuleKind.ESNext,
24
+ target: ts.ScriptTarget.ES2020,
25
+ outDir: path.join(projectDirName, 'node_modules/.pulse/config'),
26
+ esModuleInterop: true,
27
+ skipLibCheck: true,
28
+ forceConsistentCasingInFileNames: true,
29
+ },
30
+ });
31
+ program.emit();
32
+ // Fix imports in the generated js file for all files in node_modules/.pulse/config
33
+ globSync('node_modules/.pulse/config/**/*.js', {
34
+ cwd: projectDirName,
35
+ absolute: true,
36
+ }).forEach(jsFile => {
37
+ let content = fs.readFileSync(jsFile, 'utf-8');
38
+ content = content.replace(/(from\s+["']\.\/[^\s"']+)(["'])/g, (match, p1, p2) => {
39
+ // No change if the import already has any extension
40
+ if (p1.match(/\.(js|cjs|mjs|ts|tsx|json)$/)) {
41
+ return match; // No change needed
42
+ }
43
+ return `${p1}.js${p2}`;
44
+ });
45
+ fs.writeFileSync(jsFile, content);
46
+ });
47
+ // Copy package.json if exists
48
+ const pkgPath = path.join(projectDirName, 'package.json');
49
+ if (fs.existsSync(pkgPath)) {
50
+ const destPath = path.join(projectDirName, 'node_modules/.pulse/config/package.json');
51
+ fs.copyFileSync(pkgPath, destPath);
52
+ }
53
+ const compiledConfig = path.join(projectDirName, 'node_modules/.pulse/config/pulse.config.js');
54
+ const mod = await import(pathToFileURL(compiledConfig).href);
55
+ // delete the compiled config after importing
56
+ fs.rmSync(path.join(projectDirName, 'node_modules/.pulse/config'), {
57
+ recursive: true,
58
+ force: true,
59
+ });
60
+ return mod.default;
61
+ }
62
+ const pulseConfig = await loadPulseConfig();
63
+ function getLocalNetworkIP() {
64
+ const interfaces = networkInterfaces();
65
+ for (const iface of Object.values(interfaces)) {
66
+ if (!iface)
67
+ continue;
68
+ for (const config of iface) {
69
+ if (config.family === 'IPv4' && !config.internal) {
70
+ return config.address; // Returns the first non-internal IPv4 address
71
+ }
72
+ }
73
+ }
74
+ return 'localhost'; // Fallback
75
+ }
76
+ const origin = getLocalNetworkIP();
77
+ const previewStartupMessage = `
78
+ 🎉 Your Pulse extension preview \x1b[1m${pulseConfig.displayName}\x1b[0m is LIVE!
79
+
80
+ ⚡️ Local: http://localhost:3030
81
+ ⚡️ Network: http://${origin}:3030
82
+
83
+ ✨ Try it out in your browser and let the magic happen! 🚀
84
+ `;
85
+ const devStartupMessage = `
86
+ 🎉 Your Pulse extension \x1b[1m${pulseConfig.displayName}\x1b[0m is LIVE!
87
+
88
+ ⚡️ Local: http://localhost:3030/${pulseConfig.id}/${pulseConfig.version}/
89
+ ⚡️ Network: http://${origin}:3030/${pulseConfig.id}/${pulseConfig.version}/
90
+
91
+ ✨ Try it out in the Pulse Editor and let the magic happen! 🚀
92
+ `;
93
+ // #region Node Federation Plugin for Server Functions
94
+ function makeNodeFederationPlugin() {
95
+ function discoverServerFunctions() {
96
+ // Get all .ts files under src/server-function and read use default exports as entry points
97
+ const files = globSync('./src/server-function/**/*.ts');
98
+ const entryPoints = files
99
+ .map(file => file.replaceAll('\\', '/'))
100
+ .map(file => {
101
+ return {
102
+ ['./' +
103
+ file.replace('src/server-function/', '').replace(/\.ts$/, '')]: './' + file,
104
+ };
105
+ })
106
+ .reduce((acc, curr) => {
107
+ return { ...acc, ...curr };
108
+ }, {});
109
+ return entryPoints;
110
+ }
111
+ const funcs = discoverServerFunctions();
112
+ console.log(`Discovered server functions:
113
+ ${Object.entries(funcs)
114
+ .map(([name, file]) => {
115
+ return ` - ${name.slice(2)} (from ${file})`;
116
+ })
117
+ .join('\n')}
118
+ `);
119
+ return new NodeFederationPlugin({
120
+ name: pulseConfig.id + '_server',
121
+ remoteType: 'script',
122
+ useRuntimePlugin: true,
123
+ library: { type: 'commonjs-module' },
124
+ filename: 'remoteEntry.js',
125
+ exposes: {
126
+ ...funcs,
127
+ },
128
+ }, {});
129
+ }
130
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
131
+ function compileServerFunctions(compiler) {
132
+ // Remove existing entry points
133
+ try {
134
+ fs.rmSync('dist/server', { recursive: true, force: true });
135
+ }
136
+ catch (e) {
137
+ console.error('Error removing dist/server:', e);
138
+ console.log('Continuing...');
139
+ }
140
+ // Run a new webpack compilation to pick up new server functions
141
+ const options = {
142
+ ...compiler.options,
143
+ watch: false,
144
+ plugins: [
145
+ // Add a new NodeFederationPlugin with updated entry points
146
+ makeNodeFederationPlugin(),
147
+ ],
148
+ };
149
+ const newCompiler = webpack(options);
150
+ // Run the new compiler
151
+ newCompiler?.run((err, stats) => {
152
+ if (err) {
153
+ console.error(`[Server] ❌ Error during recompilation:`, err);
154
+ }
155
+ else if (stats?.hasErrors()) {
156
+ console.error(`[Server] ❌ Compilation errors:`, stats.toJson().errors);
157
+ }
158
+ else {
159
+ console.log(`[Server] ✅ Compiled server functions successfully.`);
160
+ }
161
+ });
162
+ }
163
+ // #endregion
164
+ // #region Source file parser for Pulse Config plugin
165
+ class PulseConfigPlugin {
166
+ requireFS = false;
167
+ apply(compiler) {
168
+ compiler.hooks.beforeCompile.tap('PulseConfigPlugin', () => {
169
+ this.requireFS = false;
170
+ globSync(['src/**/*.tsx', 'src/**/*.ts']).forEach(file => {
171
+ const source = fs.readFileSync(file, 'utf8');
172
+ this.scanSource(source);
173
+ });
174
+ // Persist result
175
+ pulseConfig.requireWorkspace = this.requireFS;
176
+ });
177
+ }
178
+ isWorkspaceHook(node) {
179
+ return (ts.isCallExpression(node) &&
180
+ ts.isIdentifier(node.expression) &&
181
+ [
182
+ 'useFileSystem',
183
+ 'useFile',
184
+ 'useReceiveFile',
185
+ 'useTerminal',
186
+ 'useWorkspaceInfo',
187
+ ].includes(node.expression.text));
188
+ }
189
+ scanSource(sourceText) {
190
+ const sourceFile = ts.createSourceFile('temp.tsx', sourceText, ts.ScriptTarget.Latest, true);
191
+ const visit = (node) => {
192
+ // Detect: useFileSystem(...)
193
+ if (this.isWorkspaceHook(node)) {
194
+ this.requireFS = true;
195
+ }
196
+ ts.forEachChild(node, visit);
197
+ };
198
+ visit(sourceFile);
199
+ }
200
+ }
201
+ // #endregion
202
+ // #region Webpack Configs
203
+ const previewClientConfig = {
204
+ mode: mode,
205
+ entry: {
206
+ main: './node_modules/.pulse/server/preview/frontend/index.js',
207
+ },
208
+ output: {
209
+ path: path.resolve(projectDirName, 'dist/client'),
210
+ },
211
+ resolve: {
212
+ extensions: ['.ts', '.tsx', '.js'],
213
+ },
214
+ plugins: [
215
+ new PulseConfigPlugin(),
216
+ new HtmlWebpackPlugin({
217
+ template: './node_modules/.pulse/server/preview/frontend/index.html',
218
+ }),
219
+ new MiniCssExtractPlugin({
220
+ filename: 'globals.css',
221
+ }),
222
+ new CopyWebpackPlugin({
223
+ patterns: [{ from: 'src/assets', to: 'assets' }],
224
+ }),
225
+ {
226
+ apply: compiler => {
227
+ let isFirstRun = true;
228
+ // Before build starts
229
+ compiler.hooks.watchRun.tap('ReloadMessagePlugin', () => {
230
+ if (!isFirstRun) {
231
+ console.log('[client-preview] 🔄 Reloading app...');
232
+ }
233
+ else {
234
+ console.log('[client-preview] 🔄 Building app...');
235
+ }
236
+ });
237
+ // After build finishes
238
+ compiler.hooks.done.tap('ReloadMessagePlugin', () => {
239
+ if (isFirstRun) {
240
+ console.log('[client-preview] ✅ Successfully built preview.');
241
+ console.log(previewStartupMessage);
242
+ isFirstRun = false;
243
+ }
244
+ else {
245
+ console.log('[client-preview] ✅ Reload finished');
246
+ }
247
+ // Write pulse config to dist
248
+ fs.writeFileSync(path.resolve(projectDirName, 'dist/client/pulse.config.json'), JSON.stringify(pulseConfig, null, 2));
249
+ fs.writeFileSync(path.resolve(projectDirName, 'dist/server/pulse.config.json'), JSON.stringify(pulseConfig, null, 2));
250
+ });
251
+ },
252
+ },
253
+ ],
254
+ watchOptions: {
255
+ ignored: /src\/server-function/,
256
+ },
257
+ module: {
258
+ rules: [
259
+ {
260
+ test: /\.tsx?$/,
261
+ use: 'ts-loader',
262
+ exclude: [/node_modules/, /dist/],
263
+ },
264
+ {
265
+ test: /\.css$/i,
266
+ use: [
267
+ MiniCssExtractPlugin.loader,
268
+ 'css-loader',
269
+ {
270
+ loader: 'postcss-loader',
271
+ },
272
+ ],
273
+ },
274
+ ],
275
+ },
276
+ stats: {
277
+ all: false,
278
+ errors: true,
279
+ warnings: true,
280
+ logging: 'warn',
281
+ colors: true,
282
+ },
283
+ infrastructureLogging: {
284
+ level: 'warn',
285
+ },
286
+ };
287
+ const previewHostConfig = {
288
+ mode: mode,
289
+ entry: './node_modules/@pulse-editor/cli/dist/lib/server/preview/backend/index.js',
290
+ target: 'async-node',
291
+ output: {
292
+ publicPath: 'auto',
293
+ library: { type: 'commonjs-module' },
294
+ path: path.resolve(projectDirName, 'dist/preview/backend'),
295
+ filename: 'index.cjs',
296
+ },
297
+ resolve: {
298
+ extensions: ['.ts', '.js'],
299
+ },
300
+ module: {
301
+ rules: [
302
+ {
303
+ test: /\.tsx?$/,
304
+ use: [
305
+ {
306
+ loader: 'ts-loader',
307
+ options: {
308
+ transpileOnly: false, // Enables type-checking and .d.ts file emission
309
+ },
310
+ },
311
+ ],
312
+ exclude: [/node_modules/, /dist/],
313
+ },
314
+ ],
315
+ },
316
+ plugins: [
317
+ new NodeFederationPlugin({
318
+ remoteType: 'script',
319
+ name: 'preview_host',
320
+ useRuntimePlugin: true,
321
+ exposes: {},
322
+ }, {}),
323
+ ],
324
+ stats: {
325
+ all: false,
326
+ errors: true,
327
+ warnings: true,
328
+ logging: 'warn',
329
+ colors: true,
330
+ },
331
+ infrastructureLogging: {
332
+ level: 'warn',
333
+ },
334
+ };
335
+ const mfClientConfig = {
336
+ mode: mode,
337
+ name: 'client',
338
+ entry: './src/main.tsx',
339
+ output: {
340
+ publicPath: 'auto',
341
+ path: path.resolve(projectDirName, 'dist/client'),
342
+ },
343
+ resolve: {
344
+ extensions: ['.ts', '.tsx', '.js'],
345
+ },
346
+ plugins: [
347
+ new PulseConfigPlugin(),
348
+ new MiniCssExtractPlugin({
349
+ filename: 'globals.css',
350
+ }),
351
+ // Copy assets to dist
352
+ new CopyWebpackPlugin({
353
+ patterns: [{ from: 'src/assets', to: 'assets' }],
354
+ }),
355
+ new ModuleFederationPlugin({
356
+ // Do not use hyphen character '-' in the name
357
+ name: pulseConfig.id,
358
+ filename: 'remoteEntry.js',
359
+ exposes: {
360
+ './main': './src/main.tsx',
361
+ },
362
+ shared: {
363
+ react: {
364
+ requiredVersion: '19.2.0',
365
+ import: 'react', // the "react" package will be used a provided and fallback module
366
+ shareKey: 'react', // under this name the shared module will be placed in the share scope
367
+ shareScope: 'default', // share scope with this name will be used
368
+ singleton: true, // only a single version of the shared module is allowed
369
+ },
370
+ 'react-dom': {
371
+ requiredVersion: '19.2.0',
372
+ singleton: true, // only a single version of the shared module is allowed
373
+ },
374
+ },
375
+ }),
376
+ {
377
+ apply: compiler => {
378
+ if (compiler.options.mode === 'development') {
379
+ let isFirstRun = true;
380
+ // Before build starts
381
+ compiler.hooks.watchRun.tap('ReloadMessagePlugin', () => {
382
+ if (!isFirstRun) {
383
+ console.log('[client] 🔄 reloading app...');
384
+ }
385
+ else {
386
+ console.log('[client] 🔄 building app...');
387
+ }
388
+ });
389
+ // Log file updates
390
+ compiler.hooks.invalid.tap('LogFileUpdates', (file, changeTime) => {
391
+ console.log(`[watch] change detected in: ${file} at ${new Date(changeTime || Date.now()).toLocaleTimeString()}`);
392
+ });
393
+ // After build finishes
394
+ compiler.hooks.done.tap('ReloadMessagePlugin', () => {
395
+ if (isFirstRun) {
396
+ console.log('[client] ✅ Successfully built client.');
397
+ console.log(devStartupMessage);
398
+ isFirstRun = false;
399
+ }
400
+ else {
401
+ console.log('[client] ✅ Reload finished.');
402
+ }
403
+ // Write pulse config to dist
404
+ fs.writeFileSync(path.resolve(projectDirName, 'dist/client/pulse.config.json'), JSON.stringify(pulseConfig, null, 2));
405
+ });
406
+ }
407
+ else {
408
+ // Print build success/failed message
409
+ compiler.hooks.done.tap('BuildMessagePlugin', stats => {
410
+ if (stats.hasErrors()) {
411
+ console.log(`[client] ❌ Failed to build client.`);
412
+ }
413
+ else {
414
+ console.log(`[client] ✅ Successfully built client.`);
415
+ // Write pulse config to dist
416
+ fs.writeFileSync(path.resolve(projectDirName, 'dist/client/pulse.config.json'), JSON.stringify(pulseConfig, null, 2));
417
+ }
418
+ });
419
+ }
420
+ },
421
+ },
422
+ ],
423
+ module: {
424
+ rules: [
425
+ {
426
+ test: /\.tsx?$/,
427
+ use: 'ts-loader',
428
+ exclude: [/node_modules/, /dist/],
429
+ },
430
+ {
431
+ test: /\.css$/i,
432
+ use: [
433
+ MiniCssExtractPlugin.loader,
434
+ 'css-loader',
435
+ {
436
+ loader: 'postcss-loader',
437
+ },
438
+ ],
439
+ exclude: [/dist/],
440
+ },
441
+ ],
442
+ },
443
+ stats: {
444
+ all: false,
445
+ errors: true,
446
+ warnings: true,
447
+ logging: 'warn',
448
+ colors: true,
449
+ assets: false,
450
+ },
451
+ infrastructureLogging: {
452
+ level: 'warn',
453
+ },
454
+ };
455
+ const mfServerConfig = {
456
+ mode: mode,
457
+ name: 'server',
458
+ entry: {},
459
+ target: 'async-node',
460
+ output: {
461
+ publicPath: 'auto',
462
+ path: path.resolve(projectDirName, 'dist/server'),
463
+ },
464
+ resolve: {
465
+ extensions: ['.ts', '.js'],
466
+ },
467
+ plugins: [
468
+ {
469
+ apply: compiler => {
470
+ if (compiler.options.mode === 'development') {
471
+ let isFirstRun = true;
472
+ // Before build starts
473
+ compiler.hooks.watchRun.tap('ReloadMessagePlugin', () => {
474
+ if (!isFirstRun) {
475
+ console.log(`[Server] 🔄 Reloading app...`);
476
+ }
477
+ else {
478
+ console.log(`[Server] 🔄 Building app...`);
479
+ }
480
+ compileServerFunctions(compiler);
481
+ });
482
+ // After build finishes
483
+ compiler.hooks.done.tap('ReloadMessagePlugin', () => {
484
+ if (isFirstRun) {
485
+ console.log(`[Server] ✅ Successfully built server.`);
486
+ isFirstRun = false;
487
+ }
488
+ else {
489
+ console.log(`[Server] ✅ Reload finished.`);
490
+ }
491
+ });
492
+ // Watch for changes in the server-function directory to trigger rebuilds
493
+ compiler.hooks.thisCompilation.tap('WatchServerFunctions', compilation => {
494
+ compilation.contextDependencies.add(path.resolve(projectDirName, 'src/server-function'));
495
+ });
496
+ }
497
+ else {
498
+ // Print build success/failed message
499
+ compiler.hooks.done.tap('BuildMessagePlugin', stats => {
500
+ if (stats.hasErrors()) {
501
+ console.log(`[Server] ❌ Failed to build server.`);
502
+ }
503
+ else {
504
+ compileServerFunctions(compiler);
505
+ console.log(`[Server] ✅ Successfully built server.`);
506
+ }
507
+ });
508
+ }
509
+ },
510
+ },
511
+ ],
512
+ module: {
513
+ rules: [
514
+ {
515
+ test: /\.tsx?$/,
516
+ use: 'ts-loader',
517
+ exclude: [/node_modules/, /dist/],
518
+ },
519
+ ],
520
+ },
521
+ stats: {
522
+ all: false,
523
+ errors: true,
524
+ warnings: true,
525
+ logging: 'warn',
526
+ colors: true,
527
+ },
528
+ infrastructureLogging: {
529
+ level: 'warn',
530
+ },
531
+ };
532
+ // #endregion
533
+ if (isPreview) {
534
+ return [previewClientConfig, previewHostConfig, mfServerConfig];
535
+ }
536
+ else if (buildTarget === 'server') {
537
+ return [mfServerConfig];
538
+ }
539
+ else if (buildTarget === 'client') {
540
+ return [mfClientConfig];
541
+ }
542
+ else {
543
+ return [mfClientConfig, mfServerConfig];
544
+ }
545
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulse-editor/cli",
3
- "version": "0.1.1-beta.17",
3
+ "version": "0.1.1-beta.19",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "pulse": "dist/cli.js"
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "scripts": {
13
13
  "build": "tsx build.ts",
14
- "dev": "tsx watch --include \"./source/**/*\" build.ts",
14
+ "dev": "tsx watch --include \"./src/**/*\" build.ts",
15
15
  "test": "prettier --check . && xo && ava",
16
16
  "link": "npm link"
17
17
  },
@@ -19,27 +19,34 @@
19
19
  "dist"
20
20
  ],
21
21
  "dependencies": {
22
+ "@module-federation/enhanced": "0.21.6",
23
+ "@module-federation/node": "^2.7.25",
24
+ "@module-federation/runtime": "0.21.6",
25
+ "@pulse-editor/shared-utils": "^0.1.1-beta.67",
22
26
  "concurrently": "^9.2.1",
23
27
  "connect-livereload": "^0.6.1",
28
+ "copy-webpack-plugin": "^13.0.1",
24
29
  "cors": "^2.8.5",
25
30
  "cross-env": "^10.1.0",
26
31
  "dotenv": "^17.2.3",
27
32
  "execa": "^9.6.1",
28
33
  "express": "^5.2.1",
34
+ "glob": "^13.0.0",
35
+ "html-webpack-plugin": "^5.6.5",
29
36
  "ink": "^6.5.1",
30
37
  "ink-select-input": "^6.2.0",
31
38
  "ink-spinner": "^5.0.0",
32
39
  "ink-text-input": "^6.0.0",
33
40
  "livereload": "^0.10.3",
34
41
  "meow": "^14.0.0",
42
+ "mini-css-extract-plugin": "^2.9.4",
35
43
  "openid-client": "^6.8.1",
36
44
  "rimraf": "^6.1.2",
37
- "tsx": "^4.21.0"
45
+ "tsx": "^4.21.0",
46
+ "webpack": "^5.104.0",
47
+ "webpack-dev-server": "^5.2.2"
38
48
  },
39
49
  "devDependencies": {
40
- "@module-federation/enhanced": "0.21.6",
41
- "@module-federation/node": "2.7.25",
42
- "@module-federation/runtime": "0.21.6",
43
50
  "@sindresorhus/tsconfig": "^8.1.0",
44
51
  "@types/connect-livereload": "^0.6.3",
45
52
  "@types/cors": "^2.8.19",
@@ -82,4 +89,4 @@
82
89
  }
83
90
  },
84
91
  "prettier": "@vdemedes/prettier-config"
85
- }
92
+ }