@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.
- package/package.json +6 -2
- package/DESIGN.md +0 -901
- package/ZERO_CONFIG_GUIDE.md +0 -462
- package/src/commands/dev.ts +0 -251
- package/src/commands/init.ts +0 -138
- package/src/commands/pack.ts +0 -315
- package/src/index.ts +0 -84
- package/src/utils/builder.ts +0 -472
- package/tsconfig.json +0 -9
package/src/utils/builder.ts
DELETED
|
@@ -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
|
-
}
|