netlify-cli 15.9.1-rc.0 → 15.10.0-rc.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.9.1-rc.0",
4
+ "version": "15.10.0-rc.0",
5
5
  "author": "Netlify Inc.",
6
6
  "type": "module",
7
7
  "engines": {
@@ -44,15 +44,14 @@
44
44
  "dependencies": {
45
45
  "@bugsnag/js": "7.20.2",
46
46
  "@fastify/static": "6.10.2",
47
- "@netlify/build": "29.17.1",
48
- "@netlify/build-info": "7.7.1",
47
+ "@netlify/build": "29.17.3",
48
+ "@netlify/build-info": "7.7.3",
49
49
  "@netlify/config": "20.6.4",
50
- "@netlify/edge-bundler": "8.16.4",
50
+ "@netlify/edge-bundler": "8.17.1",
51
51
  "@netlify/local-functions-proxy": "1.1.1",
52
52
  "@netlify/serverless-functions-api": "1.5.2",
53
53
  "@netlify/zip-it-and-ship-it": "9.13.1",
54
54
  "@octokit/rest": "19.0.13",
55
- "@skn0tt/lambda-local": "2.0.3",
56
55
  "ansi-escapes": "6.2.0",
57
56
  "ansi-styles": "6.2.1",
58
57
  "ansi-to-html": "0.7.2",
@@ -107,6 +106,7 @@
107
106
  "isexe": "2.0.0",
108
107
  "jsonwebtoken": "9.0.1",
109
108
  "jwt-decode": "3.1.2",
109
+ "lambda-local": "2.1.1",
110
110
  "listr": "0.14.3",
111
111
  "locate-path": "7.2.0",
112
112
  "lodash": "4.17.21",
@@ -1,5 +1,6 @@
1
1
  // @ts-check
2
- import { join, resolve } from 'path'
2
+ import { existsSync } from 'fs'
3
+ import { join, relative, resolve } from 'path'
3
4
  import process from 'process'
4
5
  import { format } from 'util'
5
6
 
@@ -9,7 +10,7 @@ import { NodeFS, NoopLogger } from '@netlify/build-info/node'
9
10
  import { resolveConfig } from '@netlify/config'
10
11
  import { Command, Option } from 'commander'
11
12
  import debug from 'debug'
12
- import execa from 'execa'
13
+ import { findUp } from 'find-up'
13
14
  import inquirer from 'inquirer'
14
15
  import inquirerAutocompletePrompt from 'inquirer-autocomplete-prompt'
15
16
  import merge from 'lodash/merge.js'
@@ -38,20 +39,27 @@ import { identify, reportError, track } from '../utils/telemetry/index.mjs'
38
39
 
39
40
  // load the autocomplete plugin
40
41
  inquirer.registerPrompt('autocomplete', inquirerAutocompletePrompt)
41
- // Netlify CLI client id. Lives in bot@netlify.com
42
+ /** Netlify CLI client id. Lives in bot@netlify.com */
42
43
  // TODO: setup client for multiple environments
43
44
  const CLIENT_ID = 'd6f37de6614df7ae58664cfca524744d73807a377f5ee71f1a254f78412e3750'
44
45
 
45
46
  const NANO_SECS_TO_MSECS = 1e6
46
- // The fallback width for the help terminal
47
+ /** The fallback width for the help terminal */
47
48
  const FALLBACK_HELP_CMD_WIDTH = 80
48
49
 
49
50
  const HELP_$ = NETLIFY_CYAN('$')
50
- // indent on commands or description on the help page
51
+ /** indent on commands or description on the help page */
51
52
  const HELP_INDENT_WIDTH = 2
52
- // separator width between term and description
53
+ /** separator width between term and description */
53
54
  const HELP_SEPARATOR_WIDTH = 5
54
55
 
56
+ /**
57
+ * A list of commands where we don't have to perform the workspace selection at.
58
+ * Those commands work with the system or are not writing any config files that need to be
59
+ * workspace aware.
60
+ */
61
+ const COMMANDS_WITHOUT_WORKSPACE_OPTIONS = new Set(['recipes', 'completion', 'status', 'switch', 'login', 'lm'])
62
+
55
63
  /**
56
64
  * Formats a help list correctly with the correct indent
57
65
  * @param {string[]} textArray
@@ -78,10 +86,14 @@ const getDuration = function (startTime) {
78
86
  */
79
87
  async function selectWorkspace(project, filter) {
80
88
  const selected = project.workspace?.packages.find((pkg) => {
81
- if (project.relativeBaseDirectory && pkg.path.startsWith(project.relativeBaseDirectory)) {
89
+ if (
90
+ project.relativeBaseDirectory &&
91
+ project.relativeBaseDirectory.length !== 0 &&
92
+ pkg.path.startsWith(project.relativeBaseDirectory)
93
+ ) {
82
94
  return true
83
95
  }
84
- return pkg.name === filter
96
+ return (pkg.name && pkg.name === filter) || pkg.path === filter
85
97
  })
86
98
 
87
99
  if (!selected) {
@@ -96,9 +108,9 @@ async function selectWorkspace(project, filter) {
96
108
  (project.workspace?.packages || [])
97
109
  .filter((pkg) => pkg.path.includes(input))
98
110
  .map((pkg) => ({
99
- name: `${pkg.name && `${chalk.bold(pkg.name)}`} ${pkg.path} ${
100
- pkg.name && chalk.dim(`--filter ${pkg.name}`)
101
- }`,
111
+ name: `${pkg.name ? `${chalk.bold(pkg.name)} ` : ''}${pkg.path} ${chalk.dim(
112
+ `--filter ${pkg.name || pkg.path}`,
113
+ )}`,
102
114
  value: pkg.path,
103
115
  })),
104
116
  })
@@ -131,6 +143,12 @@ export default class BaseCommand extends Command {
131
143
  // eslint-disable-next-line workspace/no-process-cwd
132
144
  workingDir = process.cwd()
133
145
 
146
+ /**
147
+ * The workspace root if inside a mono repository.
148
+ * Must not be the repository root!
149
+ * @type {string|undefined}
150
+ */
151
+ jsWorkspaceRoot
134
152
  /**
135
153
  * The current workspace package we should execute the commands in
136
154
  * @type {string|undefined}
@@ -144,57 +162,61 @@ export default class BaseCommand extends Command {
144
162
  * @returns
145
163
  */
146
164
  createCommand(name) {
147
- return (
148
- new BaseCommand(name)
149
- // If --silent or --json flag passed disable logger
150
- .addOption(new Option('--json', 'Output return values as JSON').hideHelp(true))
151
- .addOption(new Option('--silent', 'Silence CLI output').hideHelp(true))
152
- .addOption(new Option('--cwd <cwd>').hideHelp(true))
153
- .addOption(new Option('-o, --offline').hideHelp(true))
154
- .addOption(new Option('--auth <token>', 'Netlify auth token').hideHelp(true))
155
- .addOption(
156
- new Option(
157
- '--httpProxy [address]',
158
- 'Old, prefer --http-proxy. Proxy server address to route requests through.',
159
- )
160
- .default(process.env.HTTP_PROXY || process.env.HTTPS_PROXY)
161
- .hideHelp(true),
162
- )
163
- .addOption(
164
- new Option(
165
- '--httpProxyCertificateFilename [file]',
166
- 'Old, prefer --http-proxy-certificate-filename. Certificate file to use when connecting using a proxy server.',
167
- )
168
- .default(process.env.NETLIFY_PROXY_CERTIFICATE_FILENAME)
169
- .hideHelp(true),
165
+ const base = new BaseCommand(name)
166
+ // If --silent or --json flag passed disable logger
167
+ .addOption(new Option('--json', 'Output return values as JSON').hideHelp(true))
168
+ .addOption(new Option('--silent', 'Silence CLI output').hideHelp(true))
169
+ .addOption(new Option('--cwd <cwd>').hideHelp(true))
170
+ .addOption(new Option('-o, --offline').hideHelp(true))
171
+ .addOption(new Option('--auth <token>', 'Netlify auth token').hideHelp(true))
172
+ .addOption(
173
+ new Option('--httpProxy [address]', 'Old, prefer --http-proxy. Proxy server address to route requests through.')
174
+ .default(process.env.HTTP_PROXY || process.env.HTTPS_PROXY)
175
+ .hideHelp(true),
176
+ )
177
+ .addOption(
178
+ new Option(
179
+ '--httpProxyCertificateFilename [file]',
180
+ 'Old, prefer --http-proxy-certificate-filename. Certificate file to use when connecting using a proxy server.',
170
181
  )
171
- .option(
182
+ .default(process.env.NETLIFY_PROXY_CERTIFICATE_FILENAME)
183
+ .hideHelp(true),
184
+ )
185
+ .addOption(
186
+ new Option(
172
187
  '--http-proxy-certificate-filename [file]',
173
188
  'Certificate file to use when connecting using a proxy server',
174
- process.env.NETLIFY_PROXY_CERTIFICATE_FILENAME,
175
- )
176
- .option(
177
- '--http-proxy [address]',
178
- 'Proxy server address to route requests through.',
179
- process.env.HTTP_PROXY || process.env.HTTPS_PROXY,
180
189
  )
181
- .option('--debug', 'Print debugging information')
190
+ .default(process.env.NETLIFY_PROXY_CERTIFICATE_FILENAME)
191
+ .hideHelp(true),
192
+ )
193
+ .addOption(
194
+ new Option('--httpProxy [address]', 'Proxy server address to route requests through.')
195
+ .default(process.env.HTTP_PROXY || process.env.HTTPS_PROXY)
196
+ .hideHelp(true),
197
+ )
198
+ .option('--debug', 'Print debugging information')
199
+
200
+ // only add the `--config` or `--filter` option to commands that are workspace aware
201
+ if (!COMMANDS_WITHOUT_WORKSPACE_OPTIONS.has(name)) {
202
+ base
182
203
  .option('--config <configFilePath>', 'Custom path to a netlify configuration file')
183
204
  .option(
184
205
  '--filter <app>',
185
206
  'Optional name of an application to run the command in.\nThis option is needed for working in Monorepos',
186
207
  )
187
- .hook('preAction', async (_parentCommand, actionCommand) => {
188
- if (actionCommand.opts()?.debug) {
189
- process.env.DEBUG = '*'
190
- }
191
- debug(`${name}:preAction`)('start')
192
- this.analytics = { startTime: process.hrtime.bigint() }
193
- // @ts-ignore cannot type actionCommand as BaseCommand
194
- await this.init(actionCommand)
195
- debug(`${name}:preAction`)('end')
196
- })
197
- )
208
+ }
209
+
210
+ return base.hook('preAction', async (_parentCommand, actionCommand) => {
211
+ if (actionCommand.opts()?.debug) {
212
+ process.env.DEBUG = '*'
213
+ }
214
+ debug(`${name}:preAction`)('start')
215
+ this.analytics = { startTime: process.hrtime.bigint() }
216
+ // @ts-ignore cannot type actionCommand as BaseCommand
217
+ await this.init(actionCommand)
218
+ debug(`${name}:preAction`)('end')
219
+ })
198
220
  }
199
221
 
200
222
  /** @private */
@@ -234,18 +256,22 @@ export default class BaseCommand extends Command {
234
256
  return padLeft(term, HELP_INDENT_WIDTH)
235
257
  }
236
258
 
259
+ /**
260
+ * @param {BaseCommand} command
261
+ */
237
262
  const getCommands = (command) => {
238
263
  const parentCommand = this.name() === 'netlify' ? command : command.parent
239
- return parentCommand.commands.filter((cmd) => {
240
- // eslint-disable-next-line no-underscore-dangle
241
- if (cmd._hidden) return false
242
- // the root command
243
- if (this.name() === 'netlify') {
244
- // don't include subcommands on the main page
245
- return !cmd.name().includes(':')
246
- }
247
- return cmd.name().startsWith(`${command.name()}:`)
248
- })
264
+ return (
265
+ parentCommand?.commands.filter((cmd) => {
266
+ if (cmd._hidden) return false
267
+ // the root command
268
+ if (this.name() === 'netlify') {
269
+ // don't include subcommands on the main page
270
+ return !cmd.name().includes(':')
271
+ }
272
+ return cmd.name().startsWith(`${command.name()}:`)
273
+ }) || []
274
+ )
249
275
  }
250
276
 
251
277
  /**
@@ -338,9 +364,8 @@ export default class BaseCommand extends Command {
338
364
  }
339
365
 
340
366
  // Aliases
341
- // eslint-disable-next-line no-underscore-dangle
367
+
342
368
  if (command._aliases.length !== 0) {
343
- // eslint-disable-next-line no-underscore-dangle
344
369
  const aliases = command._aliases.map((alias) => formatItem(`${parentCommand.name()} ${alias}`, null, true))
345
370
  output = [...output, chalk.bold('ALIASES'), formatHelpList(aliases), '']
346
371
  }
@@ -511,13 +536,26 @@ export default class BaseCommand extends Command {
511
536
  })
512
537
  })
513
538
  const frameworks = await this.project.detectFrameworks()
539
+ /** @type { string|undefined} */
540
+ let packageConfig = flags.config ? resolve(flags.config) : undefined
514
541
  // check if we have detected multiple projects inside which one we have to perform our operations.
515
542
  // only ask to select one if on the workspace root
516
- if (this.project.workspace?.packages.length && this.project.workspace.isRoot) {
543
+ if (
544
+ !COMMANDS_WITHOUT_WORKSPACE_OPTIONS.has(actionCommand.name()) &&
545
+ this.project.workspace?.packages.length &&
546
+ this.project.workspace.isRoot
547
+ ) {
517
548
  this.workspacePackage = await selectWorkspace(this.project, actionCommand.opts().filter)
518
549
  this.workingDir = join(this.project.jsWorkspaceRoot, this.workspacePackage)
519
550
  }
520
551
 
552
+ this.jsWorkspaceRoot = this.project.jsWorkspaceRoot
553
+ // detect if a toml exists in this package.
554
+ const tomlFile = join(this.workingDir, 'netlify.toml')
555
+ if (!packageConfig && existsSync(tomlFile)) {
556
+ packageConfig = tomlFile
557
+ }
558
+
521
559
  // ==================================================
522
560
  // Retrieve Site id and build state from the state.json
523
561
  // ==================================================
@@ -541,9 +579,10 @@ export default class BaseCommand extends Command {
541
579
  // configuration file and the API
542
580
  // ==================================================
543
581
  const cachedConfig = await actionCommand.getConfig({
544
- cwd: this.workingDir,
582
+ cwd: this.jsWorkspaceRoot || this.workingDir,
583
+ repositoryRoot: rootDir,
545
584
  // The config flag needs to be resolved from the actual process working directory
546
- configFilePath: flags.config ? resolve(flags.config) : undefined,
585
+ configFilePath: packageConfig,
547
586
  state,
548
587
  token,
549
588
  ...apiUrlOpts,
@@ -588,11 +627,20 @@ export default class BaseCommand extends Command {
588
627
  actionCommand.project = this.project
589
628
  actionCommand.workingDir = this.workingDir
590
629
  actionCommand.workspacePackage = this.workspacePackage
630
+ actionCommand.jsWorkspaceRoot = this.jsWorkspaceRoot
631
+
632
+ // Either an existing configuration file from `@netlify/config` or a file path
633
+ // that should be used for creating it.
634
+ const configFilePath = configPath || join(this.workingDir, 'netlify.toml')
635
+
591
636
  actionCommand.netlify = {
592
637
  // api methods
593
638
  api,
594
639
  apiOpts,
640
+ // The Absolute Repository root (detected through @netlify/config)
595
641
  repositoryRoot,
642
+ configFilePath,
643
+ relConfigFilePath: relative(repositoryRoot, configFilePath),
596
644
  // current site context
597
645
  site: {
598
646
  root: buildDir,
@@ -683,24 +731,19 @@ export default class BaseCommand extends Command {
683
731
  * @returns {'production' | 'dev'}
684
732
  */
685
733
  getDefaultContext() {
686
- if (this.name() === 'serve') {
687
- return 'production'
688
- }
689
-
690
- return 'dev'
734
+ return this.name() === 'serve' ? 'production' : 'dev'
691
735
  }
692
736
  }
693
737
 
694
738
  /**
695
739
  * Retrieves the repository root through a git command.
696
740
  * Returns undefined if not a git project.
741
+ * @param {string} [cwd] The optional current working directory
697
742
  * @returns {Promise<string|undefined>}
698
743
  */
699
- async function getRepositoryRoot() {
700
- try {
701
- const res = await execa('git', ['rev-parse', '--show-toplevel'], { preferLocal: true })
702
- return res.stdout
703
- } catch {
704
- // noop
744
+ async function getRepositoryRoot(cwd) {
745
+ const res = await findUp('.git', { cwd, type: 'directory' })
746
+ if (res) {
747
+ return join(res, '..')
705
748
  }
706
749
  }
@@ -2,6 +2,7 @@
2
2
  import process from 'process'
3
3
 
4
4
  import { getBuildOptions, runBuild } from '../../lib/build.mjs'
5
+ import { detectFrameworkSettings } from '../../utils/build-info.mjs'
5
6
  import { error, exit, getToken } from '../../utils/command-helpers.mjs'
6
7
  import { getEnvelopeEnv, normalizeContext } from '../../utils/env/index.mjs'
7
8
 
@@ -33,11 +34,18 @@ const injectEnv = async function (command, { api, buildOptions, context, siteInf
33
34
  * @param {import('../base-command.mjs').default} command
34
35
  */
35
36
  const build = async (options, command) => {
37
+ const { cachedConfig, siteInfo } = command.netlify
36
38
  command.setAnalyticsPayload({ dry: options.dry })
37
39
  // Retrieve Netlify Build options
38
40
  const [token] = await getToken()
41
+ const settings = await detectFrameworkSettings(command, 'build')
42
+
43
+ // override the build command with the detection result if no command is specified through the config
44
+ if (!cachedConfig.config.build.command) {
45
+ cachedConfig.config.build.command = settings?.buildCommand
46
+ cachedConfig.config.build.commandOrigin = 'heuristics'
47
+ }
39
48
 
40
- const { cachedConfig, siteInfo } = command.netlify
41
49
  const buildOptions = await getBuildOptions({
42
50
  cachedConfig,
43
51
  token,
@@ -72,6 +72,7 @@ const triggerDeploy = async ({ api, options, siteData, siteId }) => {
72
72
  * @returns {Promise<string>}
73
73
  */
74
74
  const getDeployFolder = async ({ config, options, site, siteData, workingDir }) => {
75
+ console.log()
75
76
  let deployFolder
76
77
  if (options.dir) {
77
78
  deployFolder = resolve(workingDir, options.dir)
@@ -102,6 +102,7 @@ const dev = async (options, command) => {
102
102
  autoLaunch: Boolean(options.open),
103
103
  ...(config.functionsDirectory && { functions: config.functionsDirectory }),
104
104
  ...(config.build.publish && { publish: config.build.publish }),
105
+ ...(config.build.base && { base: config.build.base }),
105
106
  ...config.dev,
106
107
  ...options,
107
108
  }
@@ -151,11 +152,9 @@ const dev = async (options, command) => {
151
152
  log(`${NETLIFYDEVWARN} Setting up local development server`)
152
153
 
153
154
  const { configPath: configPathOverride } = await runDevTimeline({
154
- cachedConfig,
155
+ command,
155
156
  options,
156
157
  settings,
157
- projectDir: command.workingDir,
158
- site,
159
158
  env: {
160
159
  URL: url,
161
160
  DEPLOY_URL: url,