@linktr.ee/linkapp 0.0.22 → 0.0.23

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.
Files changed (76) hide show
  1. package/dev-server/README.md +5 -5
  2. package/dev-server/{vite-env.d.ts → env.d.ts} +1 -1
  3. package/dev-server/featured/main.tsx +32 -311
  4. package/dev-server/package.json +5 -6
  5. package/dev-server/postcss/tailwind-source-fallback.js +94 -0
  6. package/dev-server/postcss.config.mjs +28 -3
  7. package/dev-server/preview/Preview.tsx +114 -339
  8. package/dev-server/preview/preview.css +3 -0
  9. package/dev-server/rsbuild.config.ts +45 -0
  10. package/dev-server/shared/theme-presets.ts +315 -0
  11. package/dev-server/shared/theme-utils.ts +38 -0
  12. package/dev-server/sheet/main.tsx +44 -0
  13. package/dev-server/{classic.html → sheet.html} +2 -2
  14. package/dist/commands/add.d.ts.map +1 -1
  15. package/dist/commands/add.js +20 -12
  16. package/dist/commands/add.js.map +1 -1
  17. package/dist/commands/build.d.ts.map +1 -1
  18. package/dist/commands/build.js +55 -36
  19. package/dist/commands/build.js.map +1 -1
  20. package/dist/commands/deploy.d.ts.map +1 -1
  21. package/dist/commands/deploy.js +58 -45
  22. package/dist/commands/deploy.js.map +1 -1
  23. package/dist/commands/dev.d.ts.map +1 -1
  24. package/dist/commands/dev.js +541 -128
  25. package/dist/commands/dev.js.map +1 -1
  26. package/dist/commands/login.d.ts.map +1 -1
  27. package/dist/commands/login.js +19 -9
  28. package/dist/commands/login.js.map +1 -1
  29. package/dist/commands/logout.d.ts.map +1 -1
  30. package/dist/commands/logout.js +9 -4
  31. package/dist/commands/logout.js.map +1 -1
  32. package/dist/commands/test-url-match-rules.d.ts.map +1 -1
  33. package/dist/commands/test-url-match-rules.js +24 -13
  34. package/dist/commands/test-url-match-rules.js.map +1 -1
  35. package/dist/lib/build/detect-layouts.d.ts +1 -1
  36. package/dist/lib/build/detect-layouts.d.ts.map +1 -1
  37. package/dist/lib/build/detect-layouts.js +8 -7
  38. package/dist/lib/build/detect-layouts.js.map +1 -1
  39. package/dist/lib/deploy/pack-project.js +1 -1
  40. package/dist/lib/deploy/pack-project.js.map +1 -1
  41. package/dist/lib/deploy/validation.d.ts.map +1 -1
  42. package/dist/lib/deploy/validation.js +8 -5
  43. package/dist/lib/deploy/validation.js.map +1 -1
  44. package/dist/lib/rsbuild/config-factory.d.ts +24 -0
  45. package/dist/lib/rsbuild/config-factory.d.ts.map +1 -0
  46. package/dist/lib/rsbuild/config-factory.js +135 -0
  47. package/dist/lib/rsbuild/config-factory.js.map +1 -0
  48. package/dist/lib/rsbuild/plugins/asset-versioning.d.ts +11 -0
  49. package/dist/lib/rsbuild/plugins/asset-versioning.d.ts.map +1 -0
  50. package/dist/lib/rsbuild/plugins/asset-versioning.js +62 -0
  51. package/dist/lib/rsbuild/plugins/asset-versioning.js.map +1 -0
  52. package/dist/lib/rsbuild/plugins/copy-public.d.ts +11 -0
  53. package/dist/lib/rsbuild/plugins/copy-public.d.ts.map +1 -0
  54. package/dist/lib/rsbuild/plugins/copy-public.js +32 -0
  55. package/dist/lib/rsbuild/plugins/copy-public.js.map +1 -0
  56. package/dist/lib/rsbuild/postcss/tailwind-source-fallback.d.ts +12 -0
  57. package/dist/lib/rsbuild/postcss/tailwind-source-fallback.d.ts.map +1 -0
  58. package/dist/lib/rsbuild/postcss/tailwind-source-fallback.js +60 -0
  59. package/dist/lib/rsbuild/postcss/tailwind-source-fallback.js.map +1 -0
  60. package/dist/lib/utils/setup-runtime.d.ts.map +1 -1
  61. package/dist/lib/utils/setup-runtime.js +60 -26
  62. package/dist/lib/utils/setup-runtime.js.map +1 -1
  63. package/dist/lib/vite/config-factory.d.ts.map +1 -1
  64. package/dist/lib/vite/config-factory.js +5 -1
  65. package/dist/lib/vite/config-factory.js.map +1 -1
  66. package/dist/lib/vite/plugins/copy-public.d.ts +12 -0
  67. package/dist/lib/vite/plugins/copy-public.d.ts.map +1 -0
  68. package/dist/lib/vite/plugins/copy-public.js +31 -0
  69. package/dist/lib/vite/plugins/copy-public.js.map +1 -0
  70. package/dist/schema/config.schema.d.ts +18 -1
  71. package/dist/schema/config.schema.d.ts.map +1 -1
  72. package/dist/schema/config.schema.js +1 -0
  73. package/dist/schema/config.schema.js.map +1 -1
  74. package/package.json +7 -3
  75. package/dev-server/classic/main.tsx +0 -346
  76. package/dev-server/vite.config.ts +0 -34
@@ -1,68 +1,383 @@
1
1
  import pc from 'picocolors';
2
- import { createServer as createViteServer } from 'vite';
3
- import { dirname, resolve, join } from 'node:path';
2
+ import { createRsbuild, logger } from '@rsbuild/core';
3
+ import { pluginReact } from '@rsbuild/plugin-react';
4
+ import { dirname, resolve, join, extname, relative, sep } from 'node:path';
4
5
  import { fileURLToPath } from 'node:url';
5
- import { existsSync, readFileSync, statSync } from 'node:fs';
6
- import { validateLayouts } from '../lib/build/detect-layouts.js';
6
+ import { validateLayouts, detectLayouts } from '../lib/build/detect-layouts.js';
7
7
  import { loadConfig } from '../lib/config/load-config.js';
8
+ import { detect } from 'detect-port';
9
+ import { existsSync, readFileSync, statSync, writeFileSync, mkdirSync } from 'node:fs';
10
+ import chokidar from 'chokidar';
11
+ const writeLine = (message = '') => {
12
+ process.stdout.write(`${message}\n`);
13
+ };
8
14
  const __filename = fileURLToPath(import.meta.url);
9
15
  const __dirname = dirname(__filename);
10
16
  /**
11
- * Creates a Vite plugin that watches linkapp.config.ts for changes
17
+ * Creates an Rsbuild plugin that watches linkapp.config.ts for changes
12
18
  * and triggers a full page reload when the config is updated
13
19
  */
14
- function createConfigWatcherPlugin(userProjectPath, viteServerRef) {
15
- const configPaths = [
20
+ function createConfigWatcherPlugin(userProjectPath) {
21
+ const configCandidates = [
16
22
  join(userProjectPath, 'linkapp.config.ts'),
17
23
  join(userProjectPath, '.config', 'linkapp.config.ts'),
18
24
  ];
25
+ const handleConfigReload = (server) => {
26
+ writeLine(pc.cyan(' ○ linkapp.config.ts changed, reloading...'));
27
+ try {
28
+ loadConfig(userProjectPath);
29
+ server.sockWrite('static-changed');
30
+ writeLine(pc.green(' ✓ Config reloaded'));
31
+ }
32
+ catch (error) {
33
+ writeLine(pc.red(' ✗ Failed to reload config'));
34
+ writeLine(pc.dim(` ${error instanceof Error ? error.message : error}`));
35
+ }
36
+ };
19
37
  return {
20
38
  name: 'linkapp-config-watcher',
21
- configureServer(server) {
22
- viteServerRef.current = server;
23
- // Watch for changes to linkapp.config.ts
24
- configPaths.forEach((configPath) => {
25
- if (existsSync(configPath)) {
26
- server.watcher.add(configPath);
39
+ setup(api) {
40
+ let watchers = [];
41
+ const pendingTimers = new Map();
42
+ const disposeWatchers = async () => {
43
+ for (const watcher of watchers) {
44
+ try {
45
+ await watcher.close();
46
+ }
47
+ catch (error) {
48
+ writeLine(pc.red(' ✗ Failed to close config watcher'));
49
+ writeLine(pc.dim(` ${error instanceof Error ? error.message : error}`));
50
+ }
51
+ }
52
+ watchers = [];
53
+ for (const timeout of pendingTimers.values()) {
54
+ clearTimeout(timeout);
55
+ }
56
+ pendingTimers.clear();
57
+ };
58
+ api.onBeforeStartDevServer(async ({ server }) => {
59
+ await disposeWatchers();
60
+ const filesToWatch = configCandidates.filter((candidatePath) => existsSync(candidatePath));
61
+ if (filesToWatch.length === 0) {
62
+ return;
27
63
  }
64
+ const scheduleReload = (targetPath) => {
65
+ const normalizedPath = targetPath.toString();
66
+ const existingTimeout = pendingTimers.get(normalizedPath);
67
+ if (existingTimeout) {
68
+ clearTimeout(existingTimeout);
69
+ }
70
+ const timeout = setTimeout(() => {
71
+ pendingTimers.delete(normalizedPath);
72
+ handleConfigReload(server);
73
+ }, 120);
74
+ pendingTimers.set(normalizedPath, timeout);
75
+ };
76
+ try {
77
+ const watcher = chokidar.watch(filesToWatch, {
78
+ ignoreInitial: true,
79
+ awaitWriteFinish: {
80
+ stabilityThreshold: 200,
81
+ pollInterval: 50,
82
+ },
83
+ });
84
+ watcher.on('all', (event, changedPath) => {
85
+ if (!changedPath) {
86
+ return;
87
+ }
88
+ const normalizedChangedPath = changedPath.toString();
89
+ if (!configCandidates.includes(normalizedChangedPath)) {
90
+ return;
91
+ }
92
+ switch (event) {
93
+ case 'add':
94
+ case 'change':
95
+ case 'unlink':
96
+ scheduleReload(normalizedChangedPath);
97
+ break;
98
+ default:
99
+ break;
100
+ }
101
+ });
102
+ watcher.on('error', (error) => {
103
+ writeLine(pc.red(' ✗ Config watcher encountered an error'));
104
+ writeLine(pc.dim(` ${error instanceof Error ? error.message : error}`));
105
+ });
106
+ watchers.push(watcher);
107
+ }
108
+ catch (error) {
109
+ writeLine(pc.red(' ✗ Failed to watch config file'));
110
+ writeLine(pc.dim(` ${error instanceof Error ? error.message : error}`));
111
+ }
112
+ return async () => {
113
+ await disposeWatchers();
114
+ };
28
115
  });
29
- server.watcher.on('change', (file) => {
30
- // Check if the changed file is the config file
31
- if (configPaths.some((configPath) => file === configPath)) {
32
- console.log(pc.cyan(' ○ linkapp.config.ts changed, reloading...'));
116
+ api.onCloseDevServer(async () => {
117
+ await disposeWatchers();
118
+ });
119
+ },
120
+ };
121
+ }
122
+ /**
123
+ * Creates middleware to serve static assets from the user's public directory
124
+ * while matching the legacy Vite dev server behavior.
125
+ */
126
+ function createPublicDirPlugin(userPublicDir) {
127
+ const contentTypes = {
128
+ css: 'text/css',
129
+ gif: 'image/gif',
130
+ html: 'text/html',
131
+ ico: 'image/x-icon',
132
+ jpeg: 'image/jpeg',
133
+ jpg: 'image/jpeg',
134
+ js: 'application/javascript',
135
+ json: 'application/json',
136
+ png: 'image/png',
137
+ svg: 'image/svg+xml',
138
+ txt: 'text/plain',
139
+ webp: 'image/webp',
140
+ };
141
+ return {
142
+ name: 'linkapp:public-dir',
143
+ setup(api) {
144
+ api.onBeforeStartDevServer(async ({ server }) => {
145
+ if (!existsSync(userPublicDir)) {
146
+ return;
147
+ }
148
+ const publicDirRoot = resolve(userPublicDir);
149
+ server.middlewares.use((req, res, next) => {
150
+ if (!req.url) {
151
+ next();
152
+ return;
153
+ }
154
+ const method = req.method ?? 'GET';
155
+ if (method !== 'GET' && method !== 'HEAD') {
156
+ next();
157
+ return;
158
+ }
159
+ const [rawPath] = req.url.split('?');
160
+ if (!rawPath || rawPath === '/' || rawPath === '') {
161
+ next();
162
+ return;
163
+ }
164
+ let decodedPath;
165
+ try {
166
+ decodedPath = decodeURIComponent(rawPath);
167
+ }
168
+ catch {
169
+ next();
170
+ return;
171
+ }
172
+ const normalizedPath = decodedPath.startsWith('/') ? decodedPath : `/${decodedPath}`;
173
+ const targetPath = resolve(publicDirRoot, `.${normalizedPath}`);
174
+ const relativePath = relative(publicDirRoot, targetPath);
175
+ if (relativePath.startsWith('..') ||
176
+ relativePath.includes(`..${sep}`) ||
177
+ targetPath === publicDirRoot) {
178
+ next();
179
+ return;
180
+ }
181
+ let stats;
33
182
  try {
34
- // Reload the config to validate it
35
- loadConfig(userProjectPath);
36
- // Trigger a full page reload for all connected clients
37
- // This will cause the server to re-evaluate the config on the next request
38
- server.ws.send({
39
- type: 'full-reload',
40
- path: '*',
41
- });
42
- console.log(pc.green(' ✓ Config reloaded'));
183
+ stats = statSync(targetPath);
184
+ }
185
+ catch {
186
+ next();
187
+ return;
188
+ }
189
+ if (!stats.isFile()) {
190
+ next();
191
+ return;
192
+ }
193
+ const extension = extname(targetPath).slice(1).toLowerCase();
194
+ const contentType = (extension && contentTypes[extension]) || 'application/octet-stream';
195
+ res.setHeader('Content-Type', contentType);
196
+ res.setHeader('Cache-Control', 'no-cache');
197
+ if (method === 'HEAD') {
198
+ res.end();
199
+ return;
200
+ }
201
+ try {
202
+ const content = readFileSync(targetPath);
203
+ res.end(content);
43
204
  }
44
205
  catch (error) {
45
- console.log(pc.red(' ✗ Failed to reload config'));
46
- console.log(pc.dim(` ${error instanceof Error ? error.message : error}`));
206
+ next(error);
47
207
  }
48
- }
208
+ });
49
209
  });
50
210
  },
51
211
  };
52
212
  }
213
+ /**
214
+ * Generates the sheet entry point dynamically
215
+ */
216
+ function generateSheetEntryPoint(devServerPath) {
217
+ // Use absolute paths to shared theme files in the dev-server directory
218
+ const themePresetsPath = resolve(devServerPath, 'shared/theme-presets');
219
+ const themeUtilsPath = resolve(devServerPath, 'shared/theme-utils');
220
+ return `import Sheet from "@/app/sheet";
221
+ import { StrictMode } from "react";
222
+ import { createRoot } from "react-dom/client";
223
+ import "@/app/globals.css";
224
+ import { THEME_PRESETS } from "${themePresetsPath}";
225
+ import { getThemeFromUrl, mergeThemeProps } from "${themeUtilsPath}";
226
+
227
+ // Declare global window property for theme application
228
+ declare global {
229
+ interface Window {
230
+ __linkapp_applyTheme?: (variables: Record<string, string>) => void
231
+ }
232
+ }
233
+
234
+ // Preview props injected by dev server via Vite define
235
+ declare const __PREVIEW_PROPS__: Record<string, unknown>;
236
+
237
+ // Extract just the variables from THEME_PRESETS for theme lookups
238
+ const THEME_VARS = Object.fromEntries(
239
+ Object.entries(THEME_PRESETS).map(([key, { variables }]) => [key, variables])
240
+ );
241
+
242
+ // Get theme variables from URL
243
+ const themeVariables = getThemeFromUrl(THEME_VARS);
244
+
245
+ // Merge with preview props
246
+ const previewProps = __PREVIEW_PROPS__ || {};
247
+ const mergedProps = mergeThemeProps(themeVariables, previewProps);
248
+
249
+ // Apply theme CSS variables on mount
250
+ if (themeVariables && window.__linkapp_applyTheme) {
251
+ window.__linkapp_applyTheme(themeVariables);
252
+ }
253
+
254
+ const rootElement = document.getElementById("root");
255
+ if (!rootElement) {
256
+ throw new Error("Root element not found");
257
+ }
258
+
259
+ createRoot(rootElement).render(
260
+ <StrictMode>
261
+ <Sheet {...mergedProps} />
262
+ </StrictMode>,
263
+ );
264
+ `;
265
+ }
266
+ /**
267
+ * Generates the featured entry point dynamically based on available layouts
268
+ */
269
+ function generateFeaturedEntryPoint(devServerPath, hasFeaturedCarousel) {
270
+ // Use absolute paths to shared theme files in the dev-server directory
271
+ const themePresetsPath = resolve(devServerPath, 'shared/theme-presets');
272
+ const themeUtilsPath = resolve(devServerPath, 'shared/theme-utils');
273
+ const featuredCarouselImport = hasFeaturedCarousel
274
+ ? `import FeaturedCarousel from "@/app/featured-carousel";`
275
+ : '';
276
+ const componentSelection = hasFeaturedCarousel
277
+ ? `// Select the appropriate component based on groupLayoutOption parameter
278
+ const LayoutComponent = (groupLayoutOptionParam === 'carousel' && FeaturedCarousel)
279
+ ? FeaturedCarousel
280
+ : Featured;
281
+
282
+ console.log('[Featured Dev Server] Rendering:', {
283
+ groupLayoutOption: groupLayoutOptionParam,
284
+ component: groupLayoutOptionParam === 'carousel' && FeaturedCarousel ? 'FeaturedCarousel' : 'Featured',
285
+ hasFeaturedCarousel: true,
286
+ });`
287
+ : `// Only Featured layout is available
288
+ const LayoutComponent = Featured;
289
+
290
+ console.log('[Featured Dev Server] Rendering:', {
291
+ groupLayoutOption: groupLayoutOptionParam,
292
+ component: 'Featured',
293
+ hasFeaturedCarousel: false,
294
+ });`;
295
+ return `import Featured from "@/app/featured";
296
+ ${featuredCarouselImport}
297
+ import { StrictMode } from "react";
298
+ import { createRoot } from "react-dom/client";
299
+ import "@/app/globals.css";
300
+ import { THEME_PRESETS } from "${themePresetsPath}";
301
+ import { getThemeFromUrl, mergeThemeProps } from "${themeUtilsPath}";
302
+
303
+ // Declare global window property for theme application
304
+ declare global {
305
+ interface Window {
306
+ __linkapp_applyTheme?: (variables: Record<string, string>) => void
307
+ }
308
+ }
309
+
310
+ // Preview props injected by dev server via Vite define
311
+ declare const __PREVIEW_PROPS__: Record<string, unknown>;
312
+
313
+ // Extract just the variables from THEME_PRESETS for theme lookups
314
+ const THEME_VARS = Object.fromEntries(
315
+ Object.entries(THEME_PRESETS).map(([key, { variables }]) => [key, variables])
316
+ );
317
+
318
+ // Get theme variables and groupLayoutOption from URL
319
+ const params = new URLSearchParams(window.location.search);
320
+ const groupLayoutOptionParam = params.get('groupLayoutOption');
321
+ const themeVariables = getThemeFromUrl(THEME_VARS);
322
+
323
+ // Merge with preview props and add groupLayoutOption
324
+ const previewProps = __PREVIEW_PROPS__ || {};
325
+ const mergedProps = mergeThemeProps(themeVariables, previewProps, {
326
+ groupLayoutOption: groupLayoutOptionParam || undefined,
327
+ });
328
+
329
+ // Apply theme CSS variables on mount
330
+ if (themeVariables && window.__linkapp_applyTheme) {
331
+ window.__linkapp_applyTheme(themeVariables);
332
+ }
333
+
334
+ const rootElement = document.getElementById("root");
335
+ if (!rootElement) {
336
+ throw new Error("Root element not found");
337
+ }
338
+
339
+ ${componentSelection}
340
+
341
+ createRoot(rootElement).render(
342
+ <StrictMode>
343
+ <LayoutComponent {...mergedProps} />
344
+ </StrictMode>,
345
+ );
346
+ `;
347
+ }
53
348
  export async function devCommand(options) {
54
349
  const userProjectPath = process.cwd();
350
+ process.env.LINKAPP_USER_PROJECT_PATH = userProjectPath;
351
+ // Suppress Rsbuild's built-in logging messages
352
+ logger.override({
353
+ start: () => { }, // Suppress "start build started..." messages
354
+ ready: () => { }, // Suppress "ready built in..." messages
355
+ });
55
356
  try {
56
357
  const startTime = Date.now();
57
358
  // Validate layouts exist
58
359
  const validation = validateLayouts(userProjectPath);
59
360
  if (!validation.valid) {
60
- console.log(pc.red('✗ Invalid project structure'));
361
+ writeLine(pc.red('✗ Invalid project structure'));
61
362
  for (const error of validation.errors) {
62
- console.log(pc.red(` ${error}`));
363
+ writeLine(pc.red(` ${error}`));
63
364
  }
64
365
  process.exit(1);
65
366
  }
367
+ // Get dev server path (needed for generating entry points and later for Rsbuild config)
368
+ const devServerPath = resolve(__dirname, '../../dev-server');
369
+ // Detect available layouts for dynamic entry generation
370
+ const layoutDetection = detectLayouts(userProjectPath);
371
+ const hasFeaturedCarousel = layoutDetection.layouts.some(l => l.name === 'featured-carousel');
372
+ // Generate dynamic entry points in .linkapp directory
373
+ const linkappDir = join(userProjectPath, '.linkapp');
374
+ if (!existsSync(linkappDir)) {
375
+ mkdirSync(linkappDir, { recursive: true });
376
+ }
377
+ const sheetEntryPath = join(linkappDir, 'dev-sheet-main.tsx');
378
+ const featuredEntryPath = join(linkappDir, 'dev-featured-main.tsx');
379
+ writeFileSync(sheetEntryPath, generateSheetEntryPoint(devServerPath), 'utf-8');
380
+ writeFileSync(featuredEntryPath, generateFeaturedEntryPoint(devServerPath, hasFeaturedCarousel), 'utf-8');
66
381
  // Load config and extract preview_props and settings
67
382
  let previewProps = {};
68
383
  let settingsConfig = {};
@@ -72,114 +387,212 @@ export async function devCommand(options) {
72
387
  settingsConfig = config.settings || {};
73
388
  }
74
389
  catch (error) {
75
- console.log(pc.yellow('⚠ Warning: Could not load config, using default preview props'));
76
- console.log(pc.dim(` ${error instanceof Error ? error.message : error}`));
390
+ writeLine(pc.yellow('⚠ Warning: Could not load config, using default preview props'));
391
+ writeLine(pc.dim(` ${error instanceof Error ? error.message : error}`));
77
392
  }
78
- const port = options.port || 3000;
79
- const devServerPath = resolve(__dirname, '../../dev-server');
393
+ const requestedPort = options.port || 3000;
394
+ const availablePort = await detect(requestedPort);
395
+ // Show warning if the requested port was occupied
396
+ if (requestedPort !== availablePort) {
397
+ writeLine(pc.yellow(` ⚠ Port ${requestedPort} is in use, using ${availablePort} instead`));
398
+ writeLine();
399
+ }
400
+ const port = availablePort;
80
401
  const userPublicDir = join(userProjectPath, 'public');
81
- // Create a ref to hold the Vite server instance for the config watcher
82
- const viteServerRef = { current: null };
83
- // Create Vite server - inject preview props directly via define
84
- const viteServer = await createViteServer({
85
- configFile: resolve(devServerPath, 'vite.config.ts'),
86
- root: devServerPath,
87
- publicDir: userPublicDir,
88
- resolve: {
89
- alias: {
90
- '@': userProjectPath,
402
+ const printReadyMessage = (readyTime, urls) => {
403
+ writeLine();
404
+ writeLine(pc.bold(pc.cyan(' LinkApp')) + pc.dim(' ready'));
405
+ writeLine();
406
+ if (urls.length > 0) {
407
+ writeLine(` ${pc.green('➜')} ${pc.bold('Local:')} ${pc.cyan(urls[0])}`);
408
+ }
409
+ writeLine();
410
+ writeLine(pc.green(` ✓ Ready in ${readyTime}s`));
411
+ };
412
+ const setupKeyboardShortcuts = (handlers) => {
413
+ if (!process.stdin.isTTY) {
414
+ return () => { };
415
+ }
416
+ const handleData = (data) => {
417
+ if (data === '\u0003') {
418
+ void handlers.exit();
419
+ return;
420
+ }
421
+ const key = data.trim().toLowerCase();
422
+ if (key === '') {
423
+ return;
424
+ }
425
+ switch (key) {
426
+ case 'r':
427
+ void handlers.restart();
428
+ break;
429
+ case 'u':
430
+ handlers.showUrls();
431
+ break;
432
+ case 'c':
433
+ handlers.clear();
434
+ break;
435
+ case 'q':
436
+ void handlers.exit();
437
+ break;
438
+ default:
439
+ break;
440
+ }
441
+ };
442
+ process.stdin.setRawMode(true);
443
+ process.stdin.resume();
444
+ process.stdin.setEncoding('utf8');
445
+ process.stdin.on('data', handleData);
446
+ return () => {
447
+ process.stdin.off('data', handleData);
448
+ if (process.stdin.isTTY) {
449
+ process.stdin.setRawMode(false);
450
+ }
451
+ process.stdin.pause();
452
+ };
453
+ };
454
+ // Create Rsbuild server - inject preview props directly via define
455
+ const rsbuild = await createRsbuild({
456
+ rsbuildConfig: {
457
+ dev: {
458
+ progressBar: false,
91
459
  },
92
- // Dedupe React to avoid "Invalid hook call" errors from multiple React instances
93
- dedupe: ['react', 'react-dom'],
94
- },
95
- define: {
96
- __PREVIEW_PROPS__: JSON.stringify(previewProps),
97
- __SETTINGS_CONFIG__: JSON.stringify(settingsConfig),
98
- },
99
- server: {
100
- port,
101
- host: 'localhost',
102
- open: false,
103
- strictPort: false,
104
- },
105
- logLevel: 'error',
106
- plugins: [
107
- // Watch for config changes and reload
108
- createConfigWatcherPlugin(userProjectPath, viteServerRef),
109
- // Custom plugin to serve user's public directory
110
- {
111
- name: 'serve-user-public',
112
- configureServer(server) {
113
- // Only serve if public directory exists
114
- if (existsSync(userPublicDir)) {
115
- // Return middleware that runs early in the stack
116
- return () => {
117
- server.middlewares.use((req, res, next) => {
118
- if (!req.url) {
119
- next();
120
- return;
121
- }
122
- // Parse URL and remove query params
123
- const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
124
- const pathname = url.pathname;
125
- // Try to serve file from public directory
126
- const filePath = join(userPublicDir, pathname);
127
- try {
128
- if (existsSync(filePath) && statSync(filePath).isFile()) {
129
- // Determine content type
130
- const ext = pathname.split('.').pop()?.toLowerCase();
131
- const contentTypes = {
132
- 'png': 'image/png',
133
- 'jpg': 'image/jpeg',
134
- 'jpeg': 'image/jpeg',
135
- 'gif': 'image/gif',
136
- 'svg': 'image/svg+xml',
137
- 'webp': 'image/webp',
138
- 'ico': 'image/x-icon',
139
- 'json': 'application/json',
140
- 'txt': 'text/plain',
141
- 'html': 'text/html',
142
- 'css': 'text/css',
143
- 'js': 'application/javascript',
144
- };
145
- const contentType = (ext && contentTypes[ext]) || 'application/octet-stream';
146
- const content = readFileSync(filePath);
147
- res.setHeader('Content-Type', contentType);
148
- res.setHeader('Cache-Control', 'no-cache');
149
- res.end(content);
150
- return;
151
- }
152
- }
153
- catch (error) {
154
- // File doesn't exist or error reading, continue to next middleware
155
- }
156
- next();
157
- });
158
- };
159
- }
460
+ plugins: [
461
+ pluginReact(),
462
+ createConfigWatcherPlugin(userProjectPath),
463
+ createPublicDirPlugin(userPublicDir),
464
+ ],
465
+ source: {
466
+ entry: {
467
+ index: resolve(devServerPath, 'preview/main.tsx'),
468
+ sheet: sheetEntryPath,
469
+ featured: featuredEntryPath,
470
+ },
471
+ define: {
472
+ __PREVIEW_PROPS__: JSON.stringify(previewProps),
473
+ __SETTINGS_CONFIG__: JSON.stringify(settingsConfig),
160
474
  },
161
475
  },
162
- ],
476
+ resolve: {
477
+ alias: {
478
+ '@': userProjectPath,
479
+ },
480
+ dedupe: ['react', 'react-dom'],
481
+ },
482
+ html: {
483
+ template({ entryName }) {
484
+ const templates = {
485
+ index: resolve(devServerPath, 'index.html'),
486
+ sheet: resolve(devServerPath, 'sheet.html'),
487
+ featured: resolve(devServerPath, 'featured.html'),
488
+ };
489
+ return templates[entryName] || resolve(devServerPath, 'index.html');
490
+ },
491
+ },
492
+ server: {
493
+ port,
494
+ host: 'localhost',
495
+ open: false,
496
+ strictPort: false,
497
+ printUrls: false,
498
+ publicDir: {
499
+ name: userPublicDir,
500
+ watch: true,
501
+ },
502
+ },
503
+ tools: {
504
+ postcss: (opts) => {
505
+ // Load PostCSS config from dev-server directory
506
+ opts.postcssOptions = {
507
+ ...opts.postcssOptions,
508
+ config: resolve(devServerPath, 'postcss.config.mjs'),
509
+ };
510
+ return opts;
511
+ },
512
+ },
513
+ },
514
+ });
515
+ let currentServer;
516
+ let cleanupShortcuts;
517
+ let isRestarting = false;
518
+ let isShuttingDown = false;
519
+ let currentUrls = [];
520
+ let lastReadyTime = '';
521
+ const startServer = async (startedAt) => {
522
+ const result = await rsbuild.startDevServer();
523
+ currentServer = result;
524
+ currentUrls = result.urls;
525
+ lastReadyTime = ((Date.now() - startedAt) / 1000).toFixed(1);
526
+ printReadyMessage(lastReadyTime, currentUrls);
527
+ };
528
+ const restartServer = async () => {
529
+ if (isShuttingDown || isRestarting || !currentServer) {
530
+ return;
531
+ }
532
+ isRestarting = true;
533
+ writeLine();
534
+ writeLine(pc.cyan(' ↻ Restarting dev server...'));
535
+ try {
536
+ await currentServer.server.close();
537
+ const restartStart = Date.now();
538
+ await startServer(restartStart);
539
+ }
540
+ catch (error) {
541
+ currentServer = undefined;
542
+ writeLine(pc.red('✗ Failed to restart dev server'));
543
+ writeLine(pc.dim(` ${error instanceof Error ? error.message : error}`));
544
+ }
545
+ finally {
546
+ isRestarting = false;
547
+ }
548
+ };
549
+ const showUrls = () => {
550
+ writeLine();
551
+ if (currentUrls.length > 0) {
552
+ writeLine(` ${pc.green('➜')} ${pc.bold('Local:')} ${pc.cyan(currentUrls[0])}`);
553
+ }
554
+ writeLine();
555
+ };
556
+ const clearConsole = () => {
557
+ console.clear();
558
+ if (lastReadyTime) {
559
+ printReadyMessage(lastReadyTime, currentUrls);
560
+ }
561
+ };
562
+ const shutdown = async () => {
563
+ if (isShuttingDown) {
564
+ return;
565
+ }
566
+ isShuttingDown = true;
567
+ cleanupShortcuts?.();
568
+ try {
569
+ if (currentServer) {
570
+ await currentServer.server.close();
571
+ }
572
+ }
573
+ catch (closeError) {
574
+ writeLine(pc.red('✗ Error while closing dev server'));
575
+ writeLine(pc.dim(` ${closeError instanceof Error ? closeError.message : closeError}`));
576
+ }
577
+ finally {
578
+ process.exit(0);
579
+ }
580
+ };
581
+ await startServer(startTime);
582
+ cleanupShortcuts = setupKeyboardShortcuts({
583
+ restart: restartServer,
584
+ showUrls,
585
+ clear: clearConsole,
586
+ exit: shutdown,
163
587
  });
164
- await viteServer.listen();
165
- const readyTime = ((Date.now() - startTime) / 1000).toFixed(1);
166
- console.log(pc.bold(pc.cyan(' LinkApp')) + pc.dim(' ready'));
167
- console.log();
168
- console.log(pc.green(' ➜') + pc.bold(' Local: ') + pc.cyan(`http://localhost:${port}`));
169
- console.log();
170
- viteServer.bindCLIShortcuts({ print: true });
171
- console.log(pc.dim(` ✓ Ready in ${readyTime}s`));
172
- // Handle Ctrl+C
173
- process.on('SIGINT', async () => {
174
- console.log('\n');
175
- await viteServer.close();
176
- process.exit(0);
588
+ process.on('SIGINT', () => {
589
+ void shutdown();
177
590
  });
178
591
  // Keep alive
179
592
  await new Promise(() => { });
180
593
  }
181
594
  catch (error) {
182
- console.log(pc.red('✗ Failed to start server'));
595
+ writeLine(pc.red('✗ Failed to start server'));
183
596
  console.error(pc.red('✗ Error:'), error);
184
597
  process.exit(1);
185
598
  }