Compare commits

..

38 Commits

Author SHA1 Message Date
JJ Kasper
21337de7cd Publish Stable
- @now/next@2.5.4
2020-04-15 15:57:17 -05:00
JJ Kasper
a6686f9ff5 Publish Canary
- now@18.0.1-canary.1
 - @now/next@2.5.4-canary.0
2020-04-15 15:52:16 -05:00
JJ Kasper
65c621bd55 [now-next] Fix static 404 not being used in mono-repo set-up (#4083)
This fixes the static 404 page not being used when deploying in a mono-repo structure. Before we weren't taking into account the `entryDirectory` where we needed to causing us to deploy `_error` when we didn't need to

x-ref: https://github.com/zeit/now/discussions/4077#discussioncomment-4625
2020-04-15 20:29:12 +00:00
Luc
0827d3514d [now-cli] Remove custom error when fetching frameworks fail (#4081)
Fix PRODUCT-2364.

Use `Client` instead of `node-fetch` to retrieve frameworks list.
2020-04-15 17:30:48 +00:00
Steven
7bc5d9fb5b Publish Canary
- now@18.0.1-canary.0
2020-04-15 12:30:24 -04:00
Steven
c53106ecee [now-cli] Fix for RangeError invalid count value (#4074)
This PR fixes a [sentry error](https://sentry.io/organizations/zeithq/issues/1611628097/events/75ed28bc6aef42868721a0912875fdbd/) where `repeat()` is passed a negative number and throws [`RangeError: invalid count value`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Resulting_string_too_large).

This PR ensures `repeat()` is always passed 0 or greater.
2020-04-15 15:22:30 +00:00
Andy
91e4d18ab8 [now-cli] Proxy everything except Lambdas to the dev server (#4079) 2020-04-15 16:29:59 +02:00
Steven
33cd78b93a [now-cli] Fix undefined teams and cache result (#4071)
- Fix [sentry error](https://sentry.io/organizations/zeithq/issues/1610513448/events/12bde04e921442a4aae8b7b10a759ecb/) where the `teams` might be undefined.
- Fix regression from #3740 which accidentally removed caching `teams`.
- Fix superflous `console.time()` calls for the same `GET /teams` API call. See below:

```sh
> [debug] [2020-04-14T18:31:00.220Z] GET https://api.zeit.co/www/user
> [debug] [2020-04-14T18:31:00.533Z] GET https://api.zeit.co/www/user : 313.078ms
> [debug] [2020-04-14T18:31:00.543Z] GET https://api.zeit.co/teams
> [debug] [2020-04-14T18:31:00.799Z] GET https://api.zeit.co/teams : 255.459ms
> [debug] #1 GET /teams: 260.198ms
```
2020-04-14 19:35:53 +00:00
Dávid Lévai
a765d27e5a [examples] Fix typo in Gatsby example (#4063)
Co-authored-by: Steven <steven@ceriously.com>
2020-04-14 15:06:18 -04:00
Steven
e65ff4bfd5 [examples] Fix ionic-react missing public directory (#4070)
The `public` directory was missing from the `ionic-react` example because we were ignoring all `public` directories.

This PR adds the public directory back (it is copied from now-static-build test fixtures). I also updated `.gitignore` and `.gitattributes` to be a little more friendly to our test fixtures so this doesn't happen again.
2020-04-14 16:31:49 +00:00
Steven
30a4787390 [now-cli] Fix encoding domain names (#4068)
Fixes [sentry error](https://sentry.io/organizations/zeithq/issues/1611174358/events/c3455f32ce1743b58cc248d548538b21/) so that domain name is always encoded because this input comes from the user.
2020-04-14 16:00:04 +00:00
Naoyuki Kanezawa
36f6f1db77 remove query and all options from log (#4066) 2020-04-15 00:31:57 +09:00
Steven
8043e25d6d Publish Stable
- @now/frameworks@0.0.13
 - @now/build-utils@2.2.1
 - now@18.0.0
 - now-client@7.1.0
 - @now/go@1.0.7
 - @now/next@2.5.3
 - @now/node@1.5.1
 - @now/python@1.1.6
 - @now/routing-utils@1.8.1
 - @now/static-build@0.16.0
2020-04-13 20:19:14 -04:00
Steven
73b4e7aea4 Publish Canary
- now@17.1.2-canary.16
2020-04-13 19:56:50 -04:00
Steven
de7e063c9b [now-cli] Fix now env stdin detection and don't throw for known errors (#4060)
- Capitalize `Environment` in all outputs
- Fix stdin detection so there is no need for timeouts
- Dont throw if the error is bad user input, only throw for unexpected errors
- Fix tests so waiting for prompt will throw if expected output is never received
2020-04-13 19:56:22 -04:00
Steven
e7c30deee8 [now-cli] Change now env pull console output (#4059)
Change from `Created file .env` to `Create .env file` per @leo
2020-04-13 15:50:06 -04:00
Steven
7464fac792 Publish Canary
- now@17.1.2-canary.15
2020-04-13 14:26:03 -04:00
Steven
252363cce4 [now-cli] Prefer .env over .env.build during now dev (#4057)
The latest `now env` subcommand no longer makes the distinction between build time and runtime environment variables so this PR updates `now dev` to no longer make the distinction either.

The only exception is that some builders such as `@now/next` might still rely on the separation so we must preserve the distinction for legacy builders.
2020-04-13 18:22:21 +00:00
Steven
d7dceeb2a1 [now-cli] Update now env to support empty values (#4046)
This PR adds support for empty values when using `now env add|ls|rm|pull`.

This is necessary when using system environment variables such as `NOW_GITHUB_COMMIT_SHA` which will not receive a value from the user but will instead assign a value automatically during deployment.
2020-04-13 15:21:20 +00:00
Ana Trajkovska
6b5b9e8b2f Publish Canary
- @now/build-utils@2.2.1-canary.1
 - now@17.1.2-canary.14
2020-04-10 18:51:20 +02:00
Ana Trajkovska
addd036149 Improve pagination message (#4035) 2020-04-10 18:46:32 +02:00
Steven
7ca672a470 [now-build-utils] Exclude now.json from fallback build src (#4042)
https://github.com/zeit/now/discussions/4016
2020-04-10 16:06:02 +00:00
Ana Trajkovska
3af0e99689 Publish Canary
- now@17.1.2-canary.13
 - @now/python@1.1.6-canary.0
2020-04-10 17:21:15 +02:00
Ana Trajkovska
b14068de8a [now-cli] Fix removing a deployment (#4036)
* Fix removing a deployment

* Add test for removing a deployment

* Add logs

* Fix assertion
2020-04-10 17:19:48 +02:00
Nguyễn Hồng Quân
c9437e714a [now-python] Fix missing sys modules for relative module import (#4024)
Without this fix, Python app which uses [Pydantic](https://pydantic-docs.helpmanual.io/usage/postponed_annotations/) for data validation will crash with log like this:

```py
  File "pydantic/main.py", line 175, in pydantic.main.ModelMetaclass.__new__
    annotations = resolve_annotations(namespace.get('__annotations__', {}), namespace.get('__module__', None))
  File "pydantic/typing.py", line 136, in pydantic.typing.resolve_annotations
    annotations = resolve_annotations(namespace.get('__annotations__', {}), namespace.get('__module__', None))
  File "pydantic/typing.py", line 136, in pydantic.typing.resolve_annotations
    base_globals: Optional[Dict[str, Any]] = sys.modules[module_name].__dict__
KeyError: 'api.main'
```

The added code also follow Python official documentation: https://docs.python.org/3.6/library/importlib.html#importing-a-source-file-directly
2020-04-10 09:53:24 -04:00
Steven
c0aff5cf4a [now-cli] Clean up tests for now dev (#4038)
This PR does a few things:

- Change `dev.unref()` when possible and use `testFixtureStdio` instead
- Replace `fetch()` with `fetchWithRetry()` when possible
- Remove commented-out tests
2020-04-09 23:30:22 +00:00
Allen Hai
9110b14bb4 Publish Canary
- now@17.1.2-canary.12
2020-04-09 17:07:06 -05:00
Steven
40e4b69267 [now-cli] Fix now dev routing for continue: true (#4033)
There was a bug preventing `continue: true` from working between (null => filesystem) phases.

This PR fixes that bug and adds a test to ensure users can rewrite to dynamic path segments.
2020-04-09 21:35:13 +00:00
Ana Trajkovska
6c4934dcc4 Publish Canary
- @now/frameworks@0.0.13-canary.1
 - now@17.1.2-canary.11
 - @now/static-build@0.15.2-canary.4
2020-04-09 22:26:22 +02:00
Ana Trajkovska
d64e54d61a Implement pagination for now ls (#4027) 2020-04-09 22:24:25 +02:00
Allen Hai
0b75bf07f7 [examples][now-static-build] Add Ionic Angular framework (#3970)
This PR adds framework config for Ionic Angular and also updates a mistake found in the dev script of Ionic React, which mistakenly was using the same dev script as Stencil.
2020-04-09 18:44:46 +00:00
Ana Trajkovska
a861a8f3f7 Publish Canary
- now@17.1.2-canary.10
2020-04-09 16:45:12 +02:00
Ana Trajkovska
6407b17b7f Improve pagination message (#4026) 2020-04-09 16:43:26 +02:00
Steven
558463a988 Publish Canary
- now@17.1.2-canary.9
2020-04-08 08:34:50 -04:00
Steven
1a133995b8 [now-cli] Add now env command (#3999)
This PR adds a new command `now env` for managing environment variables for a given project. This will complement the changes in the Dashboard and will provide 3 possible environments: `production`, `preview`, and `development`.

- [x] Add `now env ls`
- [x] Add `now env add`
- [x] Add `now env rm`
- [x] Add `now env pull`
- [x] Add  test for `now env ls`
- [x] Add  test for `now env add`
- [x] Add  test for `now env rm -y`
- [x] Add  test for `now env pull -y`
- [x] Add  test for `now env add FOO preview < secret.txt`
- [x] Add test deployment to verify env var is assigned to both build and runtime
- [x] Test on Windows


```
  ▲ now env [options] <command>

  Commands:

    ls      [environment]              List all variables for the specified environment
    add     [name] [environment]       Add an environment variable (see examples below)
    rm      [name] [environment]       Remove an environment variable (see examples below)
    pull    [filename]                 Read development environment from the cloud and write to a file [.env]

  Options:

    -h, --help                     Output usage information
    -A FILE, --local-config=FILE   Path to the local `now.json` file
    -Q DIR, --global-config=DIR    Path to the global `.now` directory
    -d, --debug                    Debug mode [off]
    -t TOKEN, --token=TOKEN        Login token

  Examples:

  – Add a new variable to multiple environments

      $ now env add <name>
      $ now env add API_TOKEN

  – Add a new variable for a specific environment

      $ now env add <name> <production | preview | development>
      $ now env add DB_CONNECTION production

  – Add a new environment variable from stdin

      $ cat <file> | now env add <name> <production | preview | development>
      $ cat ~/.npmrc | now env add NPM_RC preview
      $ now env add DB_PASS production < secret.txt

  – Remove a variable from multiple environments

      $ now env rm <name>
      $ now env rm API_TOKEN

  – Remove a variable from a specific environment

      $ now env rm <name> <production | preview | development>
      $ now env rm NPM_RC preview
```
2020-04-07 22:49:48 +00:00
Steven
a83eecf674 Publish Canary
- @now/routing-utils@1.8.1-canary.1
2020-04-06 18:42:01 -04:00
Steven
a932ed65fa [now-routing-utils] Catch error from compile route (#4019)
Some inputs cause `compile()` to throw but its not clear which route caused it to fail.

> TypeError: Unexpected MODIFIER at 29, expected END [see source](https://sentry.io/organizations/zeithq/issues/1593291118/?project=1351065)

This PR adds logs so we can see the bad user input and correct accordingly.
2020-04-06 22:40:30 +00:00
JJ Kasper
e2ae497762 Publish Stable
- @now/next@2.5.2
2020-04-06 15:35:12 -05:00
313 changed files with 16928 additions and 470 deletions

3
.gitattributes vendored
View File

@@ -1,5 +1,8 @@
# Ignore test fixtures in GitHub Languages
# See https://github.com/github/linguist#vendored-code
examples/* linguist-vendored
utils/* linguist-vendored
test/* linguist-vendored
packages/*/test/* linguist-vendored
# Go build fails with Windows line endings.

1
.github/CODEOWNERS vendored
View File

@@ -8,6 +8,7 @@
/packages/now-cli/src/util/dev/ @tootallnate @leo @styfle @AndyBitz
/packages/now-cli/src/commands/domains/ @javivelasco @mglagola @anatrajkovska
/packages/now-cli/src/commands/certs/ @javivelasco @mglagola @anatrajkovska
/packages/now-cli/src/commands/env @styfle @lucleray
/packages/now-client @leo @rdev
/packages/now-build-utils @styfle @AndyBitz
/packages/now-node @styfle @tootallnate @lucleray

4
.gitignore vendored
View File

@@ -23,5 +23,5 @@ packages/now-cli/test/fixtures/integration
test/lib/deployment/failed-page.txt
.DS_Store
.next
.env
public
/.env
/public

View File

@@ -8,7 +8,7 @@ module.exports = {
resolve: `gatsby-plugin-manifest`,
options: {
name: 'Gatsby + Node.js (TypeScript) API',
short_name: 'Gatbsy + Node.js (TypeScript)',
short_name: 'Gatsby + Node.js (TypeScript)',
start_url: '/',
icon: 'src/images/gatsby-icon.png',
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1 @@
<svg width="350" height="140" xmlns="http://www.w3.org/2000/svg" style="background:#f6f7f9"><g fill="none" fill-rule="evenodd"><path fill="#F04141" style="mix-blend-mode:multiply" d="M61.905-34.23l96.194 54.51-66.982 54.512L22 34.887z"/><circle fill="#10DC60" style="mix-blend-mode:multiply" cx="155.5" cy="135.5" r="57.5"/><path fill="#3880FF" style="mix-blend-mode:multiply" d="M208.538 9.513l84.417 15.392L223.93 93.93z"/><path fill="#FFCE00" style="mix-blend-mode:multiply" d="M268.625 106.557l46.332-26.75 46.332 26.75v53.5l-46.332 26.75-46.332-26.75z"/><circle fill="#7044FF" style="mix-blend-mode:multiply" cx="299.5" cy="9.5" r="38.5"/><rect fill="#11D3EA" style="mix-blend-mode:multiply" transform="rotate(-60 148.47 37.886)" x="143.372" y="-7.056" width="10.196" height="89.884" rx="5.098"/><path d="M-25.389 74.253l84.86 8.107c5.498.525 9.53 5.407 9.004 10.905a10 10 0 0 1-.057.477l-12.36 85.671a10.002 10.002 0 0 1-11.634 8.42l-86.351-15.226c-5.44-.959-9.07-6.145-8.112-11.584l13.851-78.551a10 10 0 0 1 10.799-8.219z" fill="#7044FF" style="mix-blend-mode:multiply"/><circle fill="#0CD1E8" style="mix-blend-mode:multiply" cx="273.5" cy="106.5" r="20.5"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Ionic App</title>
<base href="/" />
<meta
name="viewport"
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<meta name="format-detection" content="telephone=no" />
<meta name="msapplication-tap-highlight" content="no" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="shortcut icon" type="image/png" href="%PUBLIC_URL%/assets/icon/favicon.png" />
<!-- add to homescreen for ios -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="Ionic App" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@@ -0,0 +1,21 @@
{
"short_name": "Ionic App",
"name": "My Ionic App",
"icons": [
{
"src": "assets/icon/favicon.png",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "assets/icon/icon.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "maskable"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#ffffff",
"background_color": "#ffffff"
}

View File

@@ -309,6 +309,34 @@
}
}
},
{
"name": "Ionic Angular",
"slug": "ionic-angular",
"demo": "https://ionic-angular.now-examples.now.sh",
"logo": "https://raw.githubusercontent.com/zeit/now/master/packages/frameworks/logos/ionic.svg",
"tagline": "Ionic Angular allows you to build mobile PWAs with Angular and the Ionic Framework.",
"description": "An Ionic Angular site, created with the Ionic CLI.",
"website": "https://ionicframework.com",
"detectors": {
"every": [
{
"path": "package.json",
"matchContent": "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"@ionic\\/angular\":\\s*\".+?\"[^}]*}"
}
]
},
"settings": {
"buildCommand": {
"placeholder": "`npm run build` or `ng build`"
},
"devCommand": {
"value": "ng start"
},
"outputDirectory": {
"value": "www"
}
}
},
{
"name": "Angular",
"slug": "angular",
@@ -397,7 +425,7 @@
"name": "Ionic React",
"slug": "ionic-react",
"demo": "https://ionic-react.now-examples.now.sh",
"logo": "https://raw.githubusercontent.com/zeit/now/master/packages/frameworks/logos/ionic-react.svg",
"logo": "https://raw.githubusercontent.com/zeit/now/master/packages/frameworks/logos/ionic.svg",
"tagline": "Ionic React allows you to build mobile PWAs with React and the Ionic Framework.",
"description": "An Ionic React site, created with the Ionic CLI.",
"website": "https://ionicframework.com",
@@ -411,13 +439,13 @@
},
"settings": {
"buildCommand": {
"placeholder": "npm run build"
"placeholder": "`npm run build` or `react-scripts build`"
},
"devCommand": {
"value": "stencil build --dev --watch --serve --port $PORT"
"value": "react-scripts start"
},
"outputDirectory": {
"value": "public"
"value": "build"
}
}
},

View File

Before

Width:  |  Height:  |  Size: 1003 B

After

Width:  |  Height:  |  Size: 1003 B

View File

@@ -1,6 +1,6 @@
{
"name": "@now/frameworks",
"version": "0.0.13-canary.0",
"version": "0.0.13",
"main": "frameworks.json",
"license": "UNLICENSED"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@now/build-utils",
"version": "2.2.1-canary.0",
"version": "2.2.1",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.js",

View File

@@ -190,7 +190,12 @@ export async function detectBuilders(
hasNextApiFiles = true;
}
if (!fallbackEntrypoint && buildCommand && !fileName.includes('/')) {
if (
!fallbackEntrypoint &&
buildCommand &&
!fileName.includes('/') &&
fileName !== 'now.json'
) {
fallbackEntrypoint = fileName;
}
}

View File

@@ -7,6 +7,19 @@ import {
} from '../';
describe('Test `detectBuilders`', () => {
it('should never select now.json src', async () => {
const files = ['docs/index.md', 'mkdocs.yml', 'now.json'];
const { builders, errors } = await detectBuilders(files, null, {
projectSettings: {
buildCommand: 'mkdocs build',
outputDirectory: 'site',
},
});
expect(errors).toBe(null);
expect(builders).toBeDefined();
expect(builders![0].src).not.toBe('now.json');
});
it('package.json + no build', async () => {
const pkg = { dependencies: { next: '9.0.0' } };
const files = ['package.json', 'pages/index.js', 'public/index.html'];
@@ -817,6 +830,21 @@ describe('Test `detectBuilders`', () => {
describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
const featHandleMiss = true;
it('should never select now.json src', async () => {
const files = ['docs/index.md', 'mkdocs.yml', 'now.json'];
const { builders, errors } = await detectBuilders(files, null, {
featHandleMiss,
projectSettings: {
buildCommand: 'mkdocs build',
outputDirectory: 'site',
},
});
expect(errors).toBe(null);
expect(builders).toBeDefined();
expect(builders![0].src).not.toBe('now.json');
});
it('package.json + no build', async () => {
const pkg = { dependencies: { next: '9.0.0' } };
const files = ['package.json', 'pages/index.js', 'public/index.html'];

View File

@@ -21,7 +21,6 @@ describe('Test `walkParentDirs`', () => {
await walkParentDirs({ base, start, filename });
fail('Expected error');
} catch (error) {
console.log(error);
deepEqual(
(error as Error).message,
'Expected "base" to be absolute path'
@@ -36,7 +35,6 @@ describe('Test `walkParentDirs`', () => {
await walkParentDirs({ base, start, filename });
fail('Expected error');
} catch (error) {
console.log(error);
deepEqual(
(error as Error).message,
'Expected "start" to be absolute path'

View File

@@ -1,6 +1,6 @@
{
"name": "now",
"version": "17.1.2-canary.8",
"version": "18.0.1-canary.1",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Now",
@@ -13,7 +13,7 @@
"scripts": {
"preinstall": "node ./scripts/preinstall.js",
"test-unit": "nyc ava test/unit.js test/dev-builder.unit.js test/dev-router.unit.js test/dev-server.unit.js --serial --fail-fast --verbose",
"test-integration-cli": "ava test/integration.js --serial --fail-fast",
"test-integration-cli": "ava test/integration.js --serial --fail-fast --verbose",
"test-integration-v1": "ava test/integration-v1.js --serial --fail-fast",
"test-integration-dev": "ava test/dev/integration.js --serial --fail-fast --verbose",
"prepublishOnly": "yarn build",
@@ -63,7 +63,7 @@
},
"devDependencies": {
"@sentry/node": "5.5.0",
"@sindresorhus/slugify": "0.10.0",
"@sindresorhus/slugify": "0.11.0",
"@types/ansi-escapes": "3.0.0",
"@types/ansi-regex": "4.0.0",
"@types/async-retry": "1.2.1",
@@ -143,6 +143,7 @@
"minimatch": "3.0.4",
"mri": "1.1.5",
"ms": "2.1.2",
"nanoid": "3.0.2",
"node-fetch": "2.6.0",
"npm-package-arg": "6.1.0",
"nyc": "13.2.0",

View File

@@ -8,6 +8,8 @@ import getAliases from '../../util/alias/get-aliases';
import getScope from '../../util/get-scope.ts';
import stamp from '../../util/output/stamp.ts';
import strlen from '../../util/strlen.ts';
import getCommandFlags from '../../util/get-command-flags';
import cmd from '../../util/output/cmd.ts';
export default async function ls(ctx, opts, args, output) {
const {
@@ -101,9 +103,12 @@ export default async function ls(ctx, opts, args, output) {
console.log(printAliasTable(aliases));
}
if (pagination && aliases.length === 20) {
if (pagination && pagination.count === 20) {
const flags = getCommandFlags(opts, ['_', '--next']);
output.log(
`To display the next page use the flag --next ${pagination.next}`
`To display the next page run ${cmd(
`now alias ls${flags} --next ${pagination.next}`
)}`
);
}

View File

@@ -14,6 +14,7 @@ export const latestHelp = () => `
'(default)'
)}
dev Start a local development server
env Manages the Environment Variables for your current Project
init [example] Initialize an example project
ls | list [app] Lists deployments
inspect [id] Displays information related to a deployment
@@ -28,7 +29,7 @@ export const latestHelp = () => `
domains [name] Manages your domain names
dns [name] Manages your DNS records
certs [cmd] Manages your SSL certificates
secrets [name] Manages your secret environment variables
secrets [name] Manages your global Secrets, for use in Environment Variables
logs [url] Displays the logs for a deployment
teams Manages your teams
whoami Shows the username of the currently logged in user
@@ -84,7 +85,7 @@ export const latestHelp = () => `
${chalk.cyan('$ now /usr/src/project')}
${chalk.gray('')} Deploy with environment variables
${chalk.gray('')} Deploy with Environment Variables
${chalk.cyan('$ now -e NODE_ENV=production -e SECRET=@mysql-secret')}

View File

@@ -37,7 +37,7 @@ export default async function dev(
// retrieve dev command
const [link, frameworks] = await Promise.all([
getLinkedProject(output, client, cwd),
getFrameworks(),
getFrameworks(client),
]);
if (link.status === 'error') {

View File

@@ -9,6 +9,8 @@ import stamp from '../../util/output/stamp';
import strlen from '../../util/strlen';
import { Output } from '../../util/output';
import { Domain, NowContext } from '../../types';
import getCommandFlags from '../../util/get-command-flags';
import cmd from '../../util/output/cmd';
type Options = {
'--debug': boolean;
@@ -68,9 +70,12 @@ export default async function ls(
console.log(`${formatDomainsTable(domains)}\n`);
}
if (pagination && domains.length === 20) {
if (pagination && pagination.count === 20) {
const flags = getCommandFlags(opts, ['_', '--next']);
output.log(
`To display the next page use the flag --next ${pagination.next}`
`To display the next page run ${cmd(
`now domains ls${flags} --next ${pagination.next}`
)}`
);
}

172
packages/now-cli/src/commands/env/add.ts vendored Normal file
View File

@@ -0,0 +1,172 @@
import chalk from 'chalk';
import inquirer from 'inquirer';
import { NowContext, ProjectEnvTarget } from '../../types';
import { Output } from '../../util/output';
import Client from '../../util/client';
import stamp from '../../util/output/stamp';
import { getLinkedProject } from '../../util/projects/link';
import addEnvRecord from '../../util/env/add-env-record';
import getEnvVariables from '../../util/env/get-env-records';
import {
isValidEnvTarget,
getEnvTargetPlaceholder,
getEnvTargetChoices,
} from '../../util/env/env-target';
import readStandardInput from '../../util/input/read-standard-input';
import cmd from '../../util/output/cmd';
import param from '../../util/output/param';
import withSpinner from '../../util/with-spinner';
import { emoji, prependEmoji } from '../../util/emoji';
import { isKnownError } from '../../util/env/known-error';
type Options = {
'--debug': boolean;
};
export default async function add(
ctx: NowContext,
opts: Options,
args: string[],
output: Output
) {
const {
authConfig: { token },
config,
} = ctx;
const { currentTeam } = config;
const { apiUrl } = ctx;
const debug = opts['--debug'];
const client = new Client({ apiUrl, token, currentTeam, debug });
const link = await getLinkedProject(output, client);
if (link.status === 'error') {
return link.exitCode;
} else if (link.status === 'not_linked') {
output.print(
`${chalk.red(
'Error!'
)} Your codebase isnt linked to a project on ZEIT Now. Run ${cmd(
'now'
)} to link it.\n`
);
return 1;
} else {
const { project } = link;
const stdInput = await readStandardInput();
let [envName, envTarget] = args;
if (args.length > 2) {
output.error(
`Invalid number of arguments. Usage: ${cmd(
`now env add <name> ${getEnvTargetPlaceholder()}`
)}`
);
return 1;
}
if (stdInput && (!envName || !envTarget)) {
output.error(
`Invalid number of arguments. Usage: ${cmd(
`now env add <name> <target> < <file>`
)}`
);
return 1;
}
let envTargets: ProjectEnvTarget[] = [];
if (envTarget) {
if (!isValidEnvTarget(envTarget)) {
output.error(
`The Environment ${param(
envTarget
)} is invalid. It must be one of: ${getEnvTargetPlaceholder()}.`
);
return 1;
}
envTargets.push(envTarget);
}
while (!envName) {
const { inputName } = await inquirer.prompt({
type: 'input',
name: 'inputName',
message: `Whats the name of the variable?`,
});
envName = inputName;
if (!inputName) {
output.error('Name cannot be empty');
}
}
const envs = await getEnvVariables(output, client, project.id);
const existing = new Set(
envs.filter(r => r.key === envName).map(r => r.target)
);
const choices = getEnvTargetChoices().filter(c => !existing.has(c.value));
if (choices.length === 0) {
output.error(
`The variable ${param(
envName
)} has already been added to all Environments. To remove, run ${cmd(
`now env rm ${envName}`
)}.`
);
return 1;
}
let envValue: string;
if (stdInput) {
envValue = stdInput;
} else {
const { inputValue } = await inquirer.prompt({
type: 'password',
name: 'inputValue',
message: `Whats the value of ${envName}?`,
});
envValue = inputValue || '';
}
while (envTargets.length === 0) {
const { inputTargets } = await inquirer.prompt({
name: 'inputTargets',
type: 'checkbox',
message: `Add ${envName} to which Environments (select multiple)?`,
choices,
});
envTargets = inputTargets;
if (inputTargets.length === 0) {
output.error('Please select at least one Environment');
}
}
const addStamp = stamp();
try {
await withSpinner('Saving', () =>
addEnvRecord(output, client, project.id, envName, envValue, envTargets)
);
} catch (error) {
if (isKnownError(error) && error.serverMessage) {
output.error(error.serverMessage);
return 1;
}
throw error;
}
output.print(
`${prependEmoji(
`Added Environment Variable ${chalk.bold(
envName
)} to Project ${chalk.bold(project.name)} ${chalk.gray(addStamp())}`,
emoji('success')
)}\n`
);
return 0;
}
}

View File

@@ -0,0 +1,114 @@
import chalk from 'chalk';
import { NowContext } from '../../types';
import createOutput from '../../util/output';
import getArgs from '../../util/get-args';
import getSubcommand from '../../util/get-subcommand';
import getInvalidSubcommand from '../../util/get-invalid-subcommand';
import { getEnvTargetPlaceholder } from '../../util/env/env-target';
import handleError from '../../util/handle-error';
import logo from '../../util/output/logo';
import add from './add';
import pull from './pull';
import ls from './ls';
import rm from './rm';
const help = () => {
const placeholder = getEnvTargetPlaceholder();
console.log(`
${chalk.bold(`${logo} now env`)} [options] <command>
${chalk.dim('Commands:')}
ls [environment] List all variables for the specified Environment
add [name] [environment] Add an Environment Variable (see examples below)
rm [name] [environment] Remove an Environment Variable (see examples below)
pull [filename] Pull all Development Environment Variables from the cloud and write to a file [.env]
${chalk.dim('Options:')}
-h, --help Output usage information
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE'
)} Path to the local ${'`now.json`'} file
-Q ${chalk.bold.underline('DIR')}, --global-config=${chalk.bold.underline(
'DIR'
)} Path to the global ${'`.now`'} directory
-d, --debug Debug mode [off]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token
${chalk.dim('Examples:')}
${chalk.gray('')} Add a new variable to multiple Environments
${chalk.cyan('$ now env add <name>')}
${chalk.cyan('$ now env add API_TOKEN')}
${chalk.gray('')} Add a new variable for a specific Environment
${chalk.cyan(`$ now env add <name> ${placeholder}`)}
${chalk.cyan('$ now env add DB_CONNECTION production')}
${chalk.gray('')} Add a new Environment Variable from stdin
${chalk.cyan(`$ cat <file> | now env add <name> ${placeholder}`)}
${chalk.cyan('$ cat ~/.npmrc | now env add NPM_RC preview')}
${chalk.cyan('$ now env add DB_PASS production < secret.txt')}
${chalk.gray('')} Remove an variable from multiple Environments
${chalk.cyan('$ now env rm <name>')}
${chalk.cyan('$ now env rm API_TOKEN')}
${chalk.gray('')} Remove a variable from a specific Environment
${chalk.cyan(`$ now env rm <name> ${placeholder}`)}
${chalk.cyan('$ now env rm NPM_RC preview')}
`);
};
const COMMAND_CONFIG = {
ls: ['ls', 'list'],
add: ['add'],
rm: ['rm', 'remove'],
pull: ['pull'],
};
export default async function main(ctx: NowContext) {
let argv;
try {
argv = getArgs(ctx.argv.slice(2), {
'--yes': Boolean,
'-y': '--yes',
});
} catch (error) {
handleError(error);
return 1;
}
if (argv['--help']) {
help();
return 2;
}
const output = createOutput({ debug: argv['--debug'] });
const { subcommand, args } = getSubcommand(argv._.slice(1), COMMAND_CONFIG);
switch (subcommand) {
case 'ls':
return ls(ctx, argv, args, output);
case 'add':
return add(ctx, argv, args, output);
case 'rm':
return rm(ctx, argv, args, output);
case 'pull':
return pull(ctx, argv, args, output);
default:
output.error(getInvalidSubcommand(COMMAND_CONFIG));
help();
return 2;
}
}

117
packages/now-cli/src/commands/env/ls.ts vendored Normal file
View File

@@ -0,0 +1,117 @@
import chalk from 'chalk';
import ms from 'ms';
import plural from 'pluralize';
import { Output } from '../../util/output';
import { ProjectEnvVariable, ProjectEnvTarget, NowContext } from '../../types';
import Client from '../../util/client';
import formatTable from '../../util/format-table';
import getEnvVariables from '../../util/env/get-env-records';
import {
isValidEnvTarget,
getEnvTargetPlaceholder,
} from '../../util/env/env-target';
import { getLinkedProject } from '../../util/projects/link';
import stamp from '../../util/output/stamp';
import cmd from '../../util/output/cmd';
import param from '../../util/output/param';
type Options = {
'--debug': boolean;
};
export default async function ls(
ctx: NowContext,
opts: Options,
args: string[],
output: Output
) {
const {
authConfig: { token },
config,
} = ctx;
const { currentTeam } = config;
const { apiUrl } = ctx;
const debug = opts['--debug'];
const client = new Client({ apiUrl, token, currentTeam, debug });
const link = await getLinkedProject(output, client);
if (link.status === 'error') {
return link.exitCode;
} else if (link.status === 'not_linked') {
output.print(
`${chalk.red(
'Error!'
)} Your codebase isnt linked to a project on ZEIT Now. Run ${cmd(
'now'
)} to link it.\n`
);
return 1;
} else {
if (args.length > 1) {
output.error(
`Invalid number of arguments. Usage: ${cmd(
`now env ls ${getEnvTargetPlaceholder()}`
)}`
);
return 1;
}
const { project } = link;
const envTarget = args[0] as ProjectEnvTarget | undefined;
if (!isValidEnvTarget(envTarget)) {
output.error(
`The Environment ${param(
envTarget
)} is invalid. It must be one of: ${getEnvTargetPlaceholder()}.`
);
return 1;
}
const lsStamp = stamp();
const records = await getEnvVariables(
output,
client,
project.id,
envTarget
);
output.log(
`${plural(
'Environment Variable',
records.length,
true
)} found in Project ${chalk.bold(project.name)} ${chalk.gray(lsStamp())}`
);
console.log(getTable(records));
return 0;
}
}
function getTable(records: ProjectEnvVariable[]) {
return formatTable(
['name', 'value', 'environment', 'created'],
['l', 'l', 'l', 'l', 'l'],
[
{
name: '',
rows: records.map(getRow),
},
]
);
}
function getRow({
key,
system = false,
target,
createdAt = 0,
}: ProjectEnvVariable) {
const now = Date.now();
return [
chalk.bold(key),
chalk.gray(chalk.italic(system ? 'Populated by System' : 'Encrypted')),
target || '',
`${ms(now - createdAt)} ago`,
];
}

View File

@@ -0,0 +1,120 @@
import chalk from 'chalk';
import { NowContext, ProjectEnvTarget } from '../../types';
import { Output } from '../../util/output';
import promptBool from '../../util/prompt-bool';
import Client from '../../util/client';
import stamp from '../../util/output/stamp';
import getEnvVariables from '../../util/env/get-env-records';
import getDecryptedSecret from '../../util/env/get-decrypted-secret';
import { getLinkedProject } from '../../util/projects/link';
import cmd from '../../util/output/cmd';
import param from '../../util/output/param';
import withSpinner from '../../util/with-spinner';
import { join } from 'path';
import { promises, existsSync } from 'fs';
import { emoji, prependEmoji } from '../../util/emoji';
const { writeFile } = promises;
type Options = {
'--debug': boolean;
'--yes': boolean;
};
export default async function pull(
ctx: NowContext,
opts: Options,
args: string[],
output: Output
) {
const {
authConfig: { token },
config,
} = ctx;
const { currentTeam } = config;
const { apiUrl } = ctx;
const debug = opts['--debug'];
const client = new Client({ apiUrl, token, currentTeam, debug });
const link = await getLinkedProject(output, client);
if (link.status === 'error') {
return link.exitCode;
} else if (link.status === 'not_linked') {
output.print(
`${chalk.red(
'Error!'
)} Your codebase isnt linked to a project on ZEIT Now. Run ${cmd(
'now'
)} to link it.\n`
);
return 1;
} else {
if (args.length > 1) {
output.error(
`Invalid number of arguments. Usage: ${cmd('now env pull <file>')}`
);
return 1;
}
const { project } = link;
const [filename = '.env'] = args;
const fullPath = join(process.cwd(), filename);
const exists = existsSync(fullPath);
const skipConfirmation = opts['--yes'];
if (
exists &&
!skipConfirmation &&
!(await promptBool(
output,
`Found existing file ${param(filename)}. Do you want to overwrite?`
))
) {
output.log('Aborted');
return 0;
}
output.print(
`Downloading Development Environment Variables for Project ${chalk.bold(
project.name
)}\n`
);
const pullStamp = stamp();
const records = await withSpinner('Downloading', async () => {
const dev = ProjectEnvTarget.Development;
const envs = await getEnvVariables(output, client, project.id, dev);
const values = await Promise.all(
envs.map(env => getDecryptedSecret(output, client, env.value))
);
const results: { key: string; value: string }[] = [];
for (let i = 0; i < values.length; i++) {
results.push({ key: envs[i].key, value: values[i] });
}
return results;
});
const contents =
records
.map(({ key, value }) => `${key}="${escapeValue(value)}"`)
.join('\n') + '\n';
await writeFile(fullPath, contents, 'utf8');
output.print(
`${prependEmoji(
`${exists ? 'Updated' : 'Created'} ${chalk.bold(
filename
)} file ${chalk.gray(pullStamp())}`,
emoji('success')
)}\n`
);
return 0;
}
}
function escapeValue(value: string) {
return value
.replace(new RegExp('\\"', 'g'), '\\"') // escape quotes
.replace(new RegExp('\n', 'g'), '\\n') // combine newlines (unix) into one line
.replace(new RegExp('\r', 'g'), '\\r'); // combine newlines (windows) into one line
}

168
packages/now-cli/src/commands/env/rm.ts vendored Normal file
View File

@@ -0,0 +1,168 @@
import chalk from 'chalk';
import inquirer from 'inquirer';
import { NowContext, ProjectEnvTarget } from '../../types';
import { Output } from '../../util/output';
import promptBool from '../../util/prompt-bool';
import { getLinkedProject } from '../../util/projects/link';
import removeEnvRecord from '../../util/env/remove-env-record';
import getEnvVariables from '../../util/env/get-env-records';
import {
isValidEnvTarget,
getEnvTargetPlaceholder,
getEnvTargetChoices,
} from '../../util/env/env-target';
import Client from '../../util/client';
import stamp from '../../util/output/stamp';
import cmd from '../../util/output/cmd';
import param from '../../util/output/param';
import withSpinner from '../../util/with-spinner';
import { emoji, prependEmoji } from '../../util/emoji';
import { isKnownError } from '../../util/env/known-error';
type Options = {
'--debug': boolean;
'--yes': boolean;
};
export default async function rm(
ctx: NowContext,
opts: Options,
args: string[],
output: Output
) {
const {
authConfig: { token },
config,
} = ctx;
const { currentTeam } = config;
const { apiUrl } = ctx;
const debug = opts['--debug'];
const client = new Client({ apiUrl, token, currentTeam, debug });
const link = await getLinkedProject(output, client);
if (link.status === 'error') {
return link.exitCode;
} else if (link.status === 'not_linked') {
output.print(
`${chalk.red(
'Error!'
)} Your codebase isnt linked to a project on ZEIT Now. Run ${cmd(
'now'
)} to link it.\n`
);
return 1;
} else {
if (args.length > 2) {
output.error(
`Invalid number of arguments. Usage: ${cmd(
`now env rm <name> ${getEnvTargetPlaceholder()}`
)}`
);
return 1;
}
const { project } = link;
let [envName, envTarget] = args;
let envTargets: ProjectEnvTarget[] = [];
if (envTarget) {
if (!isValidEnvTarget(envTarget)) {
output.error(
`The Environment ${param(
envTarget
)} is invalid. It must be one of: ${getEnvTargetPlaceholder()}.`
);
return 1;
}
envTargets.push(envTarget);
}
while (!envName) {
const { inputName } = await inquirer.prompt({
type: 'input',
name: 'inputName',
message: `Whats the name of the variable?`,
});
if (!inputName) {
output.error(`Name cannot be empty`);
continue;
}
envName = inputName;
}
const envs = await getEnvVariables(output, client, project.id);
const existing = new Set(
envs.filter(r => r.key === envName).map(r => r.target)
);
if (existing.size === 0) {
output.error(
`The Environment Variable ${param(envName)} was not found.\n`
);
return 1;
}
if (envTargets.length === 0) {
const choices = getEnvTargetChoices().filter(c => existing.has(c.value));
if (choices.length === 0) {
output.error(
`The Environment Variable ${param(
envName
)} was found but it is not assigned to any Environments.\n`
);
return 1;
} else if (choices.length === 1) {
envTargets = [choices[0].value];
} else {
const { inputTargets } = await inquirer.prompt({
name: 'inputTargets',
type: 'checkbox',
message: `Remove ${envName} from which Environments (select multiple)?`,
choices,
});
envTargets = inputTargets;
}
}
const skipConfirmation = opts['--yes'];
if (
!skipConfirmation &&
!(await promptBool(
output,
`Removing Environment Variable ${param(
envName
)} from Project ${chalk.bold(project.name)}. Are you sure?`
))
) {
output.log('Aborted');
return 0;
}
const rmStamp = stamp();
try {
await withSpinner('Removing', async () => {
for (const target of envTargets) {
await removeEnvRecord(output, client, project.id, envName, target);
}
});
} catch (error) {
if (isKnownError(error) && error.serverMessage) {
output.error(error.serverMessage);
return 1;
}
throw error;
}
output.print(
`${prependEmoji(
`Removed Environment Variable ${chalk.gray(rmStamp())}`,
emoji('success')
)}\n`
);
return 0;
}
}

View File

@@ -13,6 +13,7 @@ export default new Map([
['domain', 'domains'],
['domains', 'domains'],
['downgrade', 'upgrade'],
['env', 'env'],
['help', 'help'],
['init', 'init'],
['inspect', 'inspect'],

View File

@@ -155,11 +155,7 @@ export default async function main(ctx) {
type === STATIC
? null
: caught(
now.fetch(
`/v1/now/deployments/${encodeURIComponent(
finalId
)}/events?types=event`
)
now.fetch(`/v1/now/deployments/${encodeURIComponent(finalId)}/events`)
),
isBuilds ? now.fetch(buildsUrl) : { builds: [] },
]);

View File

@@ -17,6 +17,7 @@ import getScope from '../util/get-scope.ts';
import toHost from '../util/to-host';
import parseMeta from '../util/parse-meta';
import { isValidName } from '../util/is-valid-name';
import getCommandFlags from '../util/get-command-flags';
const help = () => {
console.log(`
@@ -332,11 +333,16 @@ export default async function main(ctx) {
hsep: ' '.repeat(4),
stringLength: strlen,
}
).replace(/^/gm, ' ')}\n\n`
).replace(/^/gm, ' ')}\n`
);
if (pagination && deployments.length === 20) {
log(`To display the next page use the flag --next ${pagination.next}`);
if (pagination && pagination.count === 20) {
const flags = getCommandFlags(argv, ['_', '--next']);
log(
`To display the next page run ${cmd(
`now ls${app ? ' ' + app : ''}${flags} --next ${pagination.next}`
)}`
);
}
}

View File

@@ -4,7 +4,7 @@ import Now from '../util';
import createOutput from '../util/output';
import logo from '../util/output/logo';
import elapsed from '../util/output/elapsed.ts';
import { maybeURL, normalizeURL, parseInstanceURL } from '../util/url';
import { maybeURL, normalizeURL } from '../util/url';
import printEvents from '../util/events';
import Client from '../util/client.ts';
import getScope from '../util/get-scope.ts';
@@ -16,7 +16,6 @@ const help = () => {
${chalk.dim('Options:')}
-h, --help Output usage information
-a, --all Include access logs
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE'
)} Path to the local ${'`now.json`'} file
@@ -28,9 +27,6 @@ const help = () => {
-n ${chalk.bold.underline(
'NUMBER'
)} Number of logs [100]
-q ${chalk.bold.underline('QUERY')}, --query=${chalk.bold.underline(
'QUERY'
)} Search query
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token
@@ -65,23 +61,18 @@ export default async function main(ctx) {
let apiUrl;
let head;
let limit;
let query;
let follow;
let types;
let outputMode;
let since;
let until;
let instanceId;
argv = mri(ctx.argv.slice(2), {
string: ['query', 'since', 'until', 'output'],
boolean: ['help', 'all', 'debug', 'head', 'follow'],
string: ['since', 'until', 'output'],
boolean: ['help', 'debug', 'head', 'follow'],
alias: {
help: 'h',
all: 'a',
debug: 'd',
query: 'q',
follow: 'f',
output: 'o',
},
@@ -121,7 +112,7 @@ export default async function main(ctx) {
return 1;
}
[deploymentIdOrURL, instanceId] = parseInstanceURL(normalizedURL);
deploymentIdOrURL = normalizedURL;
}
debug = argv.debug;
@@ -129,10 +120,8 @@ export default async function main(ctx) {
head = argv.head;
limit = typeof argv.n === 'number' ? argv.n : 100;
query = argv.query || '';
follow = argv.f;
if (follow) until = 0;
types = argv.all ? [] : ['command', 'stdout', 'stderr', 'exit'];
outputMode = argv.output in logPrinters ? argv.output : 'short';
const {
@@ -204,9 +193,6 @@ export default async function main(ctx) {
const findOpts1 = {
direction,
limit,
query,
types,
instanceId,
since,
until,
}; // no follow
@@ -236,9 +222,6 @@ export default async function main(ctx) {
const since2 = lastEvent ? lastEvent.date : Date.now();
const findOpts2 = {
direction: 'forward',
query,
types,
instanceId,
since: since2,
follow: true,
};

View File

@@ -190,7 +190,10 @@ export default async function main(ctx) {
}
aliases = await Promise.all(
deployments.map(depl => getAliases(client, depl.uid))
deployments.map(async depl => {
const { aliases } = await getAliases(client, depl.uid);
return aliases;
})
);
} finally {
cancelWait();

View File

@@ -216,6 +216,33 @@ export type DNSRecordData =
| SRVRecordData
| MXRecordData;
export interface Secret {
uid: string;
name: string;
value: string;
teamId?: string;
userId?: string;
projectId?: string;
created: string;
createdAt: number;
}
export enum ProjectEnvTarget {
Production = 'production',
Preview = 'preview',
Development = 'development',
}
export interface ProjectEnvVariable {
key: string;
value: string;
configurationId?: string | null;
createdAt?: number;
updatedAt?: number;
target?: ProjectEnvTarget;
system?: boolean;
}
export interface Project {
id: string;
name: string;

View File

@@ -11,11 +11,8 @@ async function getEventsStream(now, idOrHost, options) {
direction: options.direction,
follow: options.follow ? '1' : '',
format: options.format || 'lines',
instanceId: options.instanceId,
limit: options.limit,
q: options.query,
since: options.since,
types: (options.types || []).join(','),
until: options.until,
})}`
);

View File

@@ -27,6 +27,8 @@ import {
BuilderOutput,
BuildResultV3,
BuilderOutputs,
BuilderParams,
EnvConfigs,
} from './types';
import { normalizeRoutes } from '@now/routing-utils';
import getUpdateCommand from '../get-update-command';
@@ -45,7 +47,7 @@ const treeKill = promisify(_treeKill);
async function createBuildProcess(
match: BuildMatch,
buildEnv: EnvConfig,
envConfigs: EnvConfigs,
workPath: string,
output: Output,
yarnPath?: string
@@ -64,7 +66,7 @@ async function createBuildProcess(
const env: EnvConfig = {
...process.env,
PATH,
...buildEnv,
...envConfigs.allEnv,
NOW_REGION: 'dev1',
};
@@ -109,7 +111,7 @@ export async function executeBuild(
builderWithPkg: { runInProcess, builder, package: pkg },
} = match;
const { entrypoint } = match;
const { env, debug, buildEnv, yarnPath, cwd: workPath } = devServer;
const { debug, envConfigs, yarnPath, cwd: workPath } = devServer;
const startTime = Date.now();
const showBuildTimestamp =
@@ -131,14 +133,14 @@ export async function executeBuild(
devServer.output.debug(`Creating build process for ${entrypoint}`);
buildProcess = await createBuildProcess(
match,
buildEnv,
envConfigs,
workPath,
devServer.output,
yarnPath
);
}
const buildParams = {
const buildParams: BuilderParams = {
files,
entrypoint,
workPath,
@@ -148,8 +150,10 @@ export async function executeBuild(
requestPath,
filesChanged,
filesRemoved,
env,
buildEnv,
// This env distiniction is only necessary to maintain
// backwards compatibility with the `@now/next` builder.
env: envConfigs.runEnv,
buildEnv: envConfigs.buildEnv,
},
};
@@ -361,7 +365,7 @@ export async function executeBuild(
Variables: {
...nowConfig.env,
...asset.environment,
...env,
...envConfigs.runEnv,
NOW_REGION: 'dev1',
},
},

View File

@@ -54,16 +54,18 @@ export async function devRouter(
missRoutes?: RouteConfig[],
phase?: HandleValue | null
): Promise<RouteResult> {
let found: RouteResult | undefined;
let result: RouteResult | undefined;
let { query, pathname: reqPathname = '/' } = url.parse(reqUrl, true);
const combinedHeaders: HttpHeadersConfig = { ...previousHeaders };
let status: number | undefined;
let isContinue = false;
// Try route match
if (routes) {
let idx = -1;
for (const routeConfig of routes) {
idx++;
isContinue = false;
if (isHandler(routeConfig)) {
// We don't expect any Handle, only Source routes
@@ -110,6 +112,7 @@ export async function devRouter(
status = routeConfig.status;
}
reqPathname = destPath;
isContinue = true;
continue;
}
@@ -149,9 +152,10 @@ export async function devRouter(
const isDestUrl = isURL(destPath);
if (isDestUrl) {
found = {
result = {
found: true,
dest: destPath,
continue: isContinue,
userDest: false,
isDestUrl,
status: routeConfig.status || status,
@@ -167,9 +171,10 @@ export async function devRouter(
destPath = `/${destPath}`;
}
const { pathname, query } = url.parse(destPath, true);
found = {
result = {
found: true,
dest: pathname || '/',
continue: isContinue,
userDest: Boolean(routeConfig.dest),
isDestUrl,
status: routeConfig.status || status,
@@ -185,10 +190,11 @@ export async function devRouter(
}
}
if (!found) {
found = {
if (!result) {
result = {
found: false,
dest: reqPathname,
continue: isContinue,
status,
isDestUrl: false,
uri_args: query,
@@ -197,5 +203,5 @@ export async function devRouter(
};
}
return found;
return result;
}

View File

@@ -85,6 +85,8 @@ import {
ListenSpec,
RouteConfig,
RouteResult,
HttpHeadersConfig,
EnvConfigs,
} from './types';
interface FSEvent {
@@ -108,8 +110,7 @@ export default class DevServer {
public cwd: string;
public debug: boolean;
public output: Output;
public env: EnvConfig;
public buildEnv: EnvConfig;
public envConfigs: EnvConfigs;
public frameworkSlug: string | null;
public files: BuilderInputs;
public yarnPath: string;
@@ -141,8 +142,7 @@ export default class DevServer {
this.cwd = cwd;
this.debug = options.debug;
this.output = options.output;
this.env = {};
this.buildEnv = {};
this.envConfigs = { buildEnv: {}, runEnv: {}, allEnv: {} };
this.files = {};
this.address = '';
this.devCommand = options.devCommand;
@@ -739,13 +739,13 @@ export default class DevServer {
// Retrieve the path of the native module
const nowConfig = await this.getNowConfig(false);
const nowConfigBuild = nowConfig.build || {};
const [env, buildEnv] = await Promise.all([
const [runEnv, buildEnv] = await Promise.all([
this.getLocalEnv('.env', nowConfig.env),
this.getLocalEnv('.env.build', nowConfigBuild.env),
]);
Object.assign(process.env, buildEnv);
this.env = env;
this.buildEnv = buildEnv;
const allEnv = { ...buildEnv, ...runEnv };
Object.assign(process.env, allEnv);
this.envConfigs = { buildEnv, runEnv, allEnv };
const opts = { output: this.output, isBuilds: true };
const files = await getFiles(this.cwd, nowConfig, opts);
@@ -1264,19 +1264,25 @@ export default class DevServer {
let routeResult: RouteResult | null = null;
let match: BuildMatch | null = null;
let statusCode: number | undefined;
let prevUrl = req.url;
let prevHeaders: HttpHeadersConfig = {};
for (const phase of phases) {
statusCode = undefined;
const phaseRoutes = handleMap.get(phase) || [];
routeResult = await devRouter(
req.url,
prevUrl,
req.method,
phaseRoutes,
this,
undefined,
prevHeaders,
missRoutes,
phase
);
prevUrl =
routeResult.continue && routeResult.dest ? routeResult.dest : req.url;
prevHeaders =
routeResult.continue && routeResult.headers ? routeResult.headers : {};
if (routeResult.isDestUrl) {
// Mix the `routes` result dest query params into the req path
@@ -1450,19 +1456,24 @@ export default class DevServer {
foundAsset = findAsset(match, requestPath, nowConfig);
}
if (!foundAsset) {
// if the dev command is started, proxy to it
if (this.devProcessPort) {
this.output.debug('Proxy to dev command server');
return proxyPass(
req,
res,
`http://localhost:${this.devProcessPort}`,
this.output,
false
);
}
// Proxy to the dev server:
// - when there is no asset
// - when the asset is not a Lambda (the dev server must take care of all static files)
if (
this.devProcessPort &&
(!foundAsset || (foundAsset && foundAsset.asset.type !== 'Lambda'))
) {
this.output.debug('Proxy to dev command server');
return proxyPass(
req,
res,
`http://localhost:${this.devProcessPort}`,
this.output,
false
);
}
if (!foundAsset) {
await this.send404(req, res, nowRequestId);
return;
}
@@ -1682,8 +1693,7 @@ export default class DevServer {
FORCE_COLOR: process.stdout.isTTY ? '1' : '0',
...(this.frameworkSlug === 'create-react-app' ? { BROWSER: 'none' } : {}),
...process.env,
...this.buildEnv,
...(this.frameworkSlug === 'nextjs' ? this.env : {}),
...this.envConfigs.allEnv,
NOW_REGION: 'dev1',
PORT: `${port}`,
};

View File

@@ -26,6 +26,23 @@ export interface EnvConfig {
[name: string]: string | undefined;
}
export interface EnvConfigs {
/**
* environment variables from `.env.build` file (deprecated)
*/
buildEnv: EnvConfig;
/**
* environment variables from `.env` file
*/
runEnv: EnvConfig;
/**
* environment variables from `.env` and `.env.build`
*/
allEnv: EnvConfig;
}
export interface BuildMatch extends BuildConfig {
entrypoint: string;
builderWithPkg: BuilderWithPackage;
@@ -147,6 +164,8 @@ export interface RouteResult {
found: boolean;
// "dest": <string of the dest, either file for lambda or full url for remote>
dest: string;
// `true` if last route in current phase matched but set `continue: true`
continue: boolean;
// "status": <integer in case exit code is intended to be changed>
status?: number;
// "headers": <object of the added response header values>

View File

@@ -19,7 +19,7 @@ export default async function addDNSRecord(
) {
try {
const record = await client.fetch<Response>(
`/v3/domains/${domain}/records`,
`/v3/domains/${encodeURIComponent(domain)}/records`,
{
body: recordData,
method: 'POST',

View File

@@ -22,12 +22,15 @@ export default async function importZonefile(
const zonefile = readFileSync(resolve(zonefilePath), 'utf8');
try {
const res = await client.fetch<Response>(`/v3/domains/${domain}/records`, {
headers: { 'Content-Type': 'text/dns' },
body: zonefile,
method: 'PUT',
json: false,
});
const res = await client.fetch<Response>(
`/v3/domains/${encodeURIComponent(domain)}/records`,
{
headers: { 'Content-Type': 'text/dns' },
body: zonefile,
method: 'PUT',
json: false,
}
);
const { recordIds } = (await res.json()) as JSONResponse;
cancelWait();

View File

@@ -22,5 +22,7 @@ type Response = {
};
export default async function checkTransfer(client: Client, name: string) {
return client.fetch<Response>(`/v4/domains/${name}/registry`);
return client.fetch<Response>(
`/v4/domains/${encodeURIComponent(name)}/registry`
);
}

View File

@@ -18,7 +18,7 @@ async function getDomainByName(
);
try {
const { domain } = await client.fetch<Response>(
`/v4/domains/${domainName}`
`/v4/domains/${encodeURIComponent(domainName)}`
);
cancelWait();
return domain;

View File

@@ -13,10 +13,13 @@ export default async function moveOutDomain(
destination: string
) {
try {
return await client.fetch<Response>(`/v4/domains/${name}`, {
body: { op: 'move-out', destination },
method: 'PATCH',
});
return await client.fetch<Response>(
`/v4/domains/${encodeURIComponent(name)}`,
{
body: { op: 'move-out', destination },
method: 'PATCH',
}
);
} catch (error) {
if (error.code === 'forbidden') {
return new ERRORS.DomainPermissionDenied(name, contextName);

View File

@@ -7,7 +7,9 @@ export default async function removeDomainByName(
domain: string
) {
try {
return await now.fetch(`/v3/domains/${domain}`, { method: 'DELETE' });
return await now.fetch(`/v3/domains/${encodeURIComponent(domain)}`, {
method: 'DELETE',
});
} catch (error) {
if (error.code === 'not_found') {
return new ERRORS.DomainNotFound(domain);

View File

@@ -0,0 +1,61 @@
import { Output } from '../output';
import Client from '../client';
import { Secret, ProjectEnvTarget, ProjectEnvVariable } from '../../types';
import { customAlphabet } from 'nanoid';
import slugify from '@sindresorhus/slugify';
export default async function addEnvRecord(
output: Output,
client: Client,
projectId: string,
envName: string,
envValue: string | undefined,
targets: ProjectEnvTarget[]
): Promise<void> {
output.debug(
`Adding Environment Variable ${envName} to ${targets.length} targets`
);
let values: string[] | undefined;
if (envValue) {
const urlSecret = `/v2/now/secrets/${encodeURIComponent(envName)}`;
const secrets = await Promise.all(
targets.map(target =>
client.fetch<Secret>(urlSecret, {
method: 'POST',
body: JSON.stringify({
name: generateSecretName(envName, target),
value: envValue,
projectId: projectId,
decryptable: target === ProjectEnvTarget.Development,
}),
})
)
);
values = secrets.map(secret => secret.uid);
}
const body = targets.map((target, i) => ({
key: envName,
value: values ? values[i] : '',
target,
}));
const urlProject = `/v4/projects/${projectId}/env`;
await client.fetch<ProjectEnvVariable>(urlProject, {
method: 'POST',
body: JSON.stringify(body),
});
}
const randomSecretSuffix = customAlphabet(
'123456789abcdefghijklmnopqrstuvwxyz',
4
);
function generateSecretName(envName: string, target: ProjectEnvTarget) {
return `${
slugify(envName).substring(0, 80) // we truncate because the max secret length is 100
}-${target}-${randomSecretSuffix()}`;
}

View File

@@ -0,0 +1,22 @@
import { ProjectEnvTarget } from '../../types';
function envTargets(): string[] {
return Object.values(ProjectEnvTarget);
}
export function getEnvTargetChoices() {
return Object.entries(ProjectEnvTarget).map(([key, value]) => ({
name: key,
value: value,
}));
}
export function isValidEnvTarget(
target?: string
): target is ProjectEnvTarget | undefined {
return typeof target === 'undefined' || envTargets().includes(target);
}
export function getEnvTargetPlaceholder() {
return `<${envTargets().join(' | ')}>`;
}

View File

@@ -0,0 +1,17 @@
import { Output } from '../output';
import Client from '../client';
import { Secret } from '../../types';
export default async function getDecryptedSecret(
output: Output,
client: Client,
secretId: string
): Promise<string> {
if (!secretId) {
return '';
}
output.debug(`Fetching decrypted secret ${secretId}`);
const url = `/v2/now/secrets/${secretId}?decrypt=true`;
const secret = await client.fetch<Secret>(url);
return secret.value;
}

View File

@@ -0,0 +1,18 @@
import { Output } from '../output';
import Client from '../client';
import { ProjectEnvVariable, ProjectEnvTarget } from '../../types';
export default async function getEnvVariables(
output: Output,
client: Client,
projectId: string,
target?: ProjectEnvTarget
): Promise<ProjectEnvVariable[]> {
output.debug(
`Fetching Environment Variables of project ${projectId} and target ${target}`
);
const qs = target ? `?target=${encodeURIComponent(target)}` : '';
const url = `/v4/projects/${projectId}/env${qs}`;
const records = await client.fetch<ProjectEnvVariable[]>(url);
return records;
}

View File

@@ -0,0 +1,13 @@
const knownErrorsCodes = new Set([
'PAYMENT_REQUIRED',
'BAD_REQUEST',
'SYSTEM_ENV_WITH_VALUE',
'RESERVED_ENV_VARIABLE',
'ENV_ALREADY_EXISTS',
'ENV_SHOULD_BE_A_SECRET',
]);
export function isKnownError(error: { code?: string }) {
const code = error && typeof error.code === 'string' ? error.code : '';
return knownErrorsCodes.has(code.toUpperCase());
}

View File

@@ -0,0 +1,38 @@
import { Output } from '../output';
import Client from '../client';
import { ProjectEnvTarget, Secret, ProjectEnvVariable } from '../../types';
export default async function removeEnvRecord(
output: Output,
client: Client,
projectId: string,
envName: string,
target?: ProjectEnvTarget
): Promise<void> {
output.debug(
`Removing Environment Variable ${envName} from target ${target}`
);
const qs = target ? `?target=${encodeURIComponent(target)}` : '';
const urlProject = `/v4/projects/${projectId}/env/${encodeURIComponent(
envName
)}${qs}`;
const env = await client.fetch<ProjectEnvVariable>(urlProject, {
method: 'DELETE',
});
if (env && env.value) {
const idOrName = env.value.startsWith('@') ? env.value.slice(1) : env.value;
const urlSecret = `/v2/now/secrets/${idOrName}`;
const secret = await client.fetch<Secret>(urlSecret);
// Since integrations add global secrets, we must only delete if the secret was
// specifically added to this project
if (secret && secret.projectId === projectId) {
await client.fetch<Secret>(urlSecret, {
method: 'DELETE',
});
}
}
}

View File

@@ -28,11 +28,8 @@ async function printEvents(
const q = qs.stringify({
direction: findOpts.direction,
limit: findOpts.limit,
q: findOpts.query,
types: (findOpts.types || []).join(','),
since: findOpts.since,
until: findOpts.until,
instanceId: findOpts.instanceId,
follow: findOpts.follow ? '1' : '',
format: 'lines',
});

View File

@@ -20,7 +20,7 @@ import strlen from './strlen';
export default function formatTable(
header: string[],
align: Array<'l' | 'r' | 'c' | '.'>,
blocks: { name: string, rows: string[][] }[],
blocks: { name: string; rows: string[][] }[],
hsep = ' '
) {
const nrCols = header.length;
@@ -50,8 +50,8 @@ export default function formatTable(
for (let j = 0; j < nrCols; j++) {
const col = `${row[j]}`;
const al = align[j] || 'l';
const pad =
padding[j] > 1 ? ' '.repeat(padding[j] * 8 - strlen(col)) : '';
const spaces = Math.max(padding[j] * 8 - strlen(col), 0);
const pad = ' '.repeat(spaces);
rows[i][j] = al === 'l' ? col + pad : pad + col;
}
}

View File

@@ -0,0 +1,20 @@
/*
This function returns the provided arguments from a command in a string format.
Example: if `argv` is { '--debug': true, '--all': true, '--scope': 'zeit' },
the output will be '--debug --all --scope zeit'.
Flags can be excluded using the `excludeFlags` param.
*/
export default function getCommandFlags(
argv: { [key: string]: any },
excludeFlags: string[] = []
) {
const flags = Object.keys(argv)
.filter(key => !excludeFlags.includes(key))
.map(
key => `${key}${typeof argv[key] !== 'boolean' ? ' ' + argv[key] : ''}`
);
return flags.length > 0 ? ` ${flags.join(' ')}` : '';
}

View File

@@ -1,14 +1,6 @@
import fetch from 'node-fetch';
import { Framework } from '@now/frameworks';
import Client from './client';
export async function getFrameworks(): Promise<Framework[]> {
const res = await fetch('https://api.zeit.co/v1/frameworks');
if (!res.ok) {
throw new Error('Could not retrieve frameworks');
}
const json: Framework[] = await res.json();
return json;
export async function getFrameworks(client: Client) {
return await client.fetch<Framework[]>('/v1/frameworks');
}

View File

@@ -0,0 +1,9 @@
type CommandConfig = {
[command: string]: string[];
};
export default function getInvalidSubcommand(config: CommandConfig) {
return `Please specify a valid subcommand: ${Object.keys(config).join(
' | '
)}`;
}

View File

@@ -6,7 +6,7 @@ import NowTeams from './teams.js';
let teams: Team[] | undefined;
export default async function getTeams(client: Client) {
export default async function getTeams(client: Client): Promise<Team[]> {
if (teams) return teams;
try {
@@ -17,8 +17,8 @@ export default async function getTeams(client: Client) {
debug: client._debug,
});
const teams = (await teamClient.ls()).teams;
return teams as Team[];
teams = (await teamClient.ls()).teams;
return teams || [];
} catch (error) {
if (error instanceof APIError && error.status === 403) {
throw new InvalidToken();

View File

@@ -388,7 +388,12 @@ export default class Now extends EventEmitter {
if (!app && !Object.keys(meta).length) {
// Get the 20 latest projects and their latest deployment
const query = new URLSearchParams({ limit: (20).toString() });
const projects = await fetchRetry(`/v2/projects/?${query}`);
if (nextTimestamp) {
query.set('until', String(nextTimestamp));
}
const { projects, pagination } = await fetchRetry(
`/v4/projects/?${query}`
);
const deployments = await Promise.all(
projects.map(async ({ id: projectId }) => {
@@ -400,7 +405,7 @@ export default class Now extends EventEmitter {
})
);
return { deployments: deployments.filter(x => x) };
return { deployments: deployments.filter(x => x), pagination };
}
const query = new URLSearchParams();

View File

@@ -0,0 +1,13 @@
export default async function readStandardInput(): Promise<string> {
return new Promise<string>(resolve => {
setTimeout(() => resolve(''), 500);
if (process.stdin.isTTY) {
// found tty so we know there is nothing piped to stdin
resolve('');
} else {
process.stdin.setEncoding('utf8');
process.stdin.once('data', resolve);
}
});
}

View File

@@ -1,8 +1,8 @@
import chalk from 'chalk';
import { Output } from './output';
async function promptBool(output: Output, message: string) {
return new Promise(resolve => {
async function promptBool(output: Output, message: string): Promise<boolean> {
return new Promise<boolean>(resolve => {
output.print(`${chalk.gray('>')} ${message} ${chalk.gray('[y/N] ')}`);
process.stdin
.on('data', d => {

View File

@@ -2,11 +2,7 @@ import Now from './index';
export default class Teams extends Now {
async create({ slug }) {
return this.retry(async (bail, attempt) => {
if (this._debug) {
console.time(`> [debug] #${attempt} POST /teams}`);
}
return this.retry(async bail => {
const res = await this._fetch(`/teams`, {
method: 'POST',
body: {
@@ -14,10 +10,6 @@ export default class Teams extends Now {
},
});
if (this._debug) {
console.timeEnd(`> [debug] #${attempt} POST /teams`);
}
if (res.status === 403) {
return bail(new Error('Unauthorized'));
}
@@ -40,11 +32,7 @@ export default class Teams extends Now {
}
async edit({ id, slug, name }) {
return this.retry(async (bail, attempt) => {
if (this._debug) {
console.time(`> [debug] #${attempt} PATCH /teams/${id}}`);
}
return this.retry(async bail => {
const payload = {};
if (name) {
payload.name = name;
@@ -58,10 +46,6 @@ export default class Teams extends Now {
body: payload,
});
if (this._debug) {
console.timeEnd(`> [debug] #${attempt} PATCH /teams/${id}`);
}
if (res.status === 403) {
return bail(new Error('Unauthorized'));
}
@@ -84,11 +68,7 @@ export default class Teams extends Now {
}
async inviteUser({ teamId, email }) {
return this.retry(async (bail, attempt) => {
if (this._debug) {
console.time(`> [debug] #${attempt} POST /teams/${teamId}/members}`);
}
return this.retry(async bail => {
const publicRes = await this._fetch(`/www/user/public?email=${email}`);
const { name, username } = await publicRes.json();
@@ -99,10 +79,6 @@ export default class Teams extends Now {
},
});
if (this._debug) {
console.timeEnd(`> [debug] #${attempt} POST /teams/${teamId}/members}`);
}
if (res.status === 403) {
return bail(new Error('Unauthorized'));
}
@@ -126,17 +102,9 @@ export default class Teams extends Now {
}
async ls() {
return this.retry(async (bail, attempt) => {
if (this._debug) {
console.time(`> [debug] #${attempt} GET /teams}`);
}
return this.retry(async bail => {
const res = await this._fetch(`/teams`);
if (this._debug) {
console.timeEnd(`> [debug] #${attempt} GET /teams`);
}
if (res.status === 403) {
const error = new Error('Unauthorized');
error.code = 'not_authorized';

View File

@@ -13,10 +13,3 @@ export const normalizeURL = u => {
return u;
};
export const parseInstanceURL = u => {
const m = /^(.+)-([a-z0-9]{24})(\.now\.sh)$/.exec(u);
const url = m ? m[1] + m[3] : u;
const instanceId = m ? m[2] : null;
return [url, instanceId];
};

View File

@@ -10,6 +10,7 @@ test('[dev-router] 301 redirection', async t => {
t.deepEqual(result, {
found: true,
dest: '/redirect',
continue: false,
status: 301,
headers: { location: 'https://zeit.co' },
uri_args: {},
@@ -28,6 +29,7 @@ test('[dev-router] captured groups', async t => {
t.deepEqual(result, {
found: true,
dest: '/endpoints/user.js',
continue: false,
status: undefined,
headers: {},
uri_args: {},
@@ -46,6 +48,7 @@ test('[dev-router] named groups', async t => {
t.deepEqual(result, {
found: true,
dest: '/user.js',
continue: false,
status: undefined,
headers: {},
uri_args: { id: '123' },
@@ -69,6 +72,7 @@ test('[dev-router] optional named groups', async t => {
t.deepEqual(result, {
found: true,
dest: '/api/functions/hello/index.js',
continue: false,
status: undefined,
headers: {},
uri_args: { name: '' },
@@ -88,6 +92,7 @@ test('[dev-router] proxy_pass', async t => {
t.deepEqual(result, {
found: true,
dest: 'https://zeit.co',
continue: false,
status: undefined,
headers: {},
uri_args: {},
@@ -109,6 +114,7 @@ test('[dev-router] methods', async t => {
t.deepEqual(result, {
found: true,
dest: '/get',
continue: false,
status: undefined,
headers: {},
uri_args: {},
@@ -123,6 +129,7 @@ test('[dev-router] methods', async t => {
t.deepEqual(result, {
found: true,
dest: '/post',
continue: false,
status: undefined,
headers: {},
uri_args: {},
@@ -141,6 +148,7 @@ test('[dev-router] match without prefix slash', async t => {
t.deepEqual(result, {
found: true,
dest: '/endpoints/user.js',
continue: false,
status: undefined,
headers: {},
uri_args: {},
@@ -164,6 +172,7 @@ test('[dev-router] match with needed prefixed slash', async t => {
t.deepEqual(result, {
found: true,
dest: '/some/dest',
continue: false,
userDest: true,
isDestUrl: false,
phase: undefined,
@@ -197,6 +206,7 @@ test('[dev-router] `continue: true` with fallthrough', async t => {
t.deepEqual(result, {
found: false,
dest: '/_next/static/chunks/0.js',
continue: true,
isDestUrl: false,
phase: undefined,
status: undefined,
@@ -230,6 +240,7 @@ test('[dev-router] `continue: true` with match', async t => {
t.deepEqual(result, {
found: true,
dest: '/hi',
continue: false,
status: undefined,
userDest: true,
isDestUrl: false,
@@ -253,6 +264,7 @@ test('[dev-router] match with catch-all with prefix slash', async t => {
t.deepEqual(result, {
found: true,
dest: '/www/',
continue: false,
userDest: true,
isDestUrl: false,
phase: undefined,
@@ -271,6 +283,7 @@ test('[dev-router] match with catch-all with no prefix slash', async t => {
t.deepEqual(result, {
found: true,
dest: '/www/',
continue: false,
userDest: true,
isDestUrl: false,
phase: undefined,
@@ -295,6 +308,7 @@ test('[dev-router] `continue: true` with `dest`', async t => {
t.deepEqual(result, {
found: true,
dest: 'http://localhost:5000/a/foo',
continue: false,
status: undefined,
headers: {},
uri_args: {},

View File

@@ -1 +1,2 @@
SKIP_PREFLIGHT_CHECK=true
BROWSER=none

View File

@@ -19,7 +19,7 @@
"url": "https://github.com/marko-js-samples/marko-starter-demo"
},
"scripts": {
"build": "NODE_ENV=production marko-starter build && mv dist public",
"build": "NODE_ENV=production marko-starter build && rm -rf public && mv dist public",
"format": "prettier src/**/*.{js,css,less} --write && marko-prettyprint src",
"lint": "eslint src",
"serve": "NODE_ENV=production marko-starter serve-static",

View File

@@ -0,0 +1 @@
BROWSER=none

View File

@@ -0,0 +1 @@
FOO="build-and-runtime"

View File

@@ -0,0 +1,2 @@
public
.now

View File

@@ -0,0 +1 @@
module.exports = (_req, res) => res.end(process.env.FOO);

View File

@@ -0,0 +1,6 @@
{
"private": true,
"scripts": {
"build": "mkdir public && echo $FOO > public/index.html"
}
}

View File

@@ -0,0 +1 @@
module.exports = (req, res) => res.json(req.query)

View File

@@ -0,0 +1 @@
Index Page should not be served

View File

@@ -0,0 +1,13 @@
{
"routes": [
{
"src": "/(.*)",
"dest": "/api/$1",
"headers": {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "*"
},
"continue": true
}
]
}

View File

@@ -137,7 +137,7 @@ async function getPackedBuilderPath(builderDirName) {
}
async function testPath(t, port, status, path, expectedText, headers = {}) {
const opts = { redirect: 'manual' };
const opts = { redirect: 'manual-dont-change' };
const res = await fetch(`http://localhost:${port}${path}`, opts);
const msg = `Testing path ${path}`;
t.is(res.status, status, msg);
@@ -146,9 +146,15 @@ async function testPath(t, port, status, path, expectedText, headers = {}) {
t.is(actualText.trim(), expectedText.trim(), msg);
}
if (headers) {
Object.keys(headers).forEach(key => {
const k = key.toLowerCase();
t.is(headers[k], res.headers[k], msg);
Object.entries(headers).forEach(([key, expectedValue]) => {
let actualValue = res.headers.get(key);
if (key.toLowerCase() === 'location' && actualValue === '//') {
// HACK: `node-fetch` has strang behavior for location header so fix it
// with `manual-dont-change` opt and convert double slash to single.
// See https://github.com/node-fetch/node-fetch/issues/417#issuecomment-587233352
actualValue = '/';
}
t.is(actualValue, expectedValue, msg);
});
}
}
@@ -468,6 +474,19 @@ test(
})
);
test(
'[now dev] should allow user rewrites for path segment files',
testFixtureStdio('test-zero-config-rewrite', async (t, port, testPath) => {
await testPath(404, '/');
await testPath(200, '/echo/1', '{"id":"1"}', {
'Access-Control-Allow-Origin': '*',
});
await testPath(200, '/echo/2', '{"id":"2"}', {
'Access-Control-Allow-Headers': '*',
});
})
);
test('[now dev] validate builds', async t => {
const directory = fixture('invalid-builds');
const output = await exec(directory);
@@ -717,18 +736,15 @@ test(
})
);
test('[now dev] 01-node', async t => {
const tester = testFixtureStdio('01-node', async (t, port) => {
const response = await fetch(`http://localhost:${port}`);
test(
'[now dev] 01-node',
testFixtureStdio('01-node', async (t, port) => {
const response = await fetchWithRetry(`http://localhost:${port}`);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /A simple deployment with the Now API!/gm);
});
await tester(t);
});
})
);
// Angular has `engines: { node: "10.x" }` in its `package.json`
test('[now dev] 02-angular-node', async t => {
@@ -768,26 +784,21 @@ test('[now dev] 02-angular-node', async t => {
}
});
test('[now dev] 03-aurelia', async t => {
const tester = testFixtureStdio('03-aurelia', async (t, port) => {
const response = await fetch(`http://localhost:${port}`);
test(
'[now dev] 03-aurelia',
testFixtureStdio('03-aurelia', async (t, port) => {
const response = await fetchWithRetry(`http://localhost:${port}`);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /Aurelia Navigation Skeleton/gm);
});
await tester(t);
});
})
);
test(
'[now dev] 04-create-react-app',
testFixtureStdio('04-create-react-app', async (t, port) => {
const response = await fetch(`http://localhost:${port}`);
const response = await fetchWithRetry(`http://localhost:${port}`);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /React App/gm);
})
@@ -797,10 +808,8 @@ test('[now dev] 05-gatsby', async t => {
if (shouldSkip(t, '05-gatsby', '>^6.14.0 || ^8.10.0 || >=9.10.0')) return;
const tester = testFixtureStdio('05-gatsby', async (t, port) => {
const response = await fetch(`http://localhost:${port}`);
const response = await fetchWithRetry(`http://localhost:${port}`);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /Gatsby Default Starter/gm);
});
@@ -808,24 +817,20 @@ test('[now dev] 05-gatsby', async t => {
await tester(t);
});
test('[now dev] 06-gridsome', async t => {
const tester = testFixtureStdio('06-gridsome', async (t, port) => {
const response = await fetch(`http://localhost:${port}`);
test(
'[now dev] 06-gridsome',
testFixtureStdio('06-gridsome', async (t, port) => {
const response = await fetchWithRetry(`http://localhost:${port}`);
validateResponseHeaders(t, response);
t.is(response.status, 200, await response.text());
});
await tester(t);
});
})
);
test(
'[now dev] 07-hexo-node',
testFixtureStdio('07-hexo-node', async (t, port) => {
const response = await fetchWithRetry(`http://localhost:${port}`, 180);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /Hexo \+ Node.js API/gm);
})
@@ -834,175 +839,77 @@ test(
test(
'[now dev] 08-hugo',
testFixtureStdio('08-hugo', async (t, port) => {
const response = await fetch(`http://localhost:${port}`);
const response = await fetchWithRetry(`http://localhost:${port}`);
validateResponseHeaders(t, response);
// const body = await response.text();
// t.regex(body, /Hugo on ZEIT Now/gm);
t.is(response.status, 200, await response.text());
})
);
test('[now dev] 10-nextjs-node', async t => {
const tester = testFixtureStdio('10-nextjs-node', async (t, port) => {
const response = await fetch(`http://localhost:${port}`);
test(
'[now dev] 10-nextjs-node',
testFixtureStdio('10-nextjs-node', async (t, port) => {
const response = await fetchWithRetry(`http://localhost:${port}`);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /Next.js \+ Node.js API/gm);
});
await tester(t);
});
// test('[now dev] 11-nuxtjs-node', async t => {
// const directory = fixture('11-nuxtjs-node');
// const { dev, port } = await testFixture(directory);
// try {
// // start `now dev` detached in child_process
// dev.unref();
// const response = await fetchWithRetry(`http://localhost:${port}`, 180);
// validateResponseHeaders(t, response);
// const body = await response.text();
// t.regex(body, /Nuxt.js \+ Node.js API/gm);
// } finally {
// dev.kill('SIGTERM')
// }
// });
test('[now dev] 12-polymer-node', async t => {
const directory = fixture('12-polymer-node');
const { dev, port } = await testFixture(directory);
try {
// start `now dev` detached in child_process
dev.unref();
const response = await fetchWithRetry(`http://localhost:${port}`, 180);
})
);
test(
'[now dev] 12-polymer-node',
testFixtureStdio('12-polymer-node', async (t, port) => {
const response = await fetchWithRetry(`http://localhost:${port}`);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /Polymer \+ Node.js API/gm);
} finally {
await dev.kill('SIGTERM');
}
});
test('[now dev] 13-preact-node', async t => {
const directory = fixture('13-preact-node');
const { dev, port } = await testFixture(directory);
try {
// start `now dev` detached in child_process
dev.unref();
const response = await fetchWithRetry(`http://localhost:${port}`, 180);
})
);
test(
'[now dev] 13-preact-node',
testFixtureStdio('13-preact-node', async (t, port) => {
const response = await fetchWithRetry(`http://localhost:${port}`);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /Preact \+ Node.js API/gm);
} finally {
await dev.kill('SIGTERM');
}
});
test('[now dev] 14-svelte-node', async t => {
const directory = fixture('14-svelte-node');
const { dev, port } = await testFixture(directory);
try {
// start `now dev` detached in child_process
dev.unref();
const response = await fetchWithRetry(`http://localhost:${port}`, 80);
})
);
test(
'[now dev] 14-svelte-node',
testFixtureStdio('14-svelte-node', async (t, port) => {
const response = await fetchWithRetry(`http://localhost:${port}`);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /Svelte \+ Node.js API/gm);
} finally {
await dev.kill('SIGTERM');
}
});
// test('[now dev] 15-umijs-node', async t => {
// const directory = fixture('15-umijs-node');
// const { dev, port } = await testFixture(directory);
// try {
// // start `now dev` detached in child_process
// dev.unref();
// const response = await fetchWithRetry(`http://localhost:${port}`, 80);
// validateResponseHeaders(t, response);
// const body = await response.text();
// t.regex(body, /UmiJS \+ Node.js API/gm);
// } finally {
// dev.kill('SIGTERM')
// }
// });
test('[now dev] 16-vue-node', async t => {
const directory = fixture('16-vue-node');
const { dev, port } = await testFixture(directory);
try {
// start `now dev` detached in child_process
dev.unref();
const response = await fetchWithRetry(`http://localhost:${port}`, 180);
})
);
test(
'[now dev] 16-vue-node',
testFixtureStdio('16-vue-node', async (t, port) => {
const response = await fetchWithRetry(`http://localhost:${port}`);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /Vue.js \+ Node.js API/gm);
} finally {
await dev.kill('SIGTERM');
}
});
test('[now dev] 17-vuepress-node', async t => {
const directory = fixture('17-vuepress-node');
const { dev, port } = await testFixture(directory);
try {
// start `now dev` detached in child_process
dev.unref();
const response = await fetchWithRetry(`http://localhost:${port}`, 180);
})
);
test(
'[now dev] 17-vuepress-node',
testFixtureStdio('17-vuepress-node', async (t, port) => {
const response = await fetchWithRetry(`http://localhost:${port}`);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /VuePress \+ Node.js API/gm);
} finally {
await dev.kill('SIGTERM');
}
});
test('[now dev] double slashes redirect', async t => {
const directory = fixture('01-node');
const { dev, port } = await testFixture(directory);
try {
// start `now dev` detached in child_process
dev.unref();
// Wait for `now dev` to boot up
await sleep(ms('10s'));
})
);
test(
'[now dev] double slashes redirect',
testFixtureStdio('01-node', async (t, port) => {
{
const res = await fetch(`http://localhost:${port}////?foo=bar`, {
redirect: 'manual',
@@ -1041,31 +948,24 @@ test('[now dev] double slashes redirect', async t => {
body.startsWith('December')
);
}
} finally {
await dev.kill('SIGTERM');
}
});
test('[now dev] 18-marko', async t => {
const tester = testFixtureStdio('18-marko', async (t, port) => {
const response = await fetch(`http://localhost:${port}`);
})
);
test(
'[now dev] 18-marko',
testFixtureStdio('18-marko', async (t, port) => {
const response = await fetchWithRetry(`http://localhost:${port}`);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /Marko Starter/gm);
});
await tester(t);
});
})
);
test(
'[now dev] 19-mithril',
testFixtureStdio('19-mithril', async (t, port) => {
const response = await fetch(`http://localhost:${port}`);
const response = await fetchWithRetry(`http://localhost:${port}`);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /Mithril on ZEIT Now/gm);
})
@@ -1074,52 +974,42 @@ test(
test(
'[now dev] 20-riot',
testFixtureStdio('20-riot', async (t, port) => {
const response = await fetch(`http://localhost:${port}`);
const response = await fetchWithRetry(`http://localhost:${port}`);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /Riot on ZEIT Now/gm);
})
);
test('[now dev] 21-charge', async t => {
const tester = testFixtureStdio('21-charge', async (t, port) => {
const response = await fetch(`http://localhost:${port}`);
test(
'[now dev] 21-charge',
testFixtureStdio('21-charge', async (t, port) => {
const response = await fetchWithRetry(`http://localhost:${port}`);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /Welcome to my new Charge site/gm);
});
await tester(t);
});
})
);
test(
'[now dev] 22-brunch',
testFixtureStdio('22-brunch', async (t, port) => {
const response = await fetchWithRetry(`http://localhost:${port}`, 50);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /Bon Appétit./gm);
})
);
test('[now dev] 23-docusaurus', async t => {
const tester = testFixtureStdio('23-docusaurus', async (t, port) => {
const response = await fetch(`http://localhost:${port}`);
test(
'[now dev] 23-docusaurus',
testFixtureStdio('23-docusaurus', async (t, port) => {
const response = await fetchWithRetry(`http://localhost:${port}`);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /My Site/gm);
});
await tester(t);
});
})
);
test('[now dev] 24-ember', async t => {
if (shouldSkip(t, '24-ember', '>^6.14.0 || ^8.10.0 || >=9.10.0')) return;
@@ -1136,16 +1026,12 @@ test('[now dev] 24-ember', async t => {
tester(t);
});
test('[now dev] temporary directory listing', async t => {
const directory = fixture('temporary-directory-listing');
const { dev, port } = await testFixture(directory);
try {
test(
'[now dev] temporary directory listing',
testFixtureStdio('temporary-directory-listing', async (t, port) => {
const directory = fixture('temporary-directory-listing');
await fs.unlink(path.join(directory, 'index.txt')).catch(() => null);
// start `now dev` detached in child_process
dev.unref();
await sleep(ms('20s'));
const firstResponse = await fetch(`http://localhost:${port}`);
@@ -1166,10 +1052,8 @@ test('[now dev] temporary directory listing', async t => {
await sleep(ms('1s'));
}
} finally {
await dev.kill('SIGTERM');
}
});
})
);
test('[now dev] add a `package.json` to trigger `@now/static-build`', async t => {
const directory = fixture('trigger-static-build');
@@ -1240,13 +1124,9 @@ test('[now dev] no build matches warning', async t => {
}
});
test('[now dev] do not recursivly check the path', async t => {
const directory = fixture('handle-filesystem-missing');
const { dev, port } = await testFixture(directory);
try {
dev.unref();
test(
'[now dev] do not recursivly check the path',
testFixtureStdio('handle-filesystem-missing', async (t, port) => {
{
const response = await fetchWithRetry(`http://localhost:${port}`, 180);
validateResponseHeaders(t, response);
@@ -1259,10 +1139,8 @@ test('[now dev] do not recursivly check the path', async t => {
validateResponseHeaders(t, response);
t.is(response.status, 404);
}
} finally {
dev.kill('SIGTERM');
}
});
})
);
test('[now dev] render warning for empty cwd dir', async t => {
const directory = fixture('empty');
@@ -1356,24 +1234,15 @@ test('[now dev] do not rebuild for changes in the output directory', async t =>
}
});
test('[now dev] 25-nextjs-src-dir', async t => {
const directory = fixture('25-nextjs-src-dir');
const { dev, port } = await testFixture(directory);
try {
// start `now dev` detached in child_process
dev.unref();
const response = await fetchWithRetry(`http://localhost:${port}`, 80);
test(
'[now dev] 25-nextjs-src-dir',
testFixtureStdio('25-nextjs-src-dir', async (t, port) => {
const response = await fetchWithRetry(`http://localhost:${port}`, 10);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /Next.js \+ Node.js API/gm);
} finally {
dev.kill('SIGTERM');
}
});
})
);
test(
'[now dev] 26-nextjs-secrets',
@@ -1389,6 +1258,16 @@ test(
})
);
test(
'[now dev] 27-zero-config-env',
testFixtureStdio('27-zero-config-env', async (t, port) => {
const api = await fetchWithRetry(`http://localhost:${port}/api/print`);
const index = await fetchWithRetry(`http://localhost:${port}`);
t.regex(await api.text(), new RegExp('build-and-runtime'));
t.regex(await index.text(), new RegExp('build-and-runtime'));
})
);
test(
'[now dev] Use `@now/python` with Flask requirements.txt',
testFixtureStdio('python-flask', async (t, port) => {

View File

@@ -338,6 +338,16 @@ CMD ["node", "index.js"]`,
},
}),
},
'api-env': {
'api/get-env.js': 'module.exports = (_, res) => res.json(process.env)',
'print.js': 'console.log(JSON.stringify(process.env))',
'package.json': JSON.stringify({
private: true,
scripts: {
build: 'mkdir public && node print.js > public/index.json',
},
}),
},
'alias-rules': {
'rules.json': JSON.stringify({
rules: [

View File

@@ -135,8 +135,11 @@ const apiFetch = (url, { headers, ...options } = {}) => {
};
const waitForPrompt = (cp, assertion) =>
new Promise(resolve => {
new Promise((resolve, reject) => {
console.log('Waiting for prompt...');
setTimeout(() => reject(new Error('timeout in waitForPrompt')), 60000);
const listener = chunk => {
console.log('> ' + chunk);
if (assertion(chunk)) {
cp.stdout.off && cp.stdout.off('data', listener);
cp.stderr.off && cp.stderr.off('data', listener);
@@ -325,6 +328,197 @@ test('deploy using --local-config flag above target', async t => {
t.regex(host, /root-level/gm, `Expected "root-level" but received "${host}"`);
});
test('Deploy `api-env` fixture and test `now env` command', async t => {
const target = fixture('api-env');
async function nowDeploy() {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[...defaultArgs, '--confirm'],
{
reject: false,
cwd: target,
}
);
console.log({ stdout });
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
async function nowEnvLsIsEmpty() {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['env', 'ls', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
t.regex(stderr, /0 Environment Variables found in Project/gm);
}
async function nowEnvAdd() {
const now = execa(binaryPath, ['env', 'add', ...defaultArgs], {
reject: false,
cwd: target,
});
await waitForPrompt(now, chunk =>
chunk.includes('Whats the name of the variable?')
);
now.stdin.write('MY_ENV_VAR\n');
await waitForPrompt(
now,
chunk =>
chunk.includes('Whats the value of') && chunk.includes('MY_ENV_VAR')
);
now.stdin.write('MY_VALUE\n');
await waitForPrompt(
now,
chunk =>
chunk.includes('which Environments') && chunk.includes('MY_ENV_VAR')
);
now.stdin.write('a\n'); // select all
const { exitCode, stderr, stdout } = await now;
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
async function nowEnvAddFromStdin() {
const now = execa(
binaryPath,
['env', 'add', 'MY_STDIN_VAR', 'preview', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
now.stdin.end('MY_STDIN_VALUE');
const { exitCode, stderr, stdout } = await now;
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
async function nowEnvLsIncludesVar() {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['env', 'ls', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
t.regex(stderr, /4 Environment Variables found in Project/gm);
const lines = stdout.split('\n');
const myEnvVars = lines.filter(line => line.includes('MY_ENV_VAR'));
t.is(myEnvVars.length, 3);
t.regex(myEnvVars.join('\n'), /development/gm);
t.regex(myEnvVars.join('\n'), /preview/gm);
t.regex(myEnvVars.join('\n'), /production/gm);
const myStdinVars = lines.filter(line => line.includes('MY_STDIN_VAR'));
t.is(myStdinVars.length, 1);
t.regex(myStdinVars.join('\n'), /preview/gm);
}
async function nowEnvPull() {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['env', 'pull', '-y', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
t.regex(stderr, /Created .env file/gm);
const contents = fs.readFileSync(path.join(target, '.env'), 'utf8');
t.is(contents, 'MY_ENV_VAR="MY_VALUE"\n');
}
async function nowDeployWithVar() {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
[...defaultArgs],
{
reject: false,
cwd: target,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
const { host } = new URL(stdout);
const apiUrl = `https://${host}/api/get-env`;
console.log({ apiUrl });
const apiRes = await fetch(apiUrl);
t.is(apiRes.status, 200, formatOutput({ stderr, stdout }));
const apiJson = await apiRes.json();
t.is(apiJson['MY_ENV_VAR'], 'MY_VALUE');
t.is(apiJson['MY_STDIN_VAR'], 'MY_STDIN_VALUE');
const homeUrl = `https://${host}`;
console.log({ homeUrl });
const homeRes = await fetch(homeUrl);
t.is(homeRes.status, 200, formatOutput({ stderr, stdout }));
const homeJson = await homeRes.json();
t.is(homeJson['MY_ENV_VAR'], 'MY_VALUE');
t.is(homeJson['MY_STDIN_VAR'], 'MY_STDIN_VALUE');
}
async function nowEnvRemove() {
const now = execa(binaryPath, ['env', 'rm', '-y', ...defaultArgs], {
reject: false,
cwd: target,
});
await waitForPrompt(now, chunk =>
chunk.includes('Whats the name of the variable?')
);
now.stdin.write('MY_ENV_VAR\n');
await waitForPrompt(
now,
chunk =>
chunk.includes('which Environments') && chunk.includes('MY_ENV_VAR')
);
now.stdin.write('a\n'); // select all
const { exitCode, stderr, stdout } = await now;
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
async function nowEnvRemoveWithArgs() {
const { exitCode, stderr, stdout } = await execa(
binaryPath,
['env', 'rm', 'MY_STDIN_VAR', 'preview', '-y', ...defaultArgs],
{
reject: false,
cwd: target,
}
);
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
}
await nowDeploy();
await nowEnvLsIsEmpty();
await nowEnvAdd();
await nowEnvAddFromStdin();
await nowEnvLsIncludesVar();
await nowEnvPull();
await nowDeployWithVar();
await nowEnvRemove();
await nowEnvRemoveWithArgs();
await nowEnvLsIsEmpty();
});
test('print the deploy help message', async t => {
const { stderr, stdout, exitCode } = await execa(
binaryPath,
@@ -1720,6 +1914,38 @@ test('print correct link in legacy warning', async t => {
t.regex(stderr, /migrate-to-zeit-now/);
});
test('`now rm` removes a deployment', async t => {
const directory = fixture('builds');
const { stdout } = await execa(
binaryPath,
[
directory,
'--public',
'--name',
session,
...defaultArgs,
'-V',
2,
'--force',
'--confirm',
],
{
reject: false,
}
);
const { host } = new URL(stdout);
const { exitCode, stdout: stdoutRemove } = await execute([
'rm',
host,
'--yes',
]);
t.truthy(stdoutRemove.includes(host));
t.is(exitCode, 0);
});
test('`now rm` 404 exits quickly', async t => {
const start = Date.now();
const { exitCode, stderr, stdout } = await execute([

View File

@@ -1,6 +1,6 @@
{
"name": "now-client",
"version": "7.0.2-canary.2",
"version": "7.1.0",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"homepage": "https://zeit.co",

View File

@@ -1,6 +1,6 @@
{
"name": "@now/go",
"version": "1.0.6",
"version": "1.0.7",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://zeit.co/docs/runtimes#official-runtimes/go",

View File

@@ -1,6 +1,6 @@
{
"name": "@now/next",
"version": "2.5.2-canary.0",
"version": "2.5.4",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://zeit.co/docs/runtimes#official-runtimes/next-js",

View File

@@ -569,7 +569,7 @@ export const build = async ({
},
// error handling
...(output['404']
...(output[path.join('./', entryDirectory, '404')]
? [
{ handle: 'error' } as Handler,
@@ -745,10 +745,10 @@ export const build = async ({
// this can be either 404.html in latest versions
// or _errors/404.html versions while this was experimental
static404Page =
staticPages['404'] && hasPages404
? '404'
: staticPages['_errors/404']
? '_errors/404'
staticPages[path.join(entryDirectory, '404')] && hasPages404
? path.join(entryDirectory, '404')
: staticPages[path.join(entryDirectory, '_errors/404')]
? path.join(entryDirectory, '_errors/404')
: undefined;
// > 1 because _error is a lambda but isn't used if a static 404 is available
@@ -1259,17 +1259,18 @@ export const build = async ({
{
src: path.join('/', entryDirectory, '.*'),
dest: path.join(
'/',
entryDirectory,
static404Page
? static404Page
: // if static 404 is not present but we have pages/404.js
// it is a lambda due to _app getInitialProps
hasPages404 && lambdas['404']
? '404'
: '_error'
),
// if static 404 is not present but we have pages/404.js
// it is a lambda due to _app getInitialProps
dest: static404Page
? path.join('/', static404Page)
: path.join(
'/',
entryDirectory,
hasPages404 &&
lambdas[path.join('./', entryDirectory, '404')]
? '404'
: '_error'
),
status: 404,
},
]),

View File

@@ -1,6 +1,6 @@
{
"dependencies": {
"next": "9.2.3-canary.4",
"next": "latest",
"react": "^16.8.6",
"react-dom": "^16.8.6"
}

View File

@@ -0,0 +1 @@
.now

View File

@@ -0,0 +1,36 @@
{
"version": 2,
"routes": [
{ "src": "/(.*)", "dest": "/packages/webapp/$1", "continue": true }
],
"builds": [
{
"src": "packages/webapp/next.config.js",
"use": "@now/next"
}
],
"probes": [
{
"path": "/",
"mustContain": "Hi"
},
{
"path": "/",
"responseHeaders": {
"x-now-cache": "HIT"
}
},
{
"path": "/non-existent",
"mustContain": "custom 404!!"
},
{
"path": "/non-existent",
"mustContain": "__next"
},
{
"path": "/non-existent",
"status": 404
}
]
}

View File

@@ -0,0 +1,7 @@
{
"workspaces": [
"packages/*"
],
"private": true,
"name": "mono-repo"
}

View File

@@ -0,0 +1,5 @@
module.exports = {
generateBuildId() {
return 'testing-build-id';
},
};

View File

@@ -0,0 +1,9 @@
{
"name": "webapp",
"version": "0.0.1",
"dependencies": {
"next": "9.3.4",
"react": "^16.8.6",
"react-dom": "^16.8.6"
}
}

View File

@@ -0,0 +1 @@
export default () => 'custom 404!!';

View File

@@ -0,0 +1 @@
export default () => 'Hi';

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
.now

View File

@@ -0,0 +1,30 @@
{
"version": 2,
"routes": [
{ "src": "/(.*)", "dest": "/packages/webapp/$1", "continue": true }
],
"builds": [
{
"src": "packages/webapp/next.config.js",
"use": "@now/next"
}
],
"probes": [
{
"path": "/",
"mustContain": "Hi"
},
{
"path": "/non-existent",
"mustContain": "custom 404!!"
},
{
"path": "/non-existent",
"mustContain": "__next"
},
{
"path": "/non-existent",
"status": 404
}
]
}

View File

@@ -0,0 +1,7 @@
{
"workspaces": [
"packages/*"
],
"private": true,
"name": "mono-repo"
}

View File

@@ -0,0 +1,5 @@
module.exports = {
generateBuildId() {
return 'testing-build-id';
},
};

View File

@@ -0,0 +1,9 @@
{
"name": "webapp",
"version": "0.0.1",
"dependencies": {
"next": "9.3.4",
"react": "^16.8.6",
"react-dom": "^16.8.6"
}
}

View File

@@ -0,0 +1 @@
export default () => 'custom 404!!';

View File

@@ -0,0 +1,12 @@
const App = ({ Component, pageProps }) => <Component {...pageProps} />;
App.getInitialProps = async ({ ctx, Component }) => {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
return { pageProps };
};
export default App;

View File

@@ -0,0 +1 @@
export default () => 'Hi';

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@now/node",
"version": "1.5.1-canary.0",
"version": "1.5.1",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://zeit.co/docs/runtimes#official-runtimes/node-js",

View File

@@ -1,14 +1,16 @@
from http.server import BaseHTTPRequestHandler
import sys
import base64
import json
import inspect
from importlib import util
from http.server import BaseHTTPRequestHandler
# Import relative path https://stackoverflow.com/a/67692/266535
# Import relative path https://docs.python.org/3.6/library/importlib.html#importing-a-source-file-directly
__now_spec = util.spec_from_file_location("__NOW_HANDLER_MODULE_NAME", "./__NOW_HANDLER_ENTRYPOINT")
__now_module = util.module_from_spec(__now_spec)
__now_spec.loader.exec_module(__now_module)
sys.modules["__NOW_HANDLER_MODULE_NAME"] = __now_module
__now_variables = dir(__now_module)
@@ -83,7 +85,6 @@ elif 'app' in __now_variables:
not inspect.iscoroutinefunction(__now_module.app.__call__)
):
print('using Web Server Gateway Interface (WSGI)')
import sys
from urllib.parse import urlparse, unquote
from werkzeug._compat import BytesIO
from werkzeug._compat import string_types

View File

@@ -1,6 +1,6 @@
{
"name": "@now/python",
"version": "1.1.5",
"version": "1.1.6",
"main": "./dist/index.js",
"license": "MIT",
"homepage": "https://zeit.co/docs/runtimes#official-runtimes/python",

View File

@@ -1,6 +1,6 @@
{
"name": "@now/routing-utils",
"version": "1.8.1-canary.0",
"version": "1.8.1",
"description": "ZEIT Now routing utilities",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",

Some files were not shown because too many files have changed in this diff Show More