@bagelink/workspace 1.0.0

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/src/config.ts ADDED
@@ -0,0 +1,108 @@
1
+ import type { WorkspaceConfig, WorkspaceEnvironment, WorkspaceOptions } from './types'
2
+ import { existsSync } from 'node:fs'
3
+ import { resolve, join } from 'node:path'
4
+ import process from 'node:process'
5
+ import { generateWorkspaceConfig } from './init'
6
+
7
+ /**
8
+ * Load and resolve bgl.config.ts with cascading support
9
+ * Looks for config files from current directory up to workspace root
10
+ * If no config is found, prompts to create one interactively
11
+ */
12
+ export async function resolveConfig(
13
+ mode: WorkspaceEnvironment = 'development',
14
+ options: WorkspaceOptions = {},
15
+ ): Promise<WorkspaceConfig> {
16
+ const root = options.root ?? process.cwd()
17
+ const configFile = options.configFile ?? 'bgl.config.ts'
18
+
19
+ // Try to load config from current directory
20
+ const localConfigPath = resolve(root, configFile)
21
+ const localConfig = await loadConfig(localConfigPath, mode)
22
+
23
+ if (localConfig) {
24
+ return localConfig
25
+ }
26
+
27
+ // Try parent directories (monorepo support)
28
+ let currentDir = root
29
+ const rootDir = resolve('/')
30
+
31
+ while (currentDir !== rootDir) {
32
+ const parentDir = resolve(currentDir, '..')
33
+ const parentConfigPath = join(parentDir, configFile)
34
+
35
+ if (existsSync(parentConfigPath)) {
36
+ const config = await loadConfig(parentConfigPath, mode)
37
+ if (config) {
38
+ return config
39
+ }
40
+ }
41
+
42
+ currentDir = parentDir
43
+ }
44
+
45
+ // No config found - generate one interactively
46
+ if (options.interactive !== false) {
47
+ await generateWorkspaceConfig(root, configFile)
48
+ // Try loading the newly created config
49
+ const newConfig = await loadConfig(localConfigPath, mode)
50
+ if (newConfig) {
51
+ return newConfig
52
+ }
53
+ }
54
+
55
+ throw new Error(`No bgl.config.ts found in ${root} or parent directories`)
56
+ }
57
+
58
+ /**
59
+ * Load config from a specific file path
60
+ */
61
+ async function loadConfig(
62
+ configPath: string,
63
+ mode: WorkspaceEnvironment
64
+ ): Promise<WorkspaceConfig | null> {
65
+ if (!existsSync(configPath)) {
66
+ return null
67
+ }
68
+
69
+ try {
70
+ // Dynamic import to support ESM
71
+ const module = await import(`file://${configPath}`)
72
+
73
+ // Support both named export and default export
74
+ const configMap = module.default ?? module.configs ?? module.config
75
+
76
+ if (typeof configMap === 'function') {
77
+ return configMap(mode) as WorkspaceConfig
78
+ }
79
+
80
+ if (typeof configMap === 'object' && configMap !== null) {
81
+ // If it's a map of environments
82
+ const modeConfig = configMap[mode]
83
+ if (mode in configMap && modeConfig !== undefined && modeConfig !== null) {
84
+ return modeConfig as WorkspaceConfig
85
+ }
86
+ // If it's already a config object
87
+ return configMap as WorkspaceConfig
88
+ }
89
+
90
+ return null
91
+ } catch (error) {
92
+ console.warn(`Failed to load config from ${configPath}:`, error)
93
+ return null
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Merge two configs, with the second one taking precedence
99
+ */
100
+ export function mergeConfigs(
101
+ base: WorkspaceConfig,
102
+ override: Partial<WorkspaceConfig>
103
+ ): WorkspaceConfig {
104
+ return {
105
+ ...base,
106
+ ...override,
107
+ }
108
+ }
package/src/index.ts ADDED
@@ -0,0 +1,99 @@
1
+ import type {
2
+ WorkspaceConfig,
3
+ WorkspaceEnvironment,
4
+ WorkspaceOptions,
5
+ ProxyConfig,
6
+ } from './types'
7
+ import { resolveConfig, mergeConfigs } from './config'
8
+ import { generateWorkspaceConfig, generateWorkspaceConfigSync } from './init'
9
+ import {
10
+ generateNetlifyConfig,
11
+ generateNetlifyRedirect,
12
+ writeNetlifyConfig,
13
+ setBuildEnvVars,
14
+ } from './netlify'
15
+ import { createViteProxy, createCustomProxy } from './proxy'
16
+
17
+ export type {
18
+ ProxyConfig,
19
+ WorkspaceConfig,
20
+ WorkspaceEnvironment,
21
+ WorkspaceOptions,
22
+ }
23
+
24
+ export {
25
+ createCustomProxy,
26
+ createViteProxy,
27
+ generateNetlifyConfig,
28
+ generateNetlifyRedirect,
29
+ generateWorkspaceConfig,
30
+ generateWorkspaceConfigSync,
31
+ mergeConfigs,
32
+ resolveConfig,
33
+ setBuildEnvVars,
34
+ writeNetlifyConfig,
35
+ }
36
+
37
+ /**
38
+ * Define workspace configuration
39
+ * Simple helper to get config from a config map
40
+ */
41
+ export function defineWorkspace(
42
+ configs: Record<WorkspaceEnvironment, WorkspaceConfig>
43
+ ) {
44
+ return (mode: WorkspaceEnvironment = 'development'): WorkspaceConfig => {
45
+ return configs[mode] || configs.development
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Create a workspace instance for managing project configuration
51
+ * Supports both single project and monorepo setups
52
+ */
53
+ export function createWorkspace(options: WorkspaceOptions = {}) {
54
+ let cachedConfig: WorkspaceConfig | null = null
55
+
56
+ return {
57
+ /**
58
+ * Get resolved config for the specified environment
59
+ */
60
+ async getConfig(mode: WorkspaceEnvironment = 'development'): Promise<WorkspaceConfig> {
61
+ if (!cachedConfig) {
62
+ cachedConfig = await resolveConfig(mode, options)
63
+ }
64
+ return cachedConfig
65
+ },
66
+
67
+ /**
68
+ * Create Vite proxy configuration
69
+ */
70
+ createProxy(config: WorkspaceConfig): ProxyConfig {
71
+ return createViteProxy(config)
72
+ },
73
+
74
+ /**
75
+ * Generate Netlify configuration file
76
+ */
77
+ generateNetlify(
78
+ config: WorkspaceConfig,
79
+ outPath: string = './netlify.toml',
80
+ additionalConfig?: string
81
+ ): void {
82
+ writeNetlifyConfig(config, outPath, additionalConfig)
83
+ },
84
+
85
+ /**
86
+ * Set build environment variables
87
+ */
88
+ setBuildEnv(config: WorkspaceConfig): void {
89
+ setBuildEnvVars(config)
90
+ },
91
+
92
+ /**
93
+ * Clear cached configuration
94
+ */
95
+ clearCache(): void {
96
+ cachedConfig = null
97
+ },
98
+ }
99
+ }
package/src/init.ts ADDED
@@ -0,0 +1,118 @@
1
+ import { writeFileSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+ import process from 'node:process'
4
+ import prompts from 'prompts'
5
+
6
+ /**
7
+ * Generate bgl.config.ts file interactively
8
+ */
9
+ export async function generateWorkspaceConfig(
10
+ root: string = process.cwd(),
11
+ configFile: string = 'bgl.config.ts',
12
+ ): Promise<void> {
13
+ console.log('\n🔧 No bgl.config.ts found. Let\'s create one!\n')
14
+
15
+ const response = await prompts([
16
+ {
17
+ type: 'text',
18
+ name: 'projectId',
19
+ message: 'What is your Bagel project ID?',
20
+ initial: 'my-project',
21
+ validate: (value: string) => value.length > 0 ? true : 'Project ID is required',
22
+ },
23
+ {
24
+ type: 'confirm',
25
+ name: 'useCustomHost',
26
+ message: 'Use custom production host?',
27
+ initial: false,
28
+ },
29
+ {
30
+ type: (prev: boolean) => (prev ? 'text' : null),
31
+ name: 'customHost',
32
+ message: 'Enter production host URL:',
33
+ initial: 'https://api.example.com',
34
+ },
35
+ ])
36
+
37
+ // User cancelled
38
+ if (!response || !response.projectId) {
39
+ console.log('\n❌ Config generation cancelled.\n')
40
+ process.exit(1)
41
+ }
42
+
43
+ const productionHost = response.useCustomHost === true
44
+ ? response.customHost as string
45
+ : `https://${response.projectId}.bagel.to`
46
+
47
+ const configContent = `import { defineWorkspace } from '@bagelink/workspace'
48
+ import type { WorkspaceConfig, WorkspaceEnvironment } from '@bagelink/workspace'
49
+
50
+ const configs: Record<WorkspaceEnvironment, WorkspaceConfig> = {
51
+ local: {
52
+ host: 'http://localhost:8000',
53
+ proxy: '/api',
54
+ openapi_url: 'http://localhost:8000/openapi.json',
55
+ },
56
+ development: {
57
+ host: '${productionHost}',
58
+ proxy: '/api',
59
+ openapi_url: '${productionHost}/openapi.json',
60
+ },
61
+ production: {
62
+ host: '${productionHost}',
63
+ proxy: '/api',
64
+ openapi_url: '${productionHost}/openapi.json',
65
+ },
66
+ }
67
+
68
+ export default defineWorkspace(configs)
69
+ `
70
+
71
+ const configPath = resolve(root, configFile)
72
+ writeFileSync(configPath, configContent, 'utf-8')
73
+
74
+ console.log(`\n✅ Created ${configFile}`)
75
+ console.log(` Production host: ${productionHost}`)
76
+ console.log(` Local dev host: http://localhost:8000\n`)
77
+ console.log('💡 You can edit this file to customize your configuration.\n')
78
+ }
79
+
80
+ /**
81
+ * Generate bgl.config.ts non-interactively
82
+ */
83
+ export function generateWorkspaceConfigSync(
84
+ projectId: string,
85
+ root: string = process.cwd(),
86
+ configFile: string = 'bgl.config.ts',
87
+ customHost?: string,
88
+ ): void {
89
+ const productionHost = customHost ?? `https://${projectId}.bagel.to`
90
+
91
+ const configContent = `import { defineWorkspace } from '@bagelink/workspace'
92
+ import type { WorkspaceConfig, WorkspaceEnvironment } from '@bagelink/workspace'
93
+
94
+ const configs: Record<WorkspaceEnvironment, WorkspaceConfig> = {
95
+ local: {
96
+ host: 'http://localhost:8000',
97
+ proxy: '/api',
98
+ openapi_url: 'http://localhost:8000/openapi.json',
99
+ },
100
+ development: {
101
+ host: '${productionHost}',
102
+ proxy: '/api',
103
+ openapi_url: '${productionHost}/openapi.json',
104
+ },
105
+ production: {
106
+ host: '${productionHost}',
107
+ proxy: '/api',
108
+ openapi_url: '${productionHost}/openapi.json',
109
+ },
110
+ }
111
+
112
+ export default defineWorkspace(configs)
113
+ `
114
+
115
+ const configPath = resolve(root, configFile)
116
+ writeFileSync(configPath, configContent, 'utf-8')
117
+ console.log(`✅ Created ${configPath}`)
118
+ }
package/src/netlify.ts ADDED
@@ -0,0 +1,62 @@
1
+ import type { WorkspaceConfig } from './types'
2
+ import { writeFileSync } from 'node:fs'
3
+ import { resolve } from 'node:path'
4
+ import process from 'node:process'
5
+
6
+ /**
7
+ * Generate netlify.toml redirect configuration
8
+ */
9
+ export function generateNetlifyRedirect(config: WorkspaceConfig): string {
10
+ const redirect = `[[redirects]]
11
+ from = "${config.proxy}/*"
12
+ to = "${config.host}/:splat"
13
+ status = 200
14
+ force = true
15
+ headers = {X-From = "Netlify"}
16
+ `
17
+
18
+ return redirect
19
+ }
20
+
21
+ /**
22
+ * Generate complete netlify.toml file
23
+ */
24
+ export function generateNetlifyConfig(
25
+ config: WorkspaceConfig,
26
+ additionalConfig?: string,
27
+ ): string {
28
+ const redirect = generateNetlifyRedirect(config)
29
+
30
+ if (additionalConfig !== undefined && additionalConfig !== '') {
31
+ return `${redirect}\n${additionalConfig}`
32
+ }
33
+
34
+ return redirect
35
+ }
36
+
37
+ /**
38
+ * Write netlify.toml file to disk
39
+ */
40
+ export function writeNetlifyConfig(
41
+ config: WorkspaceConfig,
42
+ outPath: string = './netlify.toml',
43
+ additionalConfig?: string
44
+ ): void {
45
+ const content = generateNetlifyConfig(config, additionalConfig)
46
+ const resolvedPath = resolve(outPath)
47
+
48
+ writeFileSync(resolvedPath, content, 'utf-8')
49
+ console.log(`✓ Generated netlify.toml at ${resolvedPath}`)
50
+ }
51
+
52
+ /**
53
+ * Set environment variables for build process
54
+ */
55
+ export function setBuildEnvVars(config: WorkspaceConfig): void {
56
+ process.env.BGL_PROXY_PATH = config.proxy
57
+ process.env.BGL_API_HOST = config.host
58
+
59
+ if (config.openapi_url !== undefined && config.openapi_url !== '') {
60
+ process.env.BGL_OPENAPI_URL = config.openapi_url
61
+ }
62
+ }
package/src/proxy.ts ADDED
@@ -0,0 +1,57 @@
1
+ import type { WorkspaceConfig, ProxyConfig } from './types'
2
+
3
+ /**
4
+ * Create Vite proxy configuration from WorkspaceConfig
5
+ */
6
+ export function createViteProxy(config: WorkspaceConfig): ProxyConfig {
7
+ const proxy: ProxyConfig = {}
8
+
9
+ // Main API proxy
10
+ if (config.proxy && config.host) {
11
+ proxy[config.proxy] = {
12
+ target: config.host,
13
+ changeOrigin: true,
14
+ rewrite: (path: string) => path.replace(new RegExp(`^${config.proxy}`), ''),
15
+ secure: true,
16
+ }
17
+ }
18
+
19
+ // Files endpoint proxy (direct, no rewrite)
20
+ if (config.host) {
21
+ proxy['/files'] = {
22
+ target: config.host,
23
+ changeOrigin: true,
24
+ secure: true,
25
+ }
26
+ }
27
+
28
+ return proxy
29
+ }
30
+
31
+ /**
32
+ * Create custom proxy configuration
33
+ */
34
+ export function createCustomProxy(
35
+ paths: string[],
36
+ target: string,
37
+ options: {
38
+ changeOrigin?: boolean
39
+ rewrite?: boolean
40
+ secure?: boolean
41
+ } = {}
42
+ ): ProxyConfig {
43
+ const proxy: ProxyConfig = {}
44
+
45
+ for (const path of paths) {
46
+ proxy[path] = {
47
+ target,
48
+ changeOrigin: options.changeOrigin ?? true,
49
+ secure: options.secure ?? true,
50
+ ...(options.rewrite === true && {
51
+ rewrite: (p: string) => p.replace(new RegExp(`^${path}`), ''),
52
+ }),
53
+ }
54
+ }
55
+
56
+ return proxy
57
+ }
package/src/types.ts ADDED
@@ -0,0 +1,49 @@
1
+ export type WorkspaceEnvironment = 'local' | 'development' | 'production'
2
+
3
+ export interface WorkspaceConfig {
4
+ /**
5
+ * The host URL of the backend API server
6
+ * @example 'http://localhost:8000' | 'https://project.bagel.to'
7
+ */
8
+ host: string
9
+
10
+ /**
11
+ * The proxy path to use for API requests
12
+ * @default '/api'
13
+ */
14
+ proxy: string
15
+
16
+ /**
17
+ * Optional OpenAPI specification URL for SDK generation
18
+ */
19
+ openapi_url?: string
20
+ }
21
+
22
+ export interface WorkspaceOptions {
23
+ /**
24
+ * Root directory of the workspace
25
+ * @default process.cwd()
26
+ */
27
+ root?: string
28
+
29
+ /**
30
+ * Path to the config file relative to root
31
+ * @default 'bgl.config.ts'
32
+ */
33
+ configFile?: string
34
+
35
+ /**
36
+ * Enable interactive config generation if no config is found
37
+ * @default true
38
+ */
39
+ interactive?: boolean
40
+ }
41
+
42
+ export interface ProxyConfig {
43
+ [path: string]: {
44
+ target: string
45
+ changeOrigin: boolean
46
+ rewrite?: (path: string) => string
47
+ secure: boolean
48
+ }
49
+ }
@@ -0,0 +1,6 @@
1
+ [[redirects]]
2
+ from = "{{BGL_PROXY_PATH}}/*"
3
+ to = "{{BGL_API_HOST}}/:splat"
4
+ status = 200
5
+ force = true
6
+ headers = {X-From = "Netlify"}