@linyjs/cli 0.0.4 → 0.0.6

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.
@@ -1,472 +0,0 @@
1
- /**
2
- * Smart builder for plugin projects
3
- *
4
- * Design Philosophy:
5
- * - Zero configuration required
6
- * - CLI handles all build details automatically
7
- * - Users just write TypeScript/TSX code
8
- * - Sensible defaults and optimizations out of the box
9
- */
10
-
11
- import fs from 'fs';
12
- import path from 'path';
13
- import { execSync } from 'child_process';
14
- import chalk from 'chalk';
15
- import ora from 'ora';
16
-
17
- export interface BuildOptions {
18
- projectRoot: string;
19
- verbose: boolean;
20
- minify?: boolean;
21
- external?: string[];
22
- }
23
-
24
- export interface BuildResult {
25
- success: boolean;
26
- outputDir: string;
27
- files: string[];
28
- size: number;
29
- duration: number;
30
- }
31
-
32
- /**
33
- * Detect build system and execute build
34
- */
35
- export async function buildProject(options: BuildOptions): Promise<BuildResult> {
36
- const startTime = Date.now();
37
- const { projectRoot, verbose } = options;
38
-
39
- const spinner = ora('Detecting build system...').start();
40
-
41
- // Step 1: Detect build system
42
- const buildSystem = detectBuildSystem(projectRoot);
43
-
44
- if (!buildSystem) {
45
- spinner.fail('No build system detected');
46
- console.error(chalk.red('\nPlease add a build script to package.json:'));
47
- console.error(chalk.gray(' "scripts": { "build": "tsc" }'));
48
- process.exit(1);
49
- }
50
-
51
- spinner.succeed(`Build system detected: ${buildSystem.name}`);
52
-
53
- if (verbose) {
54
- console.log(chalk.gray(' Type:'), buildSystem.type);
55
- console.log(chalk.gray(' Config:'), buildSystem.configFile || 'none');
56
- }
57
-
58
- // Step 2: Execute build
59
- const buildSpinner = ora('Building project...').start();
60
-
61
- try {
62
- let outputDir: string;
63
-
64
- // DEFAULT: Always use esbuild for optimal bundles
65
- // Users don't need to configure webpack/rollup themselves
66
- if (buildSystem.type === 'typescript' || buildSystem.type === 'custom') {
67
- // Even if project has tsc, we use esbuild for better optimization
68
- if (verbose) {
69
- console.log(chalk.yellow('\nšŸ’” Using esbuild for optimized bundle (ignoring tsc config)'));
70
- }
71
- outputDir = await buildWithEsbuild(projectRoot, buildSystem, options, verbose);
72
- } else if (buildSystem.type === 'esbuild') {
73
- // Project already uses esbuild, use it directly
74
- outputDir = await buildWithEsbuild(projectRoot, buildSystem, options, verbose);
75
- } else {
76
- // For webpack/rollup, still prefer our esbuild approach
77
- if (verbose) {
78
- console.log(chalk.yellow('\nšŸ’” Using CLI built-in esbuild (ignoring webpack/rollup config)'));
79
- }
80
- outputDir = await buildWithEsbuild(projectRoot, buildSystem, options, verbose);
81
- }
82
-
83
- // Step 3: Collect output files
84
- const files = collectOutputFiles(outputDir);
85
- const size = calculateTotalSize(files);
86
- const duration = Date.now() - startTime;
87
-
88
- buildSpinner.succeed(`Build completed in ${duration}ms`);
89
-
90
- if (verbose) {
91
- console.log(chalk.gray(' Output directory:'), outputDir);
92
- console.log(chalk.gray(' Total files:'), files.length);
93
- console.log(chalk.gray(' Total size:'), formatFileSize(size));
94
- }
95
-
96
- return {
97
- success: true,
98
- outputDir,
99
- files,
100
- size,
101
- duration
102
- };
103
- } catch (error) {
104
- buildSpinner.fail('Build failed');
105
-
106
- if (error instanceof Error) {
107
- console.error(chalk.red('\nBuild error:'));
108
- console.error(error.message);
109
-
110
- if (verbose && 'stdout' in error) {
111
- console.error(chalk.gray('\nDetailed output:'));
112
- console.error((error as any).stdout?.toString());
113
- }
114
- }
115
-
116
- process.exit(1);
117
- }
118
- }
119
-
120
- /**
121
- * Detect build system from project configuration
122
- */
123
- function detectBuildSystem(projectRoot: string): BuildSystem | null {
124
- const packageJsonPath = path.join(projectRoot, 'package.json');
125
-
126
- if (!fs.existsSync(packageJsonPath)) {
127
- return null;
128
- }
129
-
130
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
131
- const buildScript = packageJson.scripts?.build || '';
132
-
133
- // Check for TypeScript
134
- if (fs.existsSync(path.join(projectRoot, 'tsconfig.json'))) {
135
- if (buildScript.includes('tsc') || buildScript.includes('typescript')) {
136
- return {
137
- type: 'typescript',
138
- name: 'TypeScript Compiler',
139
- configFile: 'tsconfig.json',
140
- command: buildScript
141
- };
142
- }
143
- }
144
-
145
- // Check for esbuild
146
- if (buildScript.includes('esbuild')) {
147
- return {
148
- type: 'esbuild',
149
- name: 'esbuild',
150
- configFile: null,
151
- command: buildScript
152
- };
153
- }
154
-
155
- // Check for webpack
156
- if (fs.existsSync(path.join(projectRoot, 'webpack.config.js')) ||
157
- fs.existsSync(path.join(projectRoot, 'webpack.config.ts'))) {
158
- return {
159
- type: 'webpack',
160
- name: 'Webpack',
161
- configFile: 'webpack.config.js',
162
- command: buildScript
163
- };
164
- }
165
-
166
- // Check for rollup
167
- if (fs.existsSync(path.join(projectRoot, 'rollup.config.js'))) {
168
- return {
169
- type: 'rollup',
170
- name: 'Rollup',
171
- configFile: 'rollup.config.js',
172
- command: buildScript
173
- };
174
- }
175
-
176
- // Default to npm run build
177
- if (buildScript) {
178
- return {
179
- type: 'custom',
180
- name: 'Custom Build Script',
181
- configFile: null,
182
- command: buildScript
183
- };
184
- }
185
-
186
- return null;
187
- }
188
-
189
- /**
190
- * Build with TypeScript compiler
191
- */
192
- async function buildWithTsc(
193
- projectRoot: string,
194
- buildSystem: BuildSystem,
195
- verbose: boolean
196
- ): Promise<string> {
197
- // Read tsconfig.json to get output directory
198
- const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
199
- const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, 'utf-8'));
200
- const outputDir = tsconfig.compilerOptions?.outDir || 'dist';
201
-
202
- // Execute tsc
203
- execSync('npx tsc', {
204
- stdio: verbose ? 'inherit' : 'pipe',
205
- cwd: projectRoot
206
- });
207
-
208
- return path.join(projectRoot, outputDir);
209
- }
210
-
211
- /**
212
- * Build with esbuild (DEFAULT strategy)
213
- * This is the recommended approach for production builds
214
- */
215
- async function buildWithEsbuild(
216
- projectRoot: string,
217
- buildSystem: BuildSystem,
218
- options: BuildOptions,
219
- verbose: boolean
220
- ): Promise<string> {
221
- const outputDir = path.join(projectRoot, 'dist');
222
-
223
- // Ensure output directory exists
224
- if (!fs.existsSync(outputDir)) {
225
- fs.mkdirSync(outputDir, { recursive: true });
226
- }
227
-
228
- // Detect entry points (smart detection)
229
- const serverEntry = findEntryFile(projectRoot, 'server');
230
- const clientEntry = findEntryFile(projectRoot, 'client');
231
-
232
- if (!serverEntry && !clientEntry) {
233
- throw new Error('No entry files found. Expected src/server/index.ts or src/client/index.tsx');
234
- }
235
-
236
- // Default external dependencies (Ling framework provides these)
237
- const defaultExternal = [
238
- 'react',
239
- 'react-dom',
240
- 'express',
241
- '@linyjs/*',
242
- '@ling/*'
243
- ];
244
-
245
- const externalDeps = options.external || defaultExternal;
246
-
247
- // Build server bundle (if exists)
248
- if (serverEntry) {
249
- const serverOutput = path.join(outputDir, 'server', 'index.js');
250
-
251
- const serverCmd = [
252
- 'npx esbuild',
253
- serverEntry,
254
- '--bundle',
255
- `--outfile=${serverOutput}`,
256
- '--format=esm',
257
- '--platform=node',
258
- '--target=node18',
259
- ...externalDeps.map(dep => `--external:${dep}`),
260
- options.minify !== false ? '--minify' : '', // Default to minify
261
- '--sourcemap',
262
- '--log-level=warning'
263
- ].filter(Boolean).join(' ');
264
-
265
- if (verbose) {
266
- console.log(chalk.cyan('\nšŸ”Ø Building server bundle...'));
267
- console.log(chalk.gray(' Entry:'), serverEntry);
268
- console.log(chalk.gray(' Output:'), serverOutput);
269
- console.log(chalk.gray(' Command:'), serverCmd);
270
- }
271
-
272
- try {
273
- execSync(serverCmd, {
274
- stdio: verbose ? 'inherit' : 'pipe',
275
- cwd: projectRoot
276
- });
277
-
278
- if (verbose) {
279
- const size = fs.statSync(serverOutput).size;
280
- console.log(chalk.green(' āœ“ Server bundle created'), chalk.gray(`(${formatFileSize(size)})`));
281
- }
282
- } catch (error) {
283
- throw new Error(`Server build failed: ${(error as Error).message}`);
284
- }
285
- }
286
-
287
- // Build client bundle (if exists)
288
- if (clientEntry) {
289
- const clientOutput = path.join(outputDir, 'client', 'index.js');
290
-
291
- // For client bundle, we need different external dependencies
292
- // Server-only packages should NOT be bundled in client
293
- const clientExternal = [
294
- // āš ļø IMPORTANT: React and ReactDOM MUST be externalized
295
- // All plugins must share the SAME React instance to avoid:
296
- // - Context identity issues (createContext from different React instances are not equal)
297
- // - Hook ordering problems (different React versions have different internal structures)
298
- // - Memory leaks (multiple React renderers running simultaneously)
299
- // - Dependency injection failures (contexts/services created by one React can't be used by another)
300
- 'react', // Provided by SSR host application
301
- 'react-dom', // Provided by SSR host application
302
- 'react-dom/client', // Provided by SSR host application
303
-
304
- // UI slots interface is type-only, safe to external
305
- '@linyjs/ui-slots-interface',
306
-
307
- // Explicitly exclude server-only packages
308
- // These should never appear in client code, but just in case:
309
- 'express',
310
- '@linyjs/server-module-interface', // Server-only interface (type-only)
311
- '@linyjs/mongodb', // Server-only service
312
-
313
- // NOTE: @linyjs/client-module-interface is NOT externalized
314
- // because it contains runtime values (React Context, Hooks)
315
- // that must be included in the client bundle
316
- ];
317
-
318
- // Merge with user-provided externals (if any)
319
- if (options.external) {
320
- options.external.forEach(dep => {
321
- if (!clientExternal.includes(dep)) {
322
- clientExternal.push(dep);
323
- }
324
- });
325
- }
326
-
327
- const clientCmd = [
328
- 'npx esbuild',
329
- clientEntry,
330
- '--bundle',
331
- `--outfile=${clientOutput}`,
332
- '--format=esm',
333
- '--platform=browser',
334
- '--target=es2020',
335
- ...clientExternal.map(dep => `--external:${dep}`),
336
- options.minify !== false ? '--minify' : '',
337
- '--sourcemap',
338
- '--log-level=warning'
339
- ].filter(Boolean).join(' ');
340
-
341
- if (verbose) {
342
- console.log(chalk.cyan('\nšŸ”Ø Building client bundle...'));
343
- console.log(chalk.gray(' Entry:'), clientEntry);
344
- console.log(chalk.gray(' Output:'), clientOutput);
345
- console.log(chalk.gray(' Command:'), clientCmd);
346
- }
347
-
348
- try {
349
- execSync(clientCmd, {
350
- stdio: verbose ? 'inherit' : 'pipe',
351
- cwd: projectRoot
352
- });
353
-
354
- if (verbose) {
355
- const size = fs.statSync(clientOutput).size;
356
- console.log(chalk.green(' āœ“ Client bundle created'), chalk.gray(`(${formatFileSize(size)})`));
357
- }
358
- } catch (error) {
359
- throw new Error(`Client build failed: ${(error as Error).message}`);
360
- }
361
- }
362
-
363
- return outputDir;
364
- }
365
-
366
- /**
367
- * Build with npm run build (fallback)
368
- */
369
- async function buildWithNpm(projectRoot: string, verbose: boolean): Promise<string> {
370
- execSync('npm run build', {
371
- stdio: verbose ? 'inherit' : 'pipe',
372
- cwd: projectRoot
373
- });
374
-
375
- // Try to detect output directory
376
- const possibleDirs = ['dist', 'build', 'lib'];
377
- for (const dir of possibleDirs) {
378
- const fullPath = path.join(projectRoot, dir);
379
- if (fs.existsSync(fullPath)) {
380
- return fullPath;
381
- }
382
- }
383
-
384
- return path.join(projectRoot, 'dist');
385
- }
386
-
387
- /**
388
- * Find entry file for server or client
389
- */
390
- function findEntryFile(projectRoot: string, type: 'server' | 'client'): string | null {
391
- const possibleEntries = [
392
- `src/${type}/index.ts`,
393
- `src/${type}/index.tsx`,
394
- `src/${type}.ts`,
395
- `src/${type}.tsx`,
396
- `${type}/index.ts`
397
- ];
398
-
399
- for (const entry of possibleEntries) {
400
- const fullPath = path.join(projectRoot, entry);
401
- if (fs.existsSync(fullPath)) {
402
- return entry;
403
- }
404
- }
405
-
406
- return null;
407
- }
408
-
409
- /**
410
- * Collect all output files
411
- */
412
- function collectOutputFiles(outputDir: string): string[] {
413
- const files: string[] = [];
414
-
415
- function walk(dir: string) {
416
- const entries = fs.readdirSync(dir, { withFileTypes: true });
417
-
418
- for (const entry of entries) {
419
- const fullPath = path.join(dir, entry.name);
420
-
421
- if (entry.isDirectory()) {
422
- // Skip node_modules and .git
423
- if (entry.name === 'node_modules' || entry.name === '.git') {
424
- continue;
425
- }
426
- walk(fullPath);
427
- } else {
428
- files.push(fullPath);
429
- }
430
- }
431
- }
432
-
433
- walk(outputDir);
434
- return files;
435
- }
436
-
437
- /**
438
- * Calculate total size of files
439
- */
440
- function calculateTotalSize(files: string[]): number {
441
- return files.reduce((total, file) => {
442
- try {
443
- const stats = fs.statSync(file);
444
- return total + stats.size;
445
- } catch {
446
- return total;
447
- }
448
- }, 0);
449
- }
450
-
451
- /**
452
- * Format file size
453
- */
454
- function formatFileSize(bytes: number): string {
455
- const units = ['B', 'KB', 'MB', 'GB'];
456
- let size = bytes;
457
- let unitIndex = 0;
458
-
459
- while (size >= 1024 && unitIndex < units.length - 1) {
460
- size /= 1024;
461
- unitIndex++;
462
- }
463
-
464
- return `${size.toFixed(2)} ${units[unitIndex]}`;
465
- }
466
-
467
- interface BuildSystem {
468
- type: 'typescript' | 'esbuild' | 'webpack' | 'rollup' | 'custom';
469
- name: string;
470
- configFile: string | null;
471
- command: string;
472
- }
package/tsconfig.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "rootDir": "./src",
6
- "lib": ["ES2022", "dom"]
7
- },
8
- "include": ["src"]
9
- }