netlify-cli 12.6.0 → 12.7.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/README.md +5 -0
- package/npm-shrinkwrap.json +169 -54
- package/package.json +3 -2
- package/src/commands/base-command.mjs +16 -5
- package/src/commands/dev/dev.mjs +27 -524
- package/src/commands/functions/functions-serve.mjs +5 -1
- package/src/commands/main.mjs +2 -0
- package/src/commands/serve/serve.mjs +189 -0
- package/src/functions-templates/javascript/oauth-passport/package.json +1 -1
- package/src/lib/edge-functions/registry.mjs +2 -5
- package/src/lib/functions/registry.mjs +55 -33
- package/src/lib/functions/runtimes/js/builders/zisi.mjs +2 -1
- package/src/lib/functions/runtimes/rust/index.mjs +2 -1
- package/src/lib/functions/server.mjs +35 -18
- package/src/utils/banner.mjs +17 -0
- package/src/utils/detect-server-settings.mjs +35 -1
- package/src/utils/dev.mjs +8 -5
- package/src/utils/framework-server.mjs +66 -0
- package/src/utils/functions/functions.mjs +18 -2
- package/src/utils/graph.mjs +170 -0
- package/src/utils/proxy-server.mjs +90 -0
- package/src/utils/run-build.mjs +129 -0
- package/src/utils/shell.mjs +120 -0
- package/src/utils/static-server.mjs +34 -0
- package/src/utils/validation.mjs +15 -0
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
+
import { promises as fs } from 'fs'
|
|
2
3
|
import { resolve } from 'path'
|
|
3
4
|
|
|
4
5
|
import { isDirectoryAsync, isFileAsync } from '../../lib/fs.mjs'
|
|
5
6
|
import { getPathInProject } from '../../lib/settings.mjs'
|
|
6
7
|
|
|
8
|
+
export const INTERNAL_FUNCTIONS_FOLDER = 'functions-internal'
|
|
9
|
+
export const SERVE_FUNCTIONS_FOLDER = 'functions-serve'
|
|
10
|
+
|
|
7
11
|
/**
|
|
8
12
|
* retrieves the function directory out of the flags or config
|
|
9
13
|
* @param {object} param
|
|
@@ -26,8 +30,20 @@ export const getFunctionsManifestPath = async ({ base }) => {
|
|
|
26
30
|
return isFile ? path : null
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
export const
|
|
30
|
-
const path = resolve(base, getPathInProject(['functions
|
|
33
|
+
export const getFunctionsDistPath = async ({ base }) => {
|
|
34
|
+
const path = resolve(base, getPathInProject(['functions']))
|
|
35
|
+
const isDirectory = await isDirectoryAsync(path)
|
|
36
|
+
|
|
37
|
+
return isDirectory ? path : null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const getInternalFunctionsDir = async ({ base, ensureExists }) => {
|
|
41
|
+
const path = resolve(base, getPathInProject([INTERNAL_FUNCTIONS_FOLDER]))
|
|
42
|
+
|
|
43
|
+
if (ensureExists) {
|
|
44
|
+
await fs.mkdir(path, { recursive: true })
|
|
45
|
+
}
|
|
46
|
+
|
|
31
47
|
const isDirectory = await isDirectoryAsync(path)
|
|
32
48
|
|
|
33
49
|
return isDirectory ? path : null
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import events from 'events'
|
|
3
|
+
import process from 'process'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
OneGraphCliClient,
|
|
7
|
+
loadCLISession,
|
|
8
|
+
markCliSessionInactive,
|
|
9
|
+
persistNewOperationsDocForSession,
|
|
10
|
+
startOneGraphCLISession,
|
|
11
|
+
} from '../lib/one-graph/cli-client.mjs'
|
|
12
|
+
import {
|
|
13
|
+
defaultExampleOperationsDoc,
|
|
14
|
+
getGraphEditUrlBySiteId,
|
|
15
|
+
getNetlifyGraphConfig,
|
|
16
|
+
readGraphQLOperationsSourceFile,
|
|
17
|
+
} from '../lib/one-graph/cli-netlify-graph.mjs'
|
|
18
|
+
|
|
19
|
+
import { chalk, error, getToken, log, normalizeConfig, warn, watchDebounced } from './command-helpers.mjs'
|
|
20
|
+
import { generateNetlifyGraphJWT, processOnExit } from './dev.mjs'
|
|
21
|
+
import { addCleanupJob } from './shell.mjs'
|
|
22
|
+
|
|
23
|
+
export const startPollingForAPIAuthentication = async function (options) {
|
|
24
|
+
const { api, command, config, site, siteInfo } = options
|
|
25
|
+
const frequency = 5000
|
|
26
|
+
|
|
27
|
+
const helper = async (maybeSiteData) => {
|
|
28
|
+
const siteData = await (maybeSiteData || api.getSite({ siteId: site.id }))
|
|
29
|
+
const authlifyTokenId = siteData && siteData.authlify_token_id
|
|
30
|
+
|
|
31
|
+
const existingAuthlifyTokenId = config && config.netlifyGraphConfig && config.netlifyGraphConfig.authlifyTokenId
|
|
32
|
+
if (authlifyTokenId && authlifyTokenId !== existingAuthlifyTokenId) {
|
|
33
|
+
const netlifyToken = await command.authenticate()
|
|
34
|
+
// Only inject the authlify config if a token ID exists. This prevents
|
|
35
|
+
// calling command.authenticate() (which opens a browser window) if the
|
|
36
|
+
// user hasn't enabled API Authentication
|
|
37
|
+
const netlifyGraphConfig = {
|
|
38
|
+
netlifyToken,
|
|
39
|
+
authlifyTokenId: siteData.authlify_token_id,
|
|
40
|
+
siteId: site.id,
|
|
41
|
+
}
|
|
42
|
+
config.netlifyGraphConfig = netlifyGraphConfig
|
|
43
|
+
|
|
44
|
+
const netlifyGraphJWT = generateNetlifyGraphJWT(netlifyGraphConfig)
|
|
45
|
+
|
|
46
|
+
if (netlifyGraphJWT != null) {
|
|
47
|
+
// XXX(anmonteiro): this name is deprecated. Delete after 3/31/2022
|
|
48
|
+
process.env.ONEGRAPH_AUTHLIFY_TOKEN = netlifyGraphJWT
|
|
49
|
+
process.env.NETLIFY_GRAPH_TOKEN = netlifyGraphJWT
|
|
50
|
+
}
|
|
51
|
+
} else if (!authlifyTokenId) {
|
|
52
|
+
// If there's no `authlifyTokenId`, it's because the user disabled API
|
|
53
|
+
// Auth. Delete the config in this case.
|
|
54
|
+
delete config.netlifyGraphConfig
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setTimeout(helper, frequency)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await helper(siteInfo)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const startNetlifyGraph = async ({
|
|
64
|
+
command,
|
|
65
|
+
config,
|
|
66
|
+
options,
|
|
67
|
+
settings,
|
|
68
|
+
site,
|
|
69
|
+
startNetlifyGraphWatcher,
|
|
70
|
+
state,
|
|
71
|
+
}) => {
|
|
72
|
+
if (startNetlifyGraphWatcher && options.offline) {
|
|
73
|
+
warn(`Unable to start Netlify Graph in offline mode`)
|
|
74
|
+
} else if (startNetlifyGraphWatcher && !site.id) {
|
|
75
|
+
error(
|
|
76
|
+
`No siteId defined, unable to start Netlify Graph. To enable, run ${chalk.yellow(
|
|
77
|
+
'netlify init',
|
|
78
|
+
)} or ${chalk.yellow('netlify link')}.`,
|
|
79
|
+
)
|
|
80
|
+
} else if (startNetlifyGraphWatcher) {
|
|
81
|
+
const netlifyToken = await command.authenticate()
|
|
82
|
+
await OneGraphCliClient.ensureAppForSite(netlifyToken, site.id)
|
|
83
|
+
|
|
84
|
+
let stopWatchingCLISessions
|
|
85
|
+
|
|
86
|
+
let liveConfig = { ...config }
|
|
87
|
+
let isRestartingSession = false
|
|
88
|
+
|
|
89
|
+
const createOrResumeSession = async function () {
|
|
90
|
+
const netlifyGraphConfig = await getNetlifyGraphConfig({ command, options, settings })
|
|
91
|
+
|
|
92
|
+
let graphqlDocument = readGraphQLOperationsSourceFile(netlifyGraphConfig)
|
|
93
|
+
|
|
94
|
+
if (!graphqlDocument || graphqlDocument.trim().length === 0) {
|
|
95
|
+
graphqlDocument = defaultExampleOperationsDoc
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
stopWatchingCLISessions = await startOneGraphCLISession({
|
|
99
|
+
config: liveConfig,
|
|
100
|
+
netlifyGraphConfig,
|
|
101
|
+
netlifyToken,
|
|
102
|
+
site,
|
|
103
|
+
state,
|
|
104
|
+
oneGraphSessionId: options.sessionId,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// Should be created by startOneGraphCLISession
|
|
108
|
+
const oneGraphSessionId = loadCLISession(state)
|
|
109
|
+
|
|
110
|
+
await persistNewOperationsDocForSession({
|
|
111
|
+
config: liveConfig,
|
|
112
|
+
netlifyGraphConfig,
|
|
113
|
+
netlifyToken,
|
|
114
|
+
oneGraphSessionId,
|
|
115
|
+
operationsDoc: graphqlDocument,
|
|
116
|
+
siteId: site.id,
|
|
117
|
+
siteRoot: site.root,
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
return oneGraphSessionId
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const configWatcher = new events.EventEmitter()
|
|
124
|
+
|
|
125
|
+
// Only set up a watcher if we know the config path.
|
|
126
|
+
const { configPath } = command.netlify.site
|
|
127
|
+
if (configPath) {
|
|
128
|
+
// chokidar handle
|
|
129
|
+
command.configWatcherHandle = await watchDebounced(configPath, {
|
|
130
|
+
depth: 1,
|
|
131
|
+
onChange: async () => {
|
|
132
|
+
const cwd = options.cwd || process.cwd()
|
|
133
|
+
const [token] = await getToken(options.auth)
|
|
134
|
+
const { config: newConfig } = await command.getConfig({ cwd, state, token, ...command.netlify.apiUrlOpts })
|
|
135
|
+
|
|
136
|
+
const normalizedNewConfig = normalizeConfig(newConfig)
|
|
137
|
+
configWatcher.emit('change', normalizedNewConfig)
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
processOnExit(async () => {
|
|
142
|
+
await command.configWatcherHandle.close()
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Set up a handler for config changes.
|
|
147
|
+
configWatcher.on('change', async (newConfig) => {
|
|
148
|
+
command.netlify.config = newConfig
|
|
149
|
+
liveConfig = newConfig
|
|
150
|
+
if (isRestartingSession) {
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
stopWatchingCLISessions && stopWatchingCLISessions()
|
|
154
|
+
isRestartingSession = true
|
|
155
|
+
await createOrResumeSession()
|
|
156
|
+
isRestartingSession = false
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
const oneGraphSessionId = await createOrResumeSession()
|
|
160
|
+
const cleanupSession = () => markCliSessionInactive({ netlifyToken, sessionId: oneGraphSessionId, siteId: site.id })
|
|
161
|
+
|
|
162
|
+
addCleanupJob(cleanupSession)
|
|
163
|
+
|
|
164
|
+
const graphEditUrl = getGraphEditUrlBySiteId({ siteId: site.id, oneGraphSessionId })
|
|
165
|
+
|
|
166
|
+
log(
|
|
167
|
+
`Starting Netlify Graph session, to edit your library visit ${graphEditUrl} or run \`netlify graph:edit\` in another tab`,
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { exit, log, NETLIFYDEVERR } from './command-helpers.mjs'
|
|
3
|
+
import { startProxy } from './proxy.mjs'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {Object} InspectSettings
|
|
7
|
+
* @property {boolean} enabled - Inspect enabled
|
|
8
|
+
* @property {boolean} pause - Pause on breakpoints
|
|
9
|
+
* @property {string|undefined} address - Host/port override (optional)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {boolean|string} edgeInspect
|
|
14
|
+
* @param {boolean|string} edgeInspectBrk
|
|
15
|
+
* @returns {InspectSettings}
|
|
16
|
+
*/
|
|
17
|
+
export const generateInspectSettings = (edgeInspect, edgeInspectBrk) => {
|
|
18
|
+
const enabled = Boolean(edgeInspect) || Boolean(edgeInspectBrk)
|
|
19
|
+
const pause = Boolean(edgeInspectBrk)
|
|
20
|
+
const getAddress = () => {
|
|
21
|
+
if (edgeInspect) {
|
|
22
|
+
return typeof edgeInspect === 'string' ? edgeInspect : undefined
|
|
23
|
+
}
|
|
24
|
+
if (edgeInspectBrk) {
|
|
25
|
+
return typeof edgeInspectBrk === 'string' ? edgeInspectBrk : undefined
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
enabled,
|
|
31
|
+
pause,
|
|
32
|
+
address: getAddress(),
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
*
|
|
38
|
+
* @param {object} params
|
|
39
|
+
* @param {*} params.addonsUrls
|
|
40
|
+
* @param {import('../commands/base-command.mjs').NetlifyOptions["config"]} params.config
|
|
41
|
+
* @param {string} [params.configPath] An override for the Netlify config path
|
|
42
|
+
* @param {import('../commands/base-command.mjs').NetlifyOptions["cachedConfig"]['env']} params.env
|
|
43
|
+
* @param {InspectSettings} params.inspectSettings
|
|
44
|
+
* @param {() => Promise<object>} params.getUpdatedConfig
|
|
45
|
+
* @param {string} params.geolocationMode
|
|
46
|
+
* @param {string} params.geoCountry
|
|
47
|
+
* @param {*} params.settings
|
|
48
|
+
* @param {boolean} params.offline
|
|
49
|
+
* @param {*} params.site
|
|
50
|
+
* @param {*} params.siteInfo
|
|
51
|
+
* @param {import('./state-config.mjs').default} params.state
|
|
52
|
+
* @returns
|
|
53
|
+
*/
|
|
54
|
+
export const startProxyServer = async ({
|
|
55
|
+
addonsUrls,
|
|
56
|
+
config,
|
|
57
|
+
configPath,
|
|
58
|
+
env,
|
|
59
|
+
geoCountry,
|
|
60
|
+
geolocationMode,
|
|
61
|
+
getUpdatedConfig,
|
|
62
|
+
inspectSettings,
|
|
63
|
+
offline,
|
|
64
|
+
settings,
|
|
65
|
+
site,
|
|
66
|
+
siteInfo,
|
|
67
|
+
state,
|
|
68
|
+
}) => {
|
|
69
|
+
const url = await startProxy({
|
|
70
|
+
addonsUrls,
|
|
71
|
+
config,
|
|
72
|
+
configPath: configPath || site.configPath,
|
|
73
|
+
env,
|
|
74
|
+
geolocationMode,
|
|
75
|
+
geoCountry,
|
|
76
|
+
getUpdatedConfig,
|
|
77
|
+
inspectSettings,
|
|
78
|
+
offline,
|
|
79
|
+
projectDir: site.root,
|
|
80
|
+
settings,
|
|
81
|
+
state,
|
|
82
|
+
siteInfo,
|
|
83
|
+
})
|
|
84
|
+
if (!url) {
|
|
85
|
+
log(NETLIFYDEVERR, `Unable to start proxy server on port '${settings.port}'`)
|
|
86
|
+
exit(1)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return url
|
|
90
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { promises as fs } from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import process from 'process'
|
|
5
|
+
|
|
6
|
+
import { INTERNAL_EDGE_FUNCTIONS_FOLDER } from '../lib/edge-functions/consts.mjs'
|
|
7
|
+
import { getPathInProject } from '../lib/settings.mjs'
|
|
8
|
+
|
|
9
|
+
import { error } from './command-helpers.mjs'
|
|
10
|
+
import { startFrameworkServer } from './framework-server.mjs'
|
|
11
|
+
import { INTERNAL_FUNCTIONS_FOLDER } from './functions/index.mjs'
|
|
12
|
+
|
|
13
|
+
const netlifyBuildPromise = import('@netlify/build')
|
|
14
|
+
|
|
15
|
+
// Copies `netlify.toml`, if one is defined, into the `.netlify` internal
|
|
16
|
+
// directory and returns the path to its new location.
|
|
17
|
+
const copyConfig = async ({ configPath, siteRoot }) => {
|
|
18
|
+
const newConfigPath = path.resolve(siteRoot, getPathInProject(['netlify.toml']))
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
await fs.copyFile(configPath, newConfigPath)
|
|
22
|
+
} catch {
|
|
23
|
+
// no-op
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return newConfigPath
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const cleanInternalDirectory = async (basePath) => {
|
|
30
|
+
const ops = [INTERNAL_FUNCTIONS_FOLDER, INTERNAL_EDGE_FUNCTIONS_FOLDER, 'netlify.toml'].map((name) => {
|
|
31
|
+
const fullPath = path.resolve(basePath, getPathInProject([name]))
|
|
32
|
+
|
|
33
|
+
return fs.rm(fullPath, { force: true, recursive: true })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
await Promise.all(ops)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const getBuildOptions = ({
|
|
40
|
+
cachedConfig,
|
|
41
|
+
options: { configPath, context, cwd = process.cwd(), debug, dry, offline, quiet, saveConfig },
|
|
42
|
+
}) => ({
|
|
43
|
+
cachedConfig,
|
|
44
|
+
configPath,
|
|
45
|
+
siteId: cachedConfig.siteInfo.id,
|
|
46
|
+
token: cachedConfig.token,
|
|
47
|
+
dry,
|
|
48
|
+
debug,
|
|
49
|
+
context,
|
|
50
|
+
mode: 'cli',
|
|
51
|
+
telemetry: false,
|
|
52
|
+
buffer: false,
|
|
53
|
+
offline,
|
|
54
|
+
cwd,
|
|
55
|
+
quiet,
|
|
56
|
+
saveConfig,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const runNetlifyBuild = async ({ cachedConfig, options, settings, site, timeline = 'build' }) => {
|
|
60
|
+
const { default: buildSite, startDev } = await netlifyBuildPromise
|
|
61
|
+
const sharedOptions = getBuildOptions({
|
|
62
|
+
cachedConfig,
|
|
63
|
+
options,
|
|
64
|
+
})
|
|
65
|
+
const devCommand = async (settingsOverrides = {}) => {
|
|
66
|
+
const { ipVersion } = await startFrameworkServer({
|
|
67
|
+
settings: {
|
|
68
|
+
...settings,
|
|
69
|
+
...settingsOverrides,
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
settings.frameworkHost = ipVersion === 6 ? '::1' : '127.0.0.1'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (timeline === 'build') {
|
|
77
|
+
// Start by cleaning the internal directory, as it may have artifacts left
|
|
78
|
+
// by previous builds.
|
|
79
|
+
await cleanInternalDirectory(site.root)
|
|
80
|
+
|
|
81
|
+
// Copy `netlify.toml` into the internal directory. This will be the new
|
|
82
|
+
// location of the config file for the duration of the command.
|
|
83
|
+
const tempConfigPath = await copyConfig({ configPath: cachedConfig.configPath, siteRoot: site.root })
|
|
84
|
+
const buildSiteOptions = {
|
|
85
|
+
...sharedOptions,
|
|
86
|
+
outputConfigPath: tempConfigPath,
|
|
87
|
+
saveConfig: true,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Run Netlify Build using the main entry point.
|
|
91
|
+
const { success } = await buildSite(buildSiteOptions)
|
|
92
|
+
|
|
93
|
+
if (!success) {
|
|
94
|
+
error('Could not start local server due to a build error')
|
|
95
|
+
|
|
96
|
+
return {}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Start the dev server, forcing the usage of a static server as opposed to
|
|
100
|
+
// the framework server.
|
|
101
|
+
await devCommand({
|
|
102
|
+
command: undefined,
|
|
103
|
+
useStaticServer: true,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
return { configPath: tempConfigPath }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const startDevOptions = {
|
|
110
|
+
...sharedOptions,
|
|
111
|
+
|
|
112
|
+
// Set `quiet` to suppress non-essential output from Netlify Build unless
|
|
113
|
+
// the `debug` flag is set.
|
|
114
|
+
quiet: !options.debug,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Run Netlify Build using the `startDev` entry point.
|
|
118
|
+
const { error: startDevError, success } = await startDev(devCommand, startDevOptions)
|
|
119
|
+
|
|
120
|
+
if (!success) {
|
|
121
|
+
error(`Could not start local development server\n\n${startDevError.message}\n\n${startDevError.stack}`)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const runDevTimeline = (options) => runNetlifyBuild({ ...options, timeline: 'dev' })
|
|
128
|
+
|
|
129
|
+
export const runBuildTimeline = (options) => runNetlifyBuild({ ...options, timeline: 'build' })
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import process from 'process'
|
|
3
|
+
|
|
4
|
+
import execa from 'execa'
|
|
5
|
+
import stripAnsiCc from 'strip-ansi-control-characters'
|
|
6
|
+
|
|
7
|
+
import { chalk, log, NETLIFYDEVERR, NETLIFYDEVWARN } from './command-helpers.mjs'
|
|
8
|
+
import { processOnExit } from './dev.mjs'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @type {(() => Promise<void>)[]} - array of functions to run before the process exits
|
|
12
|
+
*/
|
|
13
|
+
const cleanupWork = []
|
|
14
|
+
|
|
15
|
+
let cleanupStarted = false
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {() => Promise<void>} job
|
|
19
|
+
*/
|
|
20
|
+
export const addCleanupJob = (job) => {
|
|
21
|
+
cleanupWork.push(job)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {object} input
|
|
26
|
+
* @param {number=} input.exitCode The exit code to return when exiting the process after cleanup
|
|
27
|
+
*/
|
|
28
|
+
const cleanupBeforeExit = async ({ exitCode }) => {
|
|
29
|
+
// If cleanup has started, then wherever started it will be responsible for exiting
|
|
30
|
+
if (!cleanupStarted) {
|
|
31
|
+
cleanupStarted = true
|
|
32
|
+
try {
|
|
33
|
+
await Promise.all(cleanupWork.map((cleanup) => cleanup()))
|
|
34
|
+
} finally {
|
|
35
|
+
process.exit(exitCode)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Run a command and pipe stdout, stderr and stdin
|
|
42
|
+
* @param {string} command
|
|
43
|
+
* @param {NodeJS.ProcessEnv} env
|
|
44
|
+
* @returns {execa.ExecaChildProcess<string>}
|
|
45
|
+
*/
|
|
46
|
+
export const runCommand = (command, env = {}, spinner = null) => {
|
|
47
|
+
const commandProcess = execa.command(command, {
|
|
48
|
+
preferLocal: true,
|
|
49
|
+
// we use reject=false to avoid rejecting synchronously when the command doesn't exist
|
|
50
|
+
reject: false,
|
|
51
|
+
env,
|
|
52
|
+
// windowsHide needs to be false for child process to terminate properly on Windows
|
|
53
|
+
windowsHide: false,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// This ensures that an active spinner stays at the bottom of the commandline
|
|
57
|
+
// even though the actual framework command might be outputting stuff
|
|
58
|
+
const pipeDataWithSpinner = (writeStream, chunk) => {
|
|
59
|
+
if (spinner && spinner.isSpinning) {
|
|
60
|
+
spinner.clear()
|
|
61
|
+
spinner.isSilent = true
|
|
62
|
+
}
|
|
63
|
+
writeStream.write(chunk, () => {
|
|
64
|
+
if (spinner && spinner.isSpinning) {
|
|
65
|
+
spinner.isSilent = false
|
|
66
|
+
spinner.render()
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
commandProcess.stdout.pipe(stripAnsiCc.stream()).on('data', pipeDataWithSpinner.bind(null, process.stdout))
|
|
72
|
+
commandProcess.stderr.pipe(stripAnsiCc.stream()).on('data', pipeDataWithSpinner.bind(null, process.stderr))
|
|
73
|
+
process.stdin.pipe(commandProcess.stdin)
|
|
74
|
+
|
|
75
|
+
// we can't try->await->catch since we don't want to block on the framework server which
|
|
76
|
+
// is a long running process
|
|
77
|
+
// eslint-disable-next-line promise/catch-or-return
|
|
78
|
+
commandProcess
|
|
79
|
+
// eslint-disable-next-line promise/prefer-await-to-then
|
|
80
|
+
.then(async () => {
|
|
81
|
+
const result = await commandProcess
|
|
82
|
+
const [commandWithoutArgs] = command.split(' ')
|
|
83
|
+
if (result.failed && isNonExistingCommandError({ command: commandWithoutArgs, error: result })) {
|
|
84
|
+
log(
|
|
85
|
+
NETLIFYDEVERR,
|
|
86
|
+
`Failed running command: ${command}. Please verify ${chalk.magenta(`'${commandWithoutArgs}'`)} exists`,
|
|
87
|
+
)
|
|
88
|
+
} else {
|
|
89
|
+
const errorMessage = result.failed
|
|
90
|
+
? `${NETLIFYDEVERR} ${result.shortMessage}`
|
|
91
|
+
: `${NETLIFYDEVWARN} "${command}" exited with code ${result.exitCode}`
|
|
92
|
+
|
|
93
|
+
log(`${errorMessage}. Shutting down Netlify Dev server`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return await cleanupBeforeExit({ exitCode: 1 })
|
|
97
|
+
})
|
|
98
|
+
processOnExit(async () => await cleanupBeforeExit({}))
|
|
99
|
+
|
|
100
|
+
return commandProcess
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const isNonExistingCommandError = ({ command, error: commandError }) => {
|
|
104
|
+
// `ENOENT` is only returned for non Windows systems
|
|
105
|
+
// See https://github.com/sindresorhus/execa/pull/447
|
|
106
|
+
if (commandError.code === 'ENOENT') {
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// if the command is a package manager we let it report the error
|
|
111
|
+
if (['yarn', 'npm'].includes(command)) {
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// this only works on English versions of Windows
|
|
116
|
+
return (
|
|
117
|
+
typeof commandError.message === 'string' &&
|
|
118
|
+
commandError.message.includes('is not recognized as an internal or external command')
|
|
119
|
+
)
|
|
120
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
import fastifyStatic from '@fastify/static'
|
|
5
|
+
import Fastify from 'fastify'
|
|
6
|
+
|
|
7
|
+
import { log, NETLIFYDEVLOG } from './command-helpers.mjs'
|
|
8
|
+
|
|
9
|
+
export const startStaticServer = async ({ settings }) => {
|
|
10
|
+
const server = Fastify()
|
|
11
|
+
const rootPath = path.resolve(settings.dist)
|
|
12
|
+
server.register(fastifyStatic, {
|
|
13
|
+
root: rootPath,
|
|
14
|
+
etag: false,
|
|
15
|
+
acceptRanges: false,
|
|
16
|
+
lastModified: false,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
server.setNotFoundHandler((_req, res) => {
|
|
20
|
+
res.code(404).sendFile('404.html', rootPath)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
server.addHook('onRequest', (req, reply, done) => {
|
|
24
|
+
reply.header('X-Powered-by', 'netlify-dev')
|
|
25
|
+
const validMethods = ['GET', 'HEAD']
|
|
26
|
+
if (!validMethods.includes(req.method)) {
|
|
27
|
+
reply.code(405).send('Method Not Allowed')
|
|
28
|
+
}
|
|
29
|
+
done()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
await server.listen({ port: settings.frameworkPort })
|
|
33
|
+
log(`\n${NETLIFYDEVLOG} Static server listening to`, settings.frameworkPort)
|
|
34
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { BANG, chalk } from './command-helpers.mjs'
|
|
3
|
+
|
|
4
|
+
export const getGeoCountryArgParser = (exampleCommand) => (arg) => {
|
|
5
|
+
// Validate that the arg passed is two letters only for country
|
|
6
|
+
// See https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes
|
|
7
|
+
if (!/^[a-z]{2}$/i.test(arg)) {
|
|
8
|
+
throw new Error(
|
|
9
|
+
`The geo country code must use a two letter abbreviation.
|
|
10
|
+
${chalk.red(BANG)} Example:
|
|
11
|
+
${exampleCommand}`,
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
return arg.toUpperCase()
|
|
15
|
+
}
|