@vltpkg/vsr 0.0.0-27 → 0.0.0-29

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 (80) hide show
  1. package/DEPLOY.md +163 -0
  2. package/LICENSE +114 -10
  3. package/config.ts +221 -0
  4. package/dist/README.md +1 -1
  5. package/dist/bin/vsr.js +9 -6
  6. package/dist/index.js +4 -6
  7. package/dist/index.js.map +2 -2
  8. package/drizzle.config.js +40 -0
  9. package/info/COMPARISONS.md +37 -0
  10. package/info/CONFIGURATION.md +143 -0
  11. package/info/CONTRIBUTING.md +32 -0
  12. package/info/DATABASE_SETUP.md +108 -0
  13. package/info/GRANULAR_ACCESS_TOKENS.md +160 -0
  14. package/info/PROJECT_STRUCTURE.md +291 -0
  15. package/info/ROADMAP.md +27 -0
  16. package/info/SUPPORT.md +39 -0
  17. package/info/TESTING.md +301 -0
  18. package/info/USER_SUPPORT.md +31 -0
  19. package/package.json +50 -6
  20. package/scripts/build-assets.js +31 -0
  21. package/scripts/build-bin.js +63 -0
  22. package/src/assets/public/images/bg.png +0 -0
  23. package/src/assets/public/images/clients/logo-bun.png +0 -0
  24. package/src/assets/public/images/clients/logo-deno.png +0 -0
  25. package/src/assets/public/images/clients/logo-npm.png +0 -0
  26. package/src/assets/public/images/clients/logo-pnpm.png +0 -0
  27. package/src/assets/public/images/clients/logo-vlt.png +0 -0
  28. package/src/assets/public/images/clients/logo-yarn.png +0 -0
  29. package/src/assets/public/images/favicon/apple-touch-icon.png +0 -0
  30. package/src/assets/public/images/favicon/favicon-96x96.png +0 -0
  31. package/src/assets/public/images/favicon/favicon.ico +0 -0
  32. package/src/assets/public/images/favicon/favicon.svg +3 -0
  33. package/src/assets/public/images/favicon/site.webmanifest +21 -0
  34. package/src/assets/public/images/favicon/web-app-manifest-192x192.png +0 -0
  35. package/src/assets/public/images/favicon/web-app-manifest-512x512.png +0 -0
  36. package/src/assets/public/styles/styles.css +231 -0
  37. package/src/bin/demo/package.json +6 -0
  38. package/src/bin/demo/vlt.json +1 -0
  39. package/src/bin/vsr.ts +496 -0
  40. package/src/db/client.ts +590 -0
  41. package/src/db/migrations/0000_faulty_ricochet.sql +14 -0
  42. package/src/db/migrations/0000_initial.sql +29 -0
  43. package/src/db/migrations/0001_uuid_validation.sql +35 -0
  44. package/src/db/migrations/0001_wealthy_magdalene.sql +7 -0
  45. package/src/db/migrations/drop.sql +3 -0
  46. package/src/db/migrations/meta/0000_snapshot.json +104 -0
  47. package/src/db/migrations/meta/0001_snapshot.json +155 -0
  48. package/src/db/migrations/meta/_journal.json +20 -0
  49. package/src/db/schema.ts +43 -0
  50. package/src/index.ts +434 -0
  51. package/src/middleware/config.ts +79 -0
  52. package/src/middleware/telemetry.ts +43 -0
  53. package/src/queue/index.ts +97 -0
  54. package/src/routes/access.ts +852 -0
  55. package/src/routes/docs.ts +63 -0
  56. package/src/routes/misc.ts +469 -0
  57. package/src/routes/packages.ts +2823 -0
  58. package/src/routes/ping.ts +39 -0
  59. package/src/routes/search.ts +131 -0
  60. package/src/routes/static.ts +74 -0
  61. package/src/routes/tokens.ts +259 -0
  62. package/src/routes/users.ts +68 -0
  63. package/src/utils/auth.ts +202 -0
  64. package/src/utils/cache.ts +587 -0
  65. package/src/utils/config.ts +50 -0
  66. package/src/utils/database.ts +69 -0
  67. package/src/utils/docs.ts +146 -0
  68. package/src/utils/packages.ts +453 -0
  69. package/src/utils/response.ts +125 -0
  70. package/src/utils/routes.ts +64 -0
  71. package/src/utils/spa.ts +52 -0
  72. package/src/utils/tracing.ts +52 -0
  73. package/src/utils/upstream.ts +172 -0
  74. package/tsconfig.json +16 -0
  75. package/tsconfig.worker.json +3 -0
  76. package/typedoc.mjs +2 -0
  77. package/types.ts +598 -0
  78. package/vitest.config.ts +25 -0
  79. package/vlt.json.example +56 -0
  80. package/wrangler.json +65 -0
package/src/bin/vsr.ts ADDED
@@ -0,0 +1,496 @@
1
+ #!/usr/bin/env NODE_OPTIONS=--no-warnings node
2
+ import type { Args } from '../../types.ts'
3
+ import { fileURLToPath } from 'node:url'
4
+ import path from 'node:path'
5
+ import { spawn } from 'node:child_process'
6
+ import { PathScurry } from 'path-scurry'
7
+ import { PackageJson } from '@vltpkg/package-json'
8
+ import { PackageInfoClient } from '@vltpkg/package-info'
9
+ import { createServer } from '@vltpkg/server'
10
+ import { existsSync, readFileSync } from 'node:fs'
11
+ import type { VltServerOptions } from '@vltpkg/server'
12
+ import {
13
+ DAEMON_PORT,
14
+ DAEMON_URL,
15
+ URL,
16
+ WRANGLER_CONFIG,
17
+ } from '../../config.ts'
18
+ import { minArgs } from 'minargs'
19
+ import { load } from '@vltpkg/vlt-json'
20
+ import { createRequire } from 'node:module'
21
+
22
+ const usage = `USAGE:
23
+
24
+ $ vsr [<command>] [<options>]
25
+
26
+ COMMANDS:
27
+
28
+ dev Start development server (default)
29
+ deploy Deploy to Cloudflare Workers
30
+
31
+ OPTIONS: DESCRIPTION:
32
+
33
+ --daemon=<boolean> Run filesystem daemon (default: true)
34
+ --telemetry=<boolean> Run with telemetry reporting (default: true)
35
+ -p, --port=<number> Run on a specific port (default: ${DAEMON_PORT})
36
+ -c, --config=<path> Load configuration from vlt.json file
37
+ -d, --debug Run in debug mode
38
+ -h, --help Print usage information
39
+
40
+ DEPLOY OPTIONS:
41
+
42
+ --env=<string> Environment to deploy to (dev, staging, prod)
43
+ --db-name=<string> Override D1 database name
44
+ --bucket-name=<string> Override R2 bucket name
45
+ --queue-name=<string> Override queue name
46
+ --dry-run Show what would be deployed without deploying
47
+
48
+ EXAMPLES:
49
+
50
+ $ vsr # Run dev server with all defaults
51
+ $ vsr dev --port=3000 --debug # Custom port with debug
52
+ $ vsr deploy # Deploy to default environment
53
+ $ vsr deploy --env=prod # Deploy to production
54
+ $ vsr deploy --dry-run # Preview deployment
55
+ $ vsr --config=/path/to/vlt.json # Use specific config file
56
+ `
57
+
58
+ const defaults: Args = {
59
+ daemon: true,
60
+ telemetry: true,
61
+ debug: false,
62
+ help: false,
63
+ port: WRANGLER_CONFIG.port,
64
+ config: undefined,
65
+ env: undefined,
66
+ 'db-name': undefined,
67
+ 'bucket-name': undefined,
68
+ 'queue-name': undefined,
69
+ 'dry-run': false,
70
+ }
71
+
72
+ const opts = {
73
+ alias: {
74
+ p: 'port',
75
+ c: 'config',
76
+ d: 'debug',
77
+ h: 'help',
78
+ },
79
+ boolean: ['debug', 'help', 'daemon', 'telemetry', 'dry-run'],
80
+ string: [
81
+ 'port',
82
+ 'config',
83
+ 'env',
84
+ 'db-name',
85
+ 'bucket-name',
86
+ 'queue-name',
87
+ ],
88
+ default: defaults,
89
+ positionalValues: true, // Allow space-separated values like -p 3000
90
+ }
91
+
92
+ // parse args
93
+ const { args, positionals } = minArgs(opts)
94
+
95
+ // Extract command (default to 'dev' if not specified)
96
+ const command = positionals[0] || 'dev'
97
+
98
+ // Helper functions to extract values from minArgs array format
99
+ function getBooleanValue(
100
+ arg: string[] | undefined,
101
+ defaultValue: boolean,
102
+ ): boolean {
103
+ if (!arg || arg.length === 0) return defaultValue
104
+ const value = arg[0]
105
+ if (value === '' || value === undefined) return true // flag present without value
106
+ return value !== 'false' && value !== '0'
107
+ }
108
+
109
+ function getStringValue(
110
+ arg: string[] | undefined,
111
+ defaultValue?: string,
112
+ ): string | undefined {
113
+ if (!arg || arg.length === 0) return defaultValue
114
+ return arg[0] || defaultValue
115
+ }
116
+
117
+ function getNumberValue(
118
+ arg: string[] | undefined,
119
+ defaultValue: number,
120
+ ): number {
121
+ if (!arg || arg.length === 0) return defaultValue
122
+ const value = arg[0]
123
+ return Number(value) || defaultValue
124
+ }
125
+
126
+ // Type definition for VSR config in vlt.json
127
+ interface VsrConfig {
128
+ registry?: {
129
+ daemon?: boolean
130
+ telemetry?: boolean
131
+ debug?: boolean
132
+ port?: number
133
+ deploy?: {
134
+ environments?: {
135
+ [key: string]: DeployEnvironment
136
+ }
137
+ sentry?: {
138
+ dsn?: string
139
+ environment?: string
140
+ sampleRate?: number
141
+ tracesSampleRate?: number
142
+ }
143
+ }
144
+ }
145
+ daemon?: boolean
146
+ telemetry?: boolean
147
+ debug?: boolean
148
+ port?: number
149
+ }
150
+
151
+ interface DeployEnvironment {
152
+ databaseName?: string
153
+ bucketName?: string
154
+ queueName?: string
155
+ vars?: Record<string, any>
156
+ sentry?: {
157
+ dsn?: string
158
+ environment?: string
159
+ sampleRate?: number
160
+ tracesSampleRate?: number
161
+ }
162
+ }
163
+
164
+ // Load configuration from vlt.json
165
+ function loadVltConfig(configPath?: string): VsrConfig {
166
+ try {
167
+ const isVsrConfig = (
168
+ x: unknown,
169
+ _file: string,
170
+ ): asserts x is VsrConfig => {
171
+ if (x === null || typeof x !== 'object') {
172
+ throw new Error('Config must be an object')
173
+ }
174
+ }
175
+
176
+ let configData: VsrConfig | undefined
177
+
178
+ if (configPath) {
179
+ // Custom config path specified
180
+ if (!existsSync(configPath)) {
181
+ // eslint-disable-next-line no-console
182
+ console.warn(`Config file not found: ${configPath}`)
183
+ return {}
184
+ }
185
+
186
+ // For custom paths, we need to read the file manually
187
+ const configContent = readFileSync(configPath, 'utf8')
188
+ try {
189
+ configData = JSON.parse(configContent) as VsrConfig
190
+ } catch (_err) {
191
+ // eslint-disable-next-line no-console
192
+ console.warn(`Failed to parse config file ${configPath}`)
193
+ return {}
194
+ }
195
+ } else {
196
+ // Use vlt-json to find and load project config
197
+ try {
198
+ configData = load('config', isVsrConfig)
199
+ } catch (_err) {
200
+ configData = undefined
201
+ }
202
+ }
203
+
204
+ return configData ?? {}
205
+ } catch (_err) {
206
+ // Can't access DEBUG here since it hasn't been defined yet
207
+ return {}
208
+ }
209
+ }
210
+
211
+ // Get the config path from CLI args
212
+ const configPath = getStringValue(args.config)
213
+
214
+ // Load config and merge with CLI args (CLI args take precedence)
215
+ const vltConfig = loadVltConfig(configPath)
216
+ const registryConfig = vltConfig.registry ?? {}
217
+
218
+ // Extract parsed args with config fallbacks, CLI args take precedence
219
+ const PORT = getNumberValue(
220
+ args.port,
221
+ registryConfig.port ?? vltConfig.port ?? defaults.port,
222
+ )
223
+ const TELEMETRY = getBooleanValue(
224
+ args.telemetry,
225
+ registryConfig.telemetry ??
226
+ vltConfig.telemetry ??
227
+ defaults.telemetry,
228
+ )
229
+ const DAEMON = getBooleanValue(
230
+ args.daemon,
231
+ registryConfig.daemon ?? vltConfig.daemon ?? defaults.daemon,
232
+ )
233
+ const DEBUG = getBooleanValue(
234
+ args.debug,
235
+ registryConfig.debug ?? vltConfig.debug ?? defaults.debug,
236
+ )
237
+
238
+ // Deploy-specific configuration
239
+ const ENV = getStringValue(args.env, 'dev')
240
+ const DRY_RUN = getBooleanValue(
241
+ args['dry-run'],
242
+ defaults['dry-run'] ?? false,
243
+ )
244
+ const DB_NAME_OVERRIDE = getStringValue(args['db-name'])
245
+ const BUCKET_NAME_OVERRIDE = getStringValue(args['bucket-name'])
246
+ const QUEUE_NAME_OVERRIDE = getStringValue(args['queue-name'])
247
+
248
+ // Print usage information
249
+ function printUsage(): void {
250
+ // eslint-disable-next-line no-console
251
+ console.log(usage)
252
+ }
253
+
254
+ // Deploy function
255
+ async function deployToCloudflare(): Promise<void> {
256
+ const deployConfig = registryConfig.deploy
257
+ const envConfig = deployConfig?.environments?.[ENV ?? 'dev'] ?? {}
258
+
259
+ // Determine resource names with precedence: CLI args > env config > defaults
260
+ const databaseName =
261
+ DB_NAME_OVERRIDE ?? envConfig.databaseName ?? 'vsr-database'
262
+ const bucketName =
263
+ BUCKET_NAME_OVERRIDE ?? envConfig.bucketName ?? 'vsr-bucket'
264
+ const queueName =
265
+ QUEUE_NAME_OVERRIDE ??
266
+ envConfig.queueName ??
267
+ 'cache-refresh-queue'
268
+
269
+ // Build Sentry configuration
270
+ const sentryConfig = envConfig.sentry ?? deployConfig?.sentry ?? {}
271
+ const sentryDsn =
272
+ sentryConfig.dsn ??
273
+ 'https://909b085eb764c00250ad312660c2fdf1@o4506397716054016.ingest.us.sentry.io/4509492612300800'
274
+ const sentryEnv = sentryConfig.environment ?? ENV
275
+
276
+ // Build wrangler deploy arguments
277
+ const wranglerArgs = [
278
+ 'deploy',
279
+ indexPath,
280
+ '--config',
281
+ wranglerConfigPath,
282
+ '--name',
283
+ `vsr-${ENV}`,
284
+ '--compatibility-date',
285
+ '2024-09-23',
286
+ '--var',
287
+ `SENTRY_DSN:${sentryDsn}`,
288
+ '--var',
289
+ `SENTRY_ENVIRONMENT:${sentryEnv}`,
290
+ '--var',
291
+ `ARG_DEBUG:${DEBUG}`,
292
+ '--var',
293
+ `ARG_TELEMETRY:${TELEMETRY}`,
294
+ '--var',
295
+ `ARG_DAEMON:${DAEMON}`,
296
+ ]
297
+
298
+ // Add D1 database binding
299
+ wranglerArgs.push('--d1', `DB=${databaseName}`)
300
+
301
+ // Add R2 bucket binding
302
+ wranglerArgs.push('--r2', `BUCKET=${bucketName}`)
303
+
304
+ // Add queue bindings
305
+ wranglerArgs.push(
306
+ '--queue-producer',
307
+ `CACHE_REFRESH_QUEUE=${queueName}`,
308
+ )
309
+ wranglerArgs.push('--queue-consumer', queueName)
310
+
311
+ // Add custom environment variables from config
312
+ if (envConfig.vars) {
313
+ for (const [key, value] of Object.entries(envConfig.vars)) {
314
+ wranglerArgs.push('--var', `${key}:${value}`)
315
+ }
316
+ }
317
+
318
+ if (DRY_RUN) {
319
+ wranglerArgs.push('--dry-run')
320
+ // eslint-disable-next-line no-console
321
+ console.log(
322
+ '🔍 Dry run - would execute:',
323
+ wranglerBin,
324
+ wranglerArgs.join(' '),
325
+ )
326
+ // eslint-disable-next-line no-console
327
+ console.log('\n📋 Deployment configuration:')
328
+ // eslint-disable-next-line no-console
329
+ console.log(` Environment: ${ENV}`)
330
+ // eslint-disable-next-line no-console
331
+ console.log(` Database: ${databaseName}`)
332
+ // eslint-disable-next-line no-console
333
+ console.log(` Bucket: ${bucketName}`)
334
+ // eslint-disable-next-line no-console
335
+ console.log(` Queue: ${queueName}`)
336
+ // eslint-disable-next-line no-console
337
+ console.log(` Sentry DSN: ${sentryDsn}`)
338
+ // eslint-disable-next-line no-console
339
+ console.log(` Sentry Environment: ${sentryEnv}`)
340
+ return
341
+ }
342
+
343
+ // eslint-disable-next-line no-console
344
+ console.log(`🚀 Deploying VSR to ${ENV} environment...`)
345
+
346
+ return new Promise((resolve, reject) => {
347
+ const deployProcess = spawn(wranglerBin, wranglerArgs, {
348
+ cwd: registryRoot,
349
+ stdio: 'inherit',
350
+ })
351
+
352
+ deployProcess.on('close', code => {
353
+ if (code === 0) {
354
+ // eslint-disable-next-line no-console
355
+ console.log(`✅ Successfully deployed VSR to ${ENV}`)
356
+ resolve()
357
+ } else {
358
+ reject(new Error(`Deployment failed with exit code ${code}`))
359
+ }
360
+ })
361
+
362
+ deployProcess.on('error', error => {
363
+ reject(
364
+ new Error(`Failed to start deployment: ${error.message}`),
365
+ )
366
+ })
367
+ })
368
+ }
369
+
370
+ // Check if the help flag was passed
371
+ if (args.help) {
372
+ printUsage()
373
+ process.exit(0)
374
+ }
375
+
376
+ // Validate command
377
+ if (!['dev', 'deploy'].includes(command)) {
378
+ // eslint-disable-next-line no-console
379
+ console.error(`❌ Unknown command: ${command}`)
380
+ printUsage()
381
+ process.exit(1)
382
+ }
383
+
384
+ // TODO: Remove the demo/dummy project once server doesn't need one
385
+ const __filename = fileURLToPath(import.meta.url)
386
+ const __dirname = path.dirname(__filename)
387
+ const demo = path.resolve(__dirname, './demo')
388
+ const require = createRequire(import.meta.url)
389
+
390
+ // Resolve paths relative to this script's location
391
+ const registryRoot = path.resolve(__dirname, '../../')
392
+ const indexPath = path.resolve(registryRoot, 'dist/index.js')
393
+ const wranglerConfigPath = path.resolve(registryRoot, 'wrangler.json')
394
+
395
+ // Find the wrangler binary from node_modules
396
+ const wranglerPkgPath = require.resolve('wrangler/package.json')
397
+ const wranglerRelBinPath = (
398
+ JSON.parse(readFileSync(wranglerPkgPath, 'utf8')) as {
399
+ bin: { wrangler: string }
400
+ }
401
+ ).bin.wrangler
402
+ const wranglerBinPath = require.resolve(
403
+ path.join('wrangler', wranglerRelBinPath),
404
+ )
405
+ const wranglerBin =
406
+ existsSync(wranglerBinPath) ? wranglerBinPath : 'wrangler'
407
+
408
+ const serverOptions = {
409
+ scurry: new PathScurry(demo),
410
+ projectRoot: demo,
411
+ packageJson: new PackageJson(),
412
+ catalog: {},
413
+ catalogs: {},
414
+ registry: 'https://registry.npmjs.org/',
415
+ registries: {},
416
+ 'scope-registries': {},
417
+ 'git-hosts': {},
418
+ 'git-host-archives': {},
419
+ } as VltServerOptions
420
+
421
+ void (async () => {
422
+ try {
423
+ if (command === 'deploy') {
424
+ await deployToCloudflare()
425
+ return
426
+ }
427
+
428
+ // Default 'dev' command
429
+ if (DAEMON) {
430
+ // Save original process.argv to restore later
431
+ const originalArgv = process.argv.slice()
432
+
433
+ // Temporarily modify process.argv to remove VSR-specific flags that daemon doesn't understand
434
+ // Keep only the basic node command and script path
435
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
436
+ process.argv = [process.argv[0]!, process.argv[1]!]
437
+
438
+ try {
439
+ const server = createServer({
440
+ ...serverOptions,
441
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
442
+ packageInfo: new PackageInfoClient() as any,
443
+ })
444
+
445
+ await server.start({
446
+ port: DAEMON_PORT,
447
+ })
448
+
449
+ // eslint-disable-next-line no-console
450
+ console.log(`Daemon: ${DAEMON_URL}`)
451
+ } finally {
452
+ // Restore original process.argv
453
+ process.argv = originalArgv
454
+ }
455
+ }
456
+
457
+ // eslint-disable-next-line no-console
458
+ console.log(`VSR: ${URL}`)
459
+
460
+ // spawn the wrangler dev process with resolved paths
461
+ const { stdout, stderr } = spawn(
462
+ wranglerBin,
463
+ [
464
+ 'dev',
465
+ indexPath,
466
+ '--config',
467
+ wranglerConfigPath,
468
+ '--local',
469
+ '--persist-to=local-store',
470
+ '--no-remote',
471
+ `--port=${PORT}`,
472
+ `--var=ARG_DEBUG:${DEBUG}`,
473
+ `--var=ARG_TELEMETRY:${TELEMETRY}`,
474
+ `--var=ARG_DAEMON:${DAEMON}`,
475
+ ],
476
+ {
477
+ cwd: registryRoot, // Set working directory to registry root
478
+ },
479
+ )
480
+
481
+ if (DEBUG) {
482
+ stdout.on('data', (data: Buffer) => {
483
+ // eslint-disable-next-line no-console
484
+ console.log(data.toString())
485
+ })
486
+ stderr.on('data', (data: Buffer) => {
487
+ // eslint-disable-next-line no-console
488
+ console.error(data.toString())
489
+ })
490
+ }
491
+ } catch (error: unknown) {
492
+ // eslint-disable-next-line no-console
493
+ console.error('Failed to start:', error)
494
+ process.exit(1)
495
+ }
496
+ })()