netlify-cli 15.3.1 → 15.4.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
3
  "description": "Netlify command line tool",
4
- "version": "15.3.1",
4
+ "version": "15.4.0",
5
5
  "author": "Netlify Inc.",
6
6
  "type": "module",
7
7
  "engines": {
@@ -44,13 +44,13 @@
44
44
  "dependencies": {
45
45
  "@bugsnag/js": "7.20.2",
46
46
  "@fastify/static": "6.10.2",
47
- "@netlify/build": "29.11.7",
48
- "@netlify/build-info": "7.0.4",
49
- "@netlify/config": "20.4.3",
50
- "@netlify/edge-bundler": "8.16.0",
51
- "@netlify/framework-info": "9.8.8",
47
+ "@netlify/build": "29.12.1",
48
+ "@netlify/build-info": "7.0.5",
49
+ "@netlify/config": "20.4.4",
50
+ "@netlify/edge-bundler": "8.16.1",
51
+ "@netlify/framework-info": "9.8.9",
52
52
  "@netlify/local-functions-proxy": "1.1.1",
53
- "@netlify/zip-it-and-ship-it": "9.7.0",
53
+ "@netlify/zip-it-and-ship-it": "9.8.0",
54
54
  "@octokit/rest": "19.0.11",
55
55
  "@skn0tt/lambda-local": "2.0.3",
56
56
  "ansi-escapes": "6.2.0",
@@ -83,6 +83,7 @@
83
83
  "express": "4.18.2",
84
84
  "express-logging": "1.1.1",
85
85
  "extract-zip": "2.0.1",
86
+ "fastest-levenshtein": "1.0.16",
86
87
  "fastify": "4.17.0",
87
88
  "find-up": "6.3.0",
88
89
  "flush-write-stream": "2.0.0",
@@ -90,7 +91,7 @@
90
91
  "from2-array": "0.0.4",
91
92
  "fuzzy": "0.1.3",
92
93
  "get-port": "5.1.1",
93
- "gh-release-fetch": "4.0.0",
94
+ "gh-release-fetch": "4.0.1",
94
95
  "git-repo-info": "2.1.1",
95
96
  "gitconfiglocal": "2.1.0",
96
97
  "hasbin": "1.2.3",
@@ -133,7 +134,6 @@
133
134
  "read-pkg-up": "9.1.0",
134
135
  "semver": "7.5.1",
135
136
  "source-map-support": "0.5.21",
136
- "string-similarity": "4.0.4",
137
137
  "strip-ansi-control-characters": "2.0.0",
138
138
  "tabtab": "3.0.2",
139
139
  "tempy": "3.0.0",
@@ -21,7 +21,7 @@ import detectServerSettings, { getConfigWithPlugins } from '../../utils/detect-s
21
21
  import { getDotEnvVariables, getSiteInformation, injectEnvVariables } from '../../utils/dev.mjs'
22
22
  import { getEnvelopeEnv, normalizeContext } from '../../utils/env/index.mjs'
23
23
  import { ensureNetlifyIgnore } from '../../utils/gitignore.mjs'
24
- import { startLiveTunnel } from '../../utils/live-tunnel.mjs'
24
+ import { getLiveTunnelSlug, startLiveTunnel } from '../../utils/live-tunnel.mjs'
25
25
  import openBrowser from '../../utils/open-browser.mjs'
26
26
  import { generateInspectSettings, startProxyServer } from '../../utils/proxy-server.mjs'
27
27
  import { getProxyUrl } from '../../utils/proxy.mjs'
@@ -37,16 +37,33 @@ import { createDevExecCommand } from './dev-exec.mjs'
37
37
  * @param {import('commander').OptionValues} config.options
38
38
  * @param {*} config.settings
39
39
  * @param {*} config.site
40
+ * @param {*} config.state
40
41
  * @returns
41
42
  */
42
- const handleLiveTunnel = async ({ api, options, settings, site }) => {
43
- if (options.live) {
43
+ const handleLiveTunnel = async ({ api, options, settings, site, state }) => {
44
+ const { live } = options
45
+
46
+ if (live) {
47
+ const customSlug = typeof live === 'string' && live.length !== 0 ? live : undefined
48
+ const slug = getLiveTunnelSlug(state, customSlug)
49
+
50
+ let message = `${NETLIFYDEVWARN} Creating live URL with ID ${chalk.yellow(slug)}`
51
+
52
+ if (!customSlug) {
53
+ message += ` (to generate a custom URL, use ${chalk.magenta('--live=<subdomain>')})`
54
+ }
55
+
56
+ log(message)
57
+
44
58
  const sessionUrl = await startLiveTunnel({
45
59
  siteId: site.id,
46
60
  netlifyApiToken: api.accessToken,
47
61
  localPort: settings.port,
62
+ slug,
48
63
  })
64
+
49
65
  process.env.BASE_URL = sessionUrl
66
+
50
67
  return sessionUrl
51
68
  }
52
69
  }
@@ -125,8 +142,9 @@ const dev = async (options, command) => {
125
142
 
126
143
  command.setAnalyticsPayload({ live: options.live })
127
144
 
128
- const liveTunnelUrl = await handleLiveTunnel({ options, site, api, settings })
145
+ const liveTunnelUrl = await handleLiveTunnel({ options, site, api, settings, state })
129
146
  const url = liveTunnelUrl || getProxyUrl(settings)
147
+
130
148
  process.env.URL = url
131
149
  process.env.DEPLOY_URL = url
132
150
 
@@ -229,7 +247,11 @@ export const createDevCommand = (program) => {
229
247
  .option('-d ,--dir <path>', 'dir with static files')
230
248
  .option('-f ,--functions <folder>', 'specify a functions folder to serve')
231
249
  .option('-o ,--offline', 'disables any features that require network access')
232
- .option('-l, --live', 'start a public live session', false)
250
+ .option(
251
+ '-l, --live [subdomain]',
252
+ 'start a public live session; optionally, supply a subdomain to generate a custom URL',
253
+ false,
254
+ )
233
255
  .addOption(
234
256
  new Option('--functionsPort <port>', 'Old, prefer --functions-port. Port of functions server')
235
257
  .argParser((value) => Number.parseInt(value))
@@ -2,8 +2,8 @@
2
2
  import process from 'process'
3
3
 
4
4
  import { Option } from 'commander'
5
+ import { closest } from 'fastest-levenshtein'
5
6
  import inquirer from 'inquirer'
6
- import { findBestMatch } from 'string-similarity'
7
7
 
8
8
  import { BANG, chalk, error, exit, log, NETLIFY_CYAN, USER_AGENT, warn } from '../utils/command-helpers.mjs'
9
9
  import execa from '../utils/execa.mjs'
@@ -118,9 +118,7 @@ const mainCommand = async function (options, command) {
118
118
  warn(`${chalk.yellow(command.args[0])} is not a ${command.name()} command.`)
119
119
 
120
120
  const allCommands = command.commands.map((cmd) => cmd.name())
121
- const {
122
- bestMatch: { target: suggestion },
123
- } = findBestMatch(command.args[0], allCommands)
121
+ const suggestion = closest(command.args[0], allCommands)
124
122
 
125
123
  const applySuggestion = await new Promise((resolve) => {
126
124
  const prompt = inquirer.prompt({
@@ -1,8 +1,8 @@
1
1
  // @ts-check
2
2
  import { basename } from 'path'
3
3
 
4
+ import { closest } from 'fastest-levenshtein'
4
5
  import inquirer from 'inquirer'
5
- import { findBestMatch } from 'string-similarity'
6
6
 
7
7
  import { NETLIFYDEVERR, chalk, log } from '../../utils/command-helpers.mjs'
8
8
 
@@ -28,7 +28,10 @@ const recipesCommand = async (recipeName, options, command) => {
28
28
  try {
29
29
  return await runRecipe({ config, recipeName: sanitizedRecipeName, repositoryRoot })
30
30
  } catch (error) {
31
- if (error.code !== 'MODULE_NOT_FOUND') {
31
+ if (
32
+ // The ESM loader throws this instead of MODULE_NOT_FOUND
33
+ error.code !== 'ERR_MODULE_NOT_FOUND'
34
+ ) {
32
35
  throw error
33
36
  }
34
37
 
@@ -36,9 +39,7 @@ const recipesCommand = async (recipeName, options, command) => {
36
39
 
37
40
  const recipes = await listRecipes()
38
41
  const recipeNames = recipes.map(({ name }) => name)
39
- const {
40
- bestMatch: { target: suggestion },
41
- } = findBestMatch(recipeName, recipeNames)
42
+ const suggestion = closest(recipeName, recipeNames)
42
43
  const applySuggestion = await new Promise((resolve) => {
43
44
  const prompt = inquirer.prompt({
44
45
  type: 'confirm',
@@ -164,9 +164,9 @@
164
164
  "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
165
165
  },
166
166
  "node_modules/qs": {
167
- "version": "6.11.1",
168
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz",
169
- "integrity": "sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==",
167
+ "version": "6.11.2",
168
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz",
169
+ "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==",
170
170
  "dependencies": {
171
171
  "side-channel": "^1.0.4"
172
172
  },
@@ -296,9 +296,9 @@
296
296
  "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
297
297
  },
298
298
  "qs": {
299
- "version": "6.11.1",
300
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz",
301
- "integrity": "sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==",
299
+ "version": "6.11.2",
300
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz",
301
+ "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==",
302
302
  "requires": {
303
303
  "side-channel": "^1.0.4"
304
304
  }
@@ -26,9 +26,9 @@
26
26
  }
27
27
  },
28
28
  "node_modules/@types/node": {
29
- "version": "14.18.47",
30
- "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.47.tgz",
31
- "integrity": "sha512-OuJi8bIng4wYHHA3YpKauL58dZrPxro3d0tabPHyiNF8rKfGKuVfr83oFlPLmKri1cX+Z3cJP39GXmnqkP11Gw=="
29
+ "version": "14.18.48",
30
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.48.tgz",
31
+ "integrity": "sha512-iL0PIMwejpmuVHgfibHpfDwOdsbmB50wr21X71VnF5d7SsBF7WK+ZvP/SCcFm7Iwb9iiYSap9rlrdhToNAWdxg=="
32
32
  },
33
33
  "node_modules/is-promise": {
34
34
  "version": "4.0.0",
@@ -58,9 +58,9 @@
58
58
  }
59
59
  },
60
60
  "@types/node": {
61
- "version": "14.18.47",
62
- "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.47.tgz",
63
- "integrity": "sha512-OuJi8bIng4wYHHA3YpKauL58dZrPxro3d0tabPHyiNF8rKfGKuVfr83oFlPLmKri1cX+Z3cJP39GXmnqkP11Gw=="
61
+ "version": "14.18.48",
62
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.48.tgz",
63
+ "integrity": "sha512-iL0PIMwejpmuVHgfibHpfDwOdsbmB50wr21X71VnF5d7SsBF7WK+ZvP/SCcFm7Iwb9iiYSap9rlrdhToNAWdxg=="
64
64
  },
65
65
  "is-promise": {
66
66
  "version": "4.0.0",
@@ -3,6 +3,7 @@ import process from 'process'
3
3
 
4
4
  import fetch from 'node-fetch'
5
5
  import pWaitFor from 'p-wait-for'
6
+ import { v4 as uuidv4 } from 'uuid'
6
7
 
7
8
  import { fetchLatestVersion, shouldFetchLatestVersion } from '../lib/exec-fetcher.mjs'
8
9
  import { getPathInHome } from '../lib/settings.mjs'
@@ -12,13 +13,14 @@ import execa from './execa.mjs'
12
13
 
13
14
  const PACKAGE_NAME = 'live-tunnel-client'
14
15
  const EXEC_NAME = PACKAGE_NAME
16
+ const SLUG_LOCAL_STATE_KEY = 'liveTunnelSlug'
15
17
 
16
18
  // 1 second
17
19
  const TUNNEL_POLL_INTERVAL = 1e3
18
20
  // 5 minutes
19
21
  const TUNNEL_POLL_TIMEOUT = 3e5
20
22
 
21
- const createTunnel = async function ({ netlifyApiToken, siteId }) {
23
+ const createTunnel = async function ({ netlifyApiToken, siteId, slug }) {
22
24
  await installTunnelClient()
23
25
 
24
26
  if (!siteId) {
@@ -29,9 +31,8 @@ const createTunnel = async function ({ netlifyApiToken, siteId }) {
29
31
  )
30
32
  process.exit(1)
31
33
  }
32
- log(`${NETLIFYDEVLOG} Creating Live Tunnel for ${siteId}`)
33
- const url = `https://api.netlify.com/api/v1/live_sessions?site_id=${siteId}`
34
34
 
35
+ const url = `https://api.netlify.com/api/v1/live_sessions?site_id=${siteId}&slug=${slug}`
35
36
  const response = await fetch(url, {
36
37
  method: 'POST',
37
38
  headers: {
@@ -87,10 +88,11 @@ const installTunnelClient = async function () {
87
88
  })
88
89
  }
89
90
 
90
- export const startLiveTunnel = async ({ localPort, netlifyApiToken, siteId }) => {
91
+ export const startLiveTunnel = async ({ localPort, netlifyApiToken, siteId, slug }) => {
91
92
  const session = await createTunnel({
92
93
  siteId,
93
94
  netlifyApiToken,
95
+ slug,
94
96
  })
95
97
 
96
98
  const isLiveTunnelReady = async function () {
@@ -121,3 +123,27 @@ export const startLiveTunnel = async ({ localPort, netlifyApiToken, siteId }) =>
121
123
 
122
124
  return session.session_url
123
125
  }
126
+
127
+ export const getLiveTunnelSlug = (state, override) => {
128
+ if (override !== undefined) {
129
+ return override
130
+ }
131
+
132
+ const newSlug = generateRandomSlug()
133
+
134
+ try {
135
+ const existingSlug = state.get(SLUG_LOCAL_STATE_KEY)
136
+
137
+ if (existingSlug !== undefined) {
138
+ return existingSlug
139
+ }
140
+
141
+ state.set(SLUG_LOCAL_STATE_KEY, newSlug)
142
+ } catch (error) {
143
+ log(`${NETLIFYDEVERR} Could not read or write local state file: ${error.message}`)
144
+ }
145
+
146
+ return newSlug
147
+ }
148
+
149
+ const generateRandomSlug = () => uuidv4().slice(0, 8)