@bagelink/workspace 1.7.3 → 1.7.8

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.
@@ -0,0 +1,505 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+ import process from 'node:process'
4
+ import prompts from 'prompts'
5
+
6
+ /**
7
+ * Initialize a new workspace with flat structure
8
+ */
9
+ export async function initWorkspace(root: string = process.cwd()): Promise<void> {
10
+ console.log('\nšŸš€ Creating Bagel workspace...\n')
11
+
12
+ const response = await prompts([
13
+ {
14
+ type: 'text',
15
+ name: 'workspaceName',
16
+ message: 'Workspace name:',
17
+ initial: 'my-workspace',
18
+ },
19
+ {
20
+ type: 'text',
21
+ name: 'projectId',
22
+ message: 'Bagel project ID:',
23
+ initial: 'my-project',
24
+ },
25
+ {
26
+ type: 'confirm',
27
+ name: 'createFirstProject',
28
+ message: 'Create first project?',
29
+ initial: true,
30
+ },
31
+ {
32
+ type: (prev: boolean) => (prev ? 'text' : null),
33
+ name: 'firstProjectName',
34
+ message: 'First project name:',
35
+ initial: 'web',
36
+ },
37
+ ])
38
+
39
+ if (!response || !response.workspaceName) {
40
+ console.log('\nāŒ Workspace creation cancelled.\n')
41
+ process.exit(1)
42
+ }
43
+
44
+ const { workspaceName, projectId, createFirstProject, firstProjectName } = response
45
+
46
+ const workspaceDir = resolve(root, workspaceName)
47
+
48
+ // Create workspace structure
49
+ createWorkspaceRoot(root, workspaceName, projectId)
50
+
51
+ // Create shared package
52
+ createSharedPackage(workspaceDir)
53
+
54
+ if (createFirstProject && firstProjectName) {
55
+ await addProject(firstProjectName, workspaceDir)
56
+ }
57
+
58
+ console.log('\nāœ… Workspace created successfully!')
59
+ console.log('\nNext steps:')
60
+ console.log(` cd ${workspaceName}`)
61
+ console.log(' bun install')
62
+ if (createFirstProject) {
63
+ console.log(` bun run dev:${firstProjectName}`)
64
+ }
65
+ else {
66
+ console.log(' bgl add <project-name> # Add a project')
67
+ }
68
+ console.log('')
69
+ }
70
+
71
+ /**
72
+ * Create workspace root files
73
+ */
74
+ function createWorkspaceRoot(root: string, name: string, projectId: string): void {
75
+ const workspaceDir = resolve(root, name)
76
+
77
+ if (existsSync(workspaceDir)) {
78
+ console.error(`āŒ Directory ${name} already exists`)
79
+ process.exit(1)
80
+ }
81
+
82
+ mkdirSync(workspaceDir, { recursive: true })
83
+
84
+ // package.json
85
+ const packageJson = {
86
+ name,
87
+ private: true,
88
+ workspaces: ['*', '!node_modules'],
89
+ scripts: {
90
+ dev: 'bun scripts/dev.ts',
91
+ 'dev:verbose': 'bun run --filter \'./[!shared]*\' dev',
92
+ build: 'bun run --filter \'./[!shared]*\' build',
93
+ typecheck: 'tsc --noEmit',
94
+ },
95
+ devDependencies: {
96
+ '@bagelink/workspace': 'latest',
97
+ 'typescript': '^5.0.0',
98
+ 'vite': 'latest',
99
+ },
100
+ }
101
+
102
+ writeFileSync(
103
+ resolve(workspaceDir, 'package.json'),
104
+ `${JSON.stringify(packageJson, null, 2)}\n`,
105
+ )
106
+
107
+ // bgl.config.ts (shared)
108
+ const bglConfig = `import { defineWorkspace } from '@bagelink/workspace'
109
+
110
+ export default defineWorkspace({
111
+ localhost: {
112
+ host: 'http://localhost:8000',
113
+ proxy: '/api',
114
+ openapi_url: 'http://localhost:8000/openapi.json',
115
+ },
116
+ development: {
117
+ host: 'https://${projectId}.bagel.to',
118
+ proxy: '/api',
119
+ openapi_url: 'https://${projectId}.bagel.to/openapi.json',
120
+ },
121
+ production: {
122
+ host: 'https://${projectId}.bagel.to',
123
+ proxy: '/api',
124
+ openapi_url: 'https://${projectId}.bagel.to/openapi.json',
125
+ },
126
+ })
127
+ `
128
+
129
+ writeFileSync(resolve(workspaceDir, 'bgl.config.ts'), bglConfig)
130
+
131
+ // tsconfig.json
132
+ const tsConfig = {
133
+ compilerOptions: {
134
+ target: 'ES2020',
135
+ useDefineForClassFields: true,
136
+ module: 'ESNext',
137
+ lib: ['ES2020', 'DOM', 'DOM.Iterable'],
138
+ skipLibCheck: true,
139
+ moduleResolution: 'bundler',
140
+ allowImportingTsExtensions: true,
141
+ resolveJsonModule: true,
142
+ isolatedModules: true,
143
+ noEmit: true,
144
+ jsx: 'preserve',
145
+ strict: true,
146
+ noUnusedLocals: true,
147
+ noUnusedParameters: true,
148
+ noFallthroughCasesInSwitch: true,
149
+ baseUrl: '.',
150
+ paths: {
151
+ 'shared/*': ['./shared/*'],
152
+ },
153
+ },
154
+ }
155
+
156
+ writeFileSync(
157
+ resolve(workspaceDir, 'tsconfig.json'),
158
+ `${JSON.stringify(tsConfig, null, 2)}\n`,
159
+ )
160
+
161
+ // .gitignore
162
+ const gitignore = `node_modules
163
+ dist
164
+ .DS_Store
165
+ *.local
166
+ .env.local
167
+ .vite
168
+ `
169
+
170
+ writeFileSync(resolve(workspaceDir, '.gitignore'), gitignore)
171
+
172
+ // Create scripts directory
173
+ const scriptsDir = resolve(workspaceDir, 'scripts')
174
+ mkdirSync(scriptsDir, { recursive: true })
175
+
176
+ // Copy dev runner script
177
+ const devRunnerContent = `#!/usr/bin/env bun
178
+ import { spawn } from 'bun'
179
+ import { readdir } from 'fs/promises'
180
+ import { resolve } from 'path'
181
+
182
+ const projectsRoot = process.cwd()
183
+ const projects = (await readdir(projectsRoot, { withFileTypes: true }))
184
+ .filter(
185
+ item =>
186
+ item.isDirectory()
187
+ && item.name !== 'node_modules'
188
+ && item.name !== 'shared'
189
+ && item.name !== 'scripts'
190
+ && item.name !== '.git'
191
+ && !item.name.startsWith('.'),
192
+ )
193
+ .map(item => item.name)
194
+
195
+ console.log(\`\\nšŸš€ Starting \${projects.length} project\${projects.length > 1 ? 's' : ''}...\\n\`)
196
+
197
+ const urlPattern = /Local:\\s+(http:\\/\\/localhost:\\d+)/
198
+
199
+ projects.forEach((project) => {
200
+ const proc = spawn({
201
+ cmd: ['bun', 'run', 'dev'],
202
+ cwd: resolve(projectsRoot, project),
203
+ stdout: 'pipe',
204
+ stderr: 'pipe',
205
+ })
206
+
207
+ const decoder = new TextDecoder()
208
+
209
+ proc.stdout.pipeTo(
210
+ new WritableStream({
211
+ write(chunk) {
212
+ const text = decoder.decode(chunk)
213
+ const match = text.match(urlPattern)
214
+ if (match) {
215
+ console.log(\` āœ“ \${project.padEnd(15)} → \${match[1]}\`)
216
+ }
217
+ },
218
+ }),
219
+ )
220
+
221
+ proc.stderr.pipeTo(
222
+ new WritableStream({
223
+ write(chunk) {
224
+ const text = decoder.decode(chunk)
225
+ if (text.includes('error') || text.includes('Error')) {
226
+ console.error(\` āœ— \${project}: \${text.trim()}\`)
227
+ }
228
+ },
229
+ }),
230
+ )
231
+ })
232
+
233
+ console.log('\\nšŸ’” Press Ctrl+C to stop all servers\\n')
234
+
235
+ process.on('SIGINT', () => {
236
+ console.log('\\n\\nšŸ‘‹ Stopping all servers...\\n')
237
+ process.exit()
238
+ })
239
+ `
240
+
241
+ writeFileSync(resolve(scriptsDir, 'dev.ts'), devRunnerContent)
242
+
243
+ console.log(`āœ… Created workspace: ${name}`)
244
+ }
245
+
246
+ /**
247
+ * Create shared package
248
+ */
249
+ function createSharedPackage(root: string): void {
250
+ const sharedDir = resolve(root, 'shared')
251
+ mkdirSync(sharedDir, { recursive: true })
252
+
253
+ // package.json
254
+ const packageJson = {
255
+ name: 'shared',
256
+ version: '1.0.0',
257
+ type: 'module',
258
+ exports: {
259
+ '.': './index.ts',
260
+ './utils': './utils/index.ts',
261
+ './types': './types/index.ts',
262
+ },
263
+ }
264
+
265
+ writeFileSync(
266
+ resolve(sharedDir, 'package.json'),
267
+ `${JSON.stringify(packageJson, null, 2)}\n`,
268
+ )
269
+
270
+ // index.ts
271
+ writeFileSync(
272
+ resolve(sharedDir, 'index.ts'),
273
+ '// Shared utilities and exports\nexport * from \'./utils\'\n',
274
+ )
275
+
276
+ // utils/index.ts
277
+ mkdirSync(resolve(sharedDir, 'utils'), { recursive: true })
278
+ writeFileSync(
279
+ resolve(sharedDir, 'utils', 'index.ts'),
280
+ '// Shared utility functions\nexport function formatDate(date: Date): string {\n return date.toISOString()\n}\n',
281
+ )
282
+
283
+ // types/index.ts
284
+ mkdirSync(resolve(sharedDir, 'types'), { recursive: true })
285
+ writeFileSync(
286
+ resolve(sharedDir, 'types', 'index.ts'),
287
+ '// Shared types\nexport interface User {\n id: string\n name: string\n}\n',
288
+ )
289
+
290
+ console.log('āœ… Created shared package')
291
+ }
292
+
293
+ /**
294
+ * Add a new project to the workspace
295
+ */
296
+ export async function addProject(
297
+ name: string,
298
+ root: string = process.cwd(),
299
+ ): Promise<void> {
300
+ const projectDir = resolve(root, name)
301
+
302
+ if (existsSync(projectDir)) {
303
+ console.error(`āŒ Project ${name} already exists`)
304
+ process.exit(1)
305
+ }
306
+
307
+ console.log(`\nšŸ“¦ Creating project: ${name}\n`)
308
+
309
+ mkdirSync(projectDir, { recursive: true })
310
+
311
+ // Determine if this is part of a workspace
312
+ const isWorkspace = existsSync(resolve(root, 'bgl.config.ts'))
313
+
314
+ // package.json
315
+ const packageJson: Record<string, unknown> = {
316
+ name,
317
+ type: 'module',
318
+ scripts: {
319
+ dev: 'vite',
320
+ build: 'vite build',
321
+ preview: 'vite preview',
322
+ },
323
+ dependencies: {} as Record<string, string>,
324
+ devDependencies: {
325
+ '@vitejs/plugin-vue': 'latest',
326
+ 'vite': 'latest',
327
+ 'vue': 'latest',
328
+ },
329
+ }
330
+
331
+ // Add shared dependency if in workspace
332
+ if (isWorkspace) {
333
+ (packageJson.dependencies as Record<string, string>).shared = 'workspace:*'
334
+ }
335
+
336
+ writeFileSync(
337
+ resolve(projectDir, 'package.json'),
338
+ `${JSON.stringify(packageJson, null, 2)}\n`,
339
+ )
340
+
341
+ // bgl.config.ts
342
+ const bglConfigContent = isWorkspace
343
+ ? `import { defineWorkspace } from '@bagelink/workspace'
344
+ import rootWorkspace from '../bgl.config'
345
+
346
+ export default defineWorkspace({
347
+ localhost: rootWorkspace('localhost'),
348
+ development: rootWorkspace('development'),
349
+ production: rootWorkspace('production'),
350
+ })
351
+ `
352
+ : `import { defineWorkspace } from '@bagelink/workspace'
353
+
354
+ export default defineWorkspace({
355
+ localhost: {
356
+ host: 'http://localhost:8000',
357
+ proxy: '/api',
358
+ },
359
+ development: {
360
+ host: 'https://my-project.bagel.to',
361
+ proxy: '/api',
362
+ },
363
+ production: {
364
+ host: 'https://my-project.bagel.to',
365
+ proxy: '/api',
366
+ },
367
+ })
368
+ `
369
+
370
+ writeFileSync(resolve(projectDir, 'bgl.config.ts'), bglConfigContent)
371
+
372
+ // vite.config.ts
373
+ const viteConfig = `import { defineConfig } from 'vite'
374
+ import vue from '@vitejs/plugin-vue'
375
+ import { createViteProxy } from '@bagelink/workspace'
376
+ import workspace from './bgl.config'
377
+
378
+ export default defineConfig(({ mode }) => {
379
+ const config = workspace(mode as 'localhost' | 'development' | 'production')
380
+
381
+ return {
382
+ plugins: [vue()],
383
+ server: {
384
+ proxy: createViteProxy(config),
385
+ },
386
+ }
387
+ })
388
+ `
389
+
390
+ writeFileSync(resolve(projectDir, 'vite.config.ts'), viteConfig)
391
+
392
+ // Create basic src structure
393
+ const srcDir = resolve(projectDir, 'src')
394
+ mkdirSync(srcDir, { recursive: true })
395
+
396
+ // index.html
397
+ const indexHtml = `<!DOCTYPE html>
398
+ <html lang="en">
399
+ <head>
400
+ <meta charset="UTF-8">
401
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
402
+ <title>${name}</title>
403
+ </head>
404
+ <body>
405
+ <div id="app"></div>
406
+ <script type="module" src="/src/main.ts"></script>
407
+ </body>
408
+ </html>
409
+ `
410
+
411
+ writeFileSync(resolve(projectDir, 'index.html'), indexHtml)
412
+
413
+ // main.ts
414
+ const mainTs = `import { createApp } from 'vue'
415
+ import App from './App.vue'
416
+
417
+ createApp(App).mount('#app')
418
+ `
419
+
420
+ writeFileSync(resolve(srcDir, 'main.ts'), mainTs)
421
+
422
+ // App.vue
423
+ const appVue = `<script setup lang="ts">
424
+ import { ref } from 'vue'
425
+ ${isWorkspace ? 'import { formatDate } from \'shared/utils\'\n' : ''}
426
+ const count = ref(0)
427
+ </script>
428
+
429
+ <template>
430
+ <div>
431
+ <h1>${name}</h1>
432
+ <button @click="count++">Count: {{ count }}</button>
433
+ ${isWorkspace ? '<p>{{ formatDate(new Date()) }}</p>' : ''}
434
+ </div>
435
+ </template>
436
+ `
437
+
438
+ writeFileSync(resolve(srcDir, 'App.vue'), appVue)
439
+
440
+ console.log(`āœ… Created project: ${name}`)
441
+
442
+ // Update workspace package.json if needed
443
+ if (isWorkspace) {
444
+ updateWorkspaceScripts(root, name)
445
+ }
446
+
447
+ console.log('\nNext steps:')
448
+ console.log(` cd ${name}`)
449
+ console.log(' bun install')
450
+ console.log(' bun run dev')
451
+ console.log('')
452
+ }
453
+
454
+ /**
455
+ * Update workspace root package.json with new project scripts
456
+ */
457
+ function updateWorkspaceScripts(root: string, projectName: string): void {
458
+ const packageJsonPath = resolve(root, 'package.json')
459
+ if (!existsSync(packageJsonPath)) return
460
+
461
+ try {
462
+ const packageJson = JSON.parse(
463
+ readFileSync(packageJsonPath, 'utf-8'),
464
+ )
465
+
466
+ if (!packageJson.scripts) {
467
+ packageJson.scripts = {}
468
+ }
469
+
470
+ // Add project-specific scripts
471
+ packageJson.scripts[`dev:${projectName}`] = `bun --filter ${projectName} dev`
472
+ packageJson.scripts[`build:${projectName}`] = `bun --filter ${projectName} build`
473
+
474
+ writeFileSync(
475
+ packageJsonPath,
476
+ `${JSON.stringify(packageJson, null, 2)}\n`,
477
+ )
478
+
479
+ console.log(`āœ… Added scripts: dev:${projectName}, build:${projectName}`)
480
+ }
481
+ catch (error) {
482
+ console.warn('āš ļø Could not update workspace scripts')
483
+ }
484
+ }
485
+
486
+ /**
487
+ * List all projects in workspace
488
+ */
489
+ export function listProjects(root: string = process.cwd()): string[] {
490
+ try {
491
+ const items = readdirSync(root, { withFileTypes: true })
492
+ return items
493
+ .filter(
494
+ item => item.isDirectory()
495
+ && item.name !== 'node_modules'
496
+ && item.name !== 'shared'
497
+ && item.name !== '.git'
498
+ && !item.name.startsWith('.'),
499
+ )
500
+ .map(item => item.name)
501
+ }
502
+ catch {
503
+ return []
504
+ }
505
+ }
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env bun
2
+ import { spawn } from 'bun'
3
+ import { readdir } from 'fs/promises'
4
+ import { resolve } from 'path'
5
+
6
+ const projectsRoot = process.cwd()
7
+ const projects = (await readdir(projectsRoot, { withFileTypes: true }))
8
+ .filter(
9
+ item =>
10
+ item.isDirectory()
11
+ && item.name !== 'node_modules'
12
+ && item.name !== 'shared'
13
+ && item.name !== '.git'
14
+ && !item.name.startsWith('.'),
15
+ )
16
+ .map(item => item.name)
17
+
18
+ console.log(`\nšŸš€ Starting ${projects.length} project${projects.length > 1 ? 's' : ''}...\n`)
19
+
20
+ const urlPattern = /Local:\s+(http:\/\/localhost:\d+)/
21
+
22
+ projects.forEach((project) => {
23
+ const proc = spawn({
24
+ cmd: ['bun', 'run', 'dev'],
25
+ cwd: resolve(projectsRoot, project),
26
+ stdout: 'pipe',
27
+ stderr: 'pipe',
28
+ })
29
+
30
+ const decoder = new TextDecoder()
31
+
32
+ proc.stdout.pipeTo(
33
+ new WritableStream({
34
+ write(chunk) {
35
+ const text = decoder.decode(chunk)
36
+ const match = text.match(urlPattern)
37
+ if (match) {
38
+ console.log(` āœ“ ${project.padEnd(15)} → ${match[1]}`)
39
+ }
40
+ },
41
+ }),
42
+ )
43
+
44
+ proc.stderr.pipeTo(
45
+ new WritableStream({
46
+ write(chunk) {
47
+ const text = decoder.decode(chunk)
48
+ if (text.includes('error') || text.includes('Error')) {
49
+ console.error(` āœ— ${project}: ${text.trim()}`)
50
+ }
51
+ },
52
+ }),
53
+ )
54
+ })
55
+
56
+ console.log('\nšŸ’” Press Ctrl+C to stop all servers\n')
57
+
58
+ process.on('SIGINT', () => {
59
+ console.log('\n\nšŸ‘‹ Stopping all servers...\n')
60
+ process.exit()
61
+ })