1
1
import fs from "node:fs/promises" ;
2
2
import os from "node:os" ;
3
3
import path from "node:path" ;
4
- import { isEnoent } from "./error.js" ;
5
- import type { Logger } from "./logger.js" ;
6
- import { commandRequiresAuthenticationMessage } from "./observableApiAuth.js" ;
4
+ import { commandRequiresAuthenticationMessage } from "./commandInstruction.js" ;
5
+ import { CliError , isEnoent } from "./error.js" ;
6
+
7
+ export interface ConfigEffects {
8
+ readFile : ( path : string , encoding : "utf8" ) => Promise < string > ;
9
+ writeFile : ( path : string , contents : string ) => Promise < void > ;
10
+ env : typeof process . env ;
11
+ cwd : typeof process . cwd ;
12
+ mkdir : ( path : string , options ?: { recursive ?: boolean } ) => Promise < void > ;
13
+ homedir : typeof os . homedir ;
14
+ }
15
+
16
+ export const defaultEffects : ConfigEffects = {
17
+ readFile : ( path , encoding ) => fs . readFile ( path , encoding ) ,
18
+ writeFile : fs . writeFile ,
19
+ mkdir : async ( path , options ) => {
20
+ await fs . mkdir ( path , options ) ;
21
+ } ,
22
+ env : process . env ,
23
+ cwd : process . cwd ,
24
+ homedir : os . homedir
25
+ } ;
7
26
8
27
const userConfigName = ".observablehq" ;
9
28
interface UserConfig {
@@ -22,17 +41,16 @@ export type ApiKey =
22
41
| { source : "env" ; envVar : string ; key : string }
23
42
| { source : "test" ; key : string } ;
24
43
25
- export async function getObservableApiKey ( logger : Logger = console ) : Promise < ApiKey > {
44
+ export async function getObservableApiKey ( effects : ConfigEffects = defaultEffects ) : Promise < ApiKey > {
26
45
const envVar = "OBSERVABLE_TOKEN" ;
27
- if ( process . env [ envVar ] ) {
28
- return { source : "env" , envVar, key : process . env [ envVar ] } ;
46
+ if ( effects . env [ envVar ] ) {
47
+ return { source : "env" , envVar, key : effects . env [ envVar ] } ;
29
48
}
30
49
const { config, configPath} = await loadUserConfig ( ) ;
31
50
if ( config . auth ?. key ) {
32
51
return { source : "file" , filePath : configPath , key : config . auth . key } ;
33
52
}
34
- logger . log ( commandRequiresAuthenticationMessage ) ;
35
- process . exit ( 1 ) ;
53
+ throw new CliError ( commandRequiresAuthenticationMessage ) ;
36
54
}
37
55
38
56
export async function setObservableApiKey ( info : null | { id : string ; key : string } ) : Promise < void > {
@@ -45,11 +63,14 @@ export async function setObservableApiKey(info: null | {id: string; key: string}
45
63
await writeUserConfig ( { config, configPath} ) ;
46
64
}
47
65
48
- export async function getDeployConfig ( sourceRoot : string ) : Promise < DeployConfig | null > {
49
- const deployConfigPath = path . join ( process . cwd ( ) , sourceRoot , ".observablehq" , "deploy.json" ) ;
66
+ export async function getDeployConfig (
67
+ sourceRoot : string ,
68
+ effects : ConfigEffects = defaultEffects
69
+ ) : Promise < DeployConfig | null > {
70
+ const deployConfigPath = path . join ( effects . cwd ( ) , sourceRoot , ".observablehq" , "deploy.json" ) ;
50
71
let content : string | null = null ;
51
72
try {
52
- content = await fs . readFile ( deployConfigPath , "utf8" ) ;
73
+ content = await effects . readFile ( deployConfigPath , "utf8" ) ;
53
74
} catch ( error ) {
54
75
content = "{}" ;
55
76
}
@@ -59,41 +80,58 @@ export async function getDeployConfig(sourceRoot: string): Promise<DeployConfig
59
80
return { projectId} ;
60
81
}
61
82
62
- export async function setDeployConfig ( sourceRoot : string , newConfig : DeployConfig ) : Promise < void > {
63
- const dir = path . join ( process . cwd ( ) , sourceRoot , ".observablehq" ) ;
83
+ export async function setDeployConfig (
84
+ sourceRoot : string ,
85
+ newConfig : DeployConfig ,
86
+ effects : ConfigEffects = defaultEffects
87
+ ) : Promise < void > {
88
+ const dir = path . join ( effects . cwd ( ) , sourceRoot , ".observablehq" ) ;
64
89
const deployConfigPath = path . join ( dir , "deploy.json" ) ;
65
90
const oldConfig = ( await getDeployConfig ( sourceRoot ) ) || { } ;
66
91
const merged = { ...oldConfig , ...newConfig } ;
67
- await fs . mkdir ( dir , { recursive : true } ) ;
68
- await fs . writeFile ( deployConfigPath , JSON . stringify ( merged , null , 2 ) + "\n" ) ;
92
+ await effects . mkdir ( dir , { recursive : true } ) ;
93
+ await effects . writeFile ( deployConfigPath , JSON . stringify ( merged , null , 2 ) + "\n" ) ;
69
94
}
70
95
71
- async function loadUserConfig ( ) : Promise < { configPath : string ; config : UserConfig } > {
72
- let cursor = path . resolve ( process . cwd ( ) ) ;
73
- while ( true ) {
74
- const configPath = path . join ( cursor , userConfigName ) ;
96
+ export async function loadUserConfig (
97
+ effects : ConfigEffects = defaultEffects
98
+ ) : Promise < { configPath : string ; config : UserConfig } > {
99
+ const homeConfigPath = path . join ( effects . homedir ( ) , userConfigName ) ;
100
+
101
+ function * pathsToTry ( ) : Generator < string > {
102
+ let cursor = path . resolve ( effects . cwd ( ) ) ;
103
+ while ( true ) {
104
+ yield path . join ( cursor , userConfigName ) ;
105
+ const nextCursor = path . dirname ( cursor ) ;
106
+ if ( nextCursor === cursor ) break ;
107
+ cursor = nextCursor ;
108
+ }
109
+ yield homeConfigPath ;
110
+ }
111
+
112
+ for ( const configPath of pathsToTry ( ) ) {
75
113
let content : string | null = null ;
76
114
try {
77
- content = await fs . readFile ( configPath , "utf8" ) ;
115
+ content = await effects . readFile ( configPath , "utf8" ) ;
78
116
} catch ( error ) {
79
117
if ( ! isEnoent ( error ) ) throw error ;
80
- const nextCursor = path . dirname ( cursor ) ;
81
- if ( nextCursor === cursor ) break ;
82
- cursor = nextCursor ;
83
118
}
84
119
85
120
if ( content !== null ) {
86
121
try {
87
122
return { config : JSON . parse ( content ) , configPath} ;
88
- } catch ( err ) {
89
- console . error ( `Problem parsing config file at ${ configPath } : ${ err } ` ) ;
123
+ } catch ( error ) {
124
+ throw new CliError ( `Problem parsing config file at ${ configPath } : ${ error } ` , { cause : error } ) ;
90
125
}
91
126
}
92
127
}
93
128
94
- return { config : { } , configPath : path . join ( os . homedir ( ) , userConfigName ) } ;
129
+ return { config : { } , configPath : homeConfigPath } ;
95
130
}
96
131
97
- async function writeUserConfig ( { configPath, config} : { configPath : string ; config : UserConfig } ) : Promise < void > {
98
- await fs . writeFile ( configPath , JSON . stringify ( config , null , 2 ) ) ;
132
+ async function writeUserConfig (
133
+ { configPath, config} : { configPath : string ; config : UserConfig } ,
134
+ effects : ConfigEffects = defaultEffects
135
+ ) : Promise < void > {
136
+ await effects . writeFile ( configPath , JSON . stringify ( config , null , 2 ) ) ;
99
137
}
0 commit comments