Compare commits

..

35 Commits

Author SHA1 Message Date
Steven
75aefdddd6 Publish Stable
- @now/build-utils@1.1.1
 - now@16.6.3
 - now-client@6.0.0
 - @now/next@2.1.1
 - @now/node@1.2.1
 - @now/routing-utils@1.4.0
 - @now/static-build@0.13.1
2019-12-09 08:03:47 -05:00
Steven
566d82e873 Publish Canary
- now@16.6.3-canary.0
 - @now/next@2.1.1-canary.1
 - @now/node@1.2.1-canary.0
 - @now/routing-utils@1.3.4-canary.6
 - @now/static-build@0.13.1-canary.1
2019-12-06 19:49:27 -05:00
Steven
44ae0b654e [now-routing-utils] Use 308 status code (#3392)
We decided that all of the new properties should default to 301 status code for any redirects.
2019-12-07 00:38:26 +00:00
Steven
d8cfaae596 [now-node][now-next][now-static-build] Remove lockfiles from cache (#3391)
The lock files should not be cached because the user may wish to make a new deployment without a `yarn.lock` or `package-lock.json`.

This recently started causing problems because of the order of downloading cache changed from before user files to after user files.

So we need to be extra careful to only cache outputs and not source files.
2019-12-06 23:35:17 +00:00
Steven
a40e0f21ee Publish Canary
- @now/build-utils@1.1.1-canary.2
2019-12-06 15:57:33 -05:00
Steven
ac1f506c98 [now-build-utils] Add --no-audit flag to npm install (#3390)
This PR will reduce deployment time when a `package-lock.json` file is found by avoiding the audit step which usually [sends audit reports](https://docs.npmjs.com/cli/audit#description) to the registry.

The [--no-audit](https://docs.npmjs.com/cli/install) flag was introduced in [npm@6](
https://medium.com/npm-inc/announcing-npm-6-5d0b1799a905) which shipped with Node 10. However, using the flag with npm@5 does not do anything which is great because npm@5 doesn't audit. So this PR is backwards compatible.

### Performance

I tried `npm install` and `npm install --no-audit` with a large project, [StateOfJS](a9fa6d47f9/homepages/stateofjs), which has 2206 packages (audited 21778 packages).

I made sure to `rm -rf node_modules` each time and ran both commands 5 times to make sure it was always faster with `--no-audit`.

- Before: 61 seconds
- After: 49 seconds
2019-12-06 20:48:30 +00:00
Steven
68d5bdcf3d [script] Fix stable publish script (#3389)
Since we switched to a single branch (instead of master/canary), lerna gets confused about which packages to publish because stable and canary releases are in the same branch.

This PR fixes the confusion by looking at the git history and using [--force-publish](https://github.com/lerna/lerna/tree/master/commands/version#--force-publish) on the changed packages.

In order to avoid confusion for the person publishing, I removed the `yarn publish-stable` script in favor of `yarn changelog` which will print the change log and emit a script that can be used to publish stable.

<details><summary>View Example Output</summary>
<p>

```
$ yarn changelog
yarn run v1.19.1
$ node changelog.js
Changes since the last Stable release (21fe0a2):

- [now-cli] Change `--debug` to avoid debugging builders (#3386) [Steven]
- [now-next] Update routes for new check: true behavior (#3383) [JJ Kasper]
- [now-build-utils] Update Detectors API (#3384) [Nathan Rajlich]
- [now-client] Bump version (#3385) [Andy]
- [now-client] (Major) Split `now-client` options (#3382) [Andy]
- [now-cli][now-client] Fix user agent (#3381) [Steven]
- [now-client] Fix `main` in package.json (#3344) [Max]
- [now-build-utils] Change `script` to `scripts` in error message (#3376) [Andy]
- [now-cli] Add support for `check: true` routes in `now dev` (#3364) [Steven]
- [now-cli] Fix preinstall script on windows when `LOCALAPPDATA` is missing (#3365) [Steven]
- [now dev] skip installing already installed versioned runtimes (#3354) [Tommaso De Rossi]
- [now-routing-utils] Update `path-to-regexp` to v6.1.0 (#3361) [Steven]
- [now-routing-utils] Add mergeRoutes function (#3358) [Steven]
- [docs] Remove deprecated LambdaRuntimes (#3346) [Steven]
- [now-routing-utils] Add support for `check: true` (#3343) [Steven]
- [now-static-build] Cache `.cache` folder for gatsby deployments (#3260) (#3342) [Luc]

To publish a stable release, execute the following:

git pull && lerna version --message 'Publish Stable' --exact --force-publish=@now/build-utils,now,now-client,@now/next,@now/routing-utils,@now/static-build
```

</p>
</details>
2019-12-06 19:31:38 +00:00
Andy Bitz
beb51f8c67 Publish Stable
- now@16.6.2
2019-12-06 13:48:44 +01:00
Andy
b881cb7111 [now-cli] Remove github property from payload before sending it (#3388)
* [now-cli] Remove `github` property from payload before sending it

* Add test and remove unused one

* Remove .only

* Remove unused fixture

* Use correct github properties
2019-12-06 13:47:16 +01:00
Andy Bitz
d83bc59257 Publish Stable
- now@16.6.1
2019-12-06 00:18:57 +01:00
Steven
5be9f297de [now-cli] Change --debug to avoid debugging builders (#3386)
* [now-cli] Change `--debug` to avoid debugging builders

* Fix tests

* Replace test with on/off
2019-12-06 00:11:22 +01:00
JJ Kasper
51d440431e Publish Canary
- @now/build-utils@1.1.1-canary.1
 - @now/next@2.1.1-canary.0
2019-12-05 15:18:35 -06:00
JJ Kasper
7cf061122c [now-next] Update routes for new check: true behavior (#3383)
As discussed this moves the `handle: filesystem` usage to the right location now that we have `check: true` for the `rewrites`
2019-12-05 20:48:17 +00:00
Nathan Rajlich
1254368505 [now-build-utils] Update Detectors API (#3384)
* Changes the `buildCommand` and `devCommand` from `string[]` to `string`
 * Renames `buildEnv` to `buildVariables` and `devEnv` to `devVariables`
2019-12-05 20:01:28 +00:00
Andy Bitz
9d4b830c5f Publish Canary
- now@16.6.1-canary.1
 - now-client@6.0.0-canary.1
2019-12-05 14:14:50 +01:00
Andy
37401b4363 [now-client] Bump version (#3385) 2019-12-05 14:14:00 +01:00
Andy
10fe08e14f [now-client] (Major) Split now-client options (#3382)
* Change types

* Split options for now-client

* Fix query and teamId

* Adjust tests

* Fix linting

* Ignore scope

* Adjust now-client tests

* Adjust more tests

* Apply prettier
2019-12-05 14:08:39 +01:00
Steven
0ecdb35d50 [now-cli][now-client] Fix user agent (#3381)
Since switching to a single branch, each package in the monorepo can be independently versioned so that some packages are using a canary version and others using a stable version.

This PR fixes an issue where a canary version of `now-cli` is bundling a stable version of `now-client` and thus does does not deploy zero config using canary builders.

The solution is to pass the User Agent from `now-cli` to `now-client` in a new option.

A nice side-effect of this PR is that we will switch the User Agent back to what it used to be pre-now-client days. It will look something like `now 16.6.1-canary.0 node-v10.17.0 darwin (x64)`.
2019-12-04 23:10:31 +00:00
Steven
caee8fe9ef Publish Canary
- now-client@5.2.5-canary.0
2019-12-04 16:46:43 -05:00
Max
7d92c27b2d [now-client] Fix main in package.json (#3344)
This sets `main` in `now-client` to a proper path.

Follow up to #3315.

Fixes #3373.
2019-12-04 13:25:27 -08:00
Steven
701eabbaba Publish Canary
- now@16.6.1-canary.0
2019-12-04 09:28:21 -05:00
Andy Bitz
e74a1b2d1a Publish Canary
- @now/build-utils@1.1.1-canary.0
2019-12-02 23:41:28 +01:00
Andy
e087b02333 [now-build-utils] Change script to scripts in error message (#3376)
Change `script` to `scripts` in error message.

[PRODUCT-740]

[PRODUCT-740]: https://zeit.atlassian.net/browse/PRODUCT-740
2019-12-02 22:33:27 +00:00
Steven
eea7f902b5 [now-cli] Add support for check: true routes in now dev (#3364)
This PR adds `now dev` support for routes that define `check: true`.

The algorithm is as follows:

- If a matching `dest` file is found, then serve it
- If a matching `src` file is found, then serve it
- Otherwise, behave the same as `continue: true` and continue processing routes
2019-11-28 10:58:13 +00:00
Steven
db7583201b [now-cli] Fix preinstall script on windows when LOCALAPPDATA is missing (#3365)
Usually `LOCALAPPDATA` is set to `C:\Users\{username}\AppData\Local` but occasionally, it is unassigned and causes installation failures. Looks like this could be due to the [registry](https://liquidwarelabs.zendesk.com/hc/en-us/articles/210634163-How-To-Make-APPDATA-and-LOCALAPPDATA-Environment-Variables-Follow-The-Registry-Keys).

If `LOCALAPPDATA` is missing, we can assume that now.exe was not installed before and can skip the deletion step that happens in the preinstall script.
2019-11-28 00:35:53 +00:00
Tommaso De Rossi
023001a8b1 [now dev] skip installing already installed versioned runtimes (#3354)
Fixes #3353
The current solution might break if a user interrupts `now dev` while yarn wrote the package in the cache package.json but has not yet added to node_modules.
This happens in like 20 ms but is possible, so we could execute `yarn` every time to be sure.
Tell me if the above is a problem or not
2019-11-27 11:33:18 +00:00
Steven
4ff8ab2435 Publish Canary
- @now/routing-utils@1.3.4-canary.5
2019-11-26 19:00:34 -05:00
Steven
d2cccbfce6 [now-routing-utils] Update path-to-regexp to v6.1.0 (#3361)
This bumps `path-to-regexp` to the latest version 6.1.0 which fixes optional capture groups like the test I added for `/next.js`.
2019-11-26 23:41:35 +00:00
Steven
970e6c400c Publish Canary
- @now/routing-utils@1.3.4-canary.4
2019-11-26 13:10:09 -05:00
Steven
b4cb7345a1 [now-routing-utils] Add mergeRoutes function (#3358)
This moves the merging logic to `@now/routing-utils` and adds support for `check: true`.

- Builder before filesystem, continue: true
- User before filesystem
- Builder before filesystem, check: true
- Builder before filesystem, continue: false
- Handle filesystem
- Builder after filesystem, continue: true
- User after filesystem
- Builder after filesystem, check: true
- Builder after filesystem, continue: false
2019-11-26 17:51:19 +00:00
Steven
7e75d8c1a3 [docs] Remove deprecated LambdaRuntimes (#3346)
- Removes Node 8.10 and old .NET which are EOL
- Adds a couple missing such as Ruby
2019-11-22 21:12:23 +00:00
Steven
a4ea551160 Publish Canary
- @now/routing-utils@1.3.4-canary.3
2019-11-22 14:41:33 -05:00
Steven
f56ad447a0 [now-routing-utils] Add support for check: true (#3343)
This PR adds support for `check: true` for a route object. It is basically a way to add a rewrite and still check the filesystem.
2019-11-22 19:03:45 +00:00
luc
7656422057 Publish Canary
- @now/static-build@0.13.1-canary.0
2019-11-22 17:39:35 +08:00
Luc
afa2231add [now-static-build] Cache .cache folder for gatsby deployments (#3260) (#3342)
Apply 77348ea71e again.

> Adds `.cache` folder to the Now cache for Gatsby deployments.

> Also adds a generic optional `cachePattern` property to the frameworks array so we can optimize cache paths for other frameworks in the future.
2019-11-22 09:16:51 +00:00
89 changed files with 1286 additions and 720 deletions

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
node_modules
package-lock.json
dist
.vscode
npm-debug.log
yarn-error.log
.nyc_output

View File

@@ -285,14 +285,13 @@ This is an abstract enumeration type that is implemented by one of the following
- `nodejs12.x`
- `nodejs10.x`
- `nodejs8.10`
- `go1.x`
- `java-1.8.0-openjdk`
- `java11`
- `python3.8`
- `python3.6`
- `python2.7`
- `dotnetcore2.1`
- `dotnetcore2.0`
- `dotnetcore1.0`
- `ruby2.5`
- `provided`
## JavaScript API

View File

@@ -12,12 +12,31 @@ if (!commit) {
throw new Error('Unable to find last publish commit');
}
const log = execSync(`git log --pretty=format:"- %s [%an]" ${commit}...HEAD`)
.toString()
.trim()
.split('\n')
.filter(line => !line.startsWith('- Publish Canary '))
.join('\n');
const log =
execSync(`git log --pretty=format:"- %s [%an]" ${commit}...HEAD`)
.toString()
.trim()
.split('\n')
.filter(line => !line.startsWith('- Publish Canary '))
.join('\n') || 'NO CHANGES DETECTED';
console.log(`Changes since the last Stable release (${commit.slice(0, 7)}):`);
console.log(`\n${log}\n`);
const pkgs =
Array.from(
new Set(
execSync(`git diff --name-only ${commit}...HEAD`)
.toString()
.trim()
.split('\n')
.filter(line => line.startsWith('packages/'))
.map(line => line.split('/')[1])
.map(pkgName => require(`./packages/${pkgName}/package.json`).name)
)
).join(',') || 'now';
console.log('To publish a stable release, execute the following:');
console.log(
`\ngit pull && lerna version --message 'Publish Stable' --exact --force-publish=${pkgs}\n`
);

View File

@@ -30,7 +30,7 @@
"scripts": {
"lerna": "lerna",
"bootstrap": "lerna bootstrap",
"publish-stable": "git pull && lerna version --message 'Publish Stable' --exact",
"publish-stable": "echo 'Run `yarn changelog` for instructions'",
"publish-canary": "git pull && lerna version prerelease --preid canary --message 'Publish Canary' --exact",
"publish-from-github": "./.circleci/publish.sh",
"changelog": "node changelog.js",

View File

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

View File

@@ -18,7 +18,7 @@ const config: Config = { zeroConfig: true };
const MISSING_BUILD_SCRIPT_ERROR: ErrorResponse = {
code: 'missing_build_script',
message:
'Your `package.json` file is missing a `build` property inside the `script` property.' +
'Your `package.json` file is missing a `build` property inside the `scripts` property.' +
'\nMore details: https://zeit.co/docs/v2/platform/frequently-asked-questions#missing-build-script',
};

View File

@@ -6,9 +6,9 @@ export default async function detectAngular({
const hasAngular = await hasDependency('@angular/cli');
if (!hasAngular) return false;
return {
buildCommand: ['ng', 'build'],
buildCommand: 'ng build',
buildDirectory: 'dist',
devCommand: ['ng', 'serve', '--port', '$PORT'],
devCommand: 'ng serve --port $PORT',
minNodeRange: '10.x',
routes: [
{

View File

@@ -10,8 +10,8 @@ export default async function detectBrunch({
if (!hasConfig) return false;
return {
buildCommand: ['brunch', 'build', '--production'],
buildCommand: 'brunch build --production',
buildDirectory: 'public',
devCommand: ['brunch', 'watch', '--server', '--port', '$PORT'],
devCommand: 'brunch watch --server --port $PORT',
};
}

View File

@@ -8,10 +8,10 @@ export default async function detectCreateReactAppEjected({
return false;
}
return {
buildCommand: ['node', 'scripts/build.js'],
buildCommand: 'node scripts/build.js',
buildDirectory: 'build',
devCommand: ['node', 'scripts/start.js'],
devEnv: { BROWSER: 'none' },
devCommand: 'node scripts/start.js',
devVariables: { BROWSER: 'none' },
routes: [
{
src: '/static/(.*)',

View File

@@ -8,10 +8,10 @@ export default async function detectCreateReactApp({
return false;
}
return {
buildCommand: ['react-scripts', 'build'],
buildCommand: 'react-scripts build',
buildDirectory: 'build',
devCommand: ['react-scripts', 'start'],
devEnv: { BROWSER: 'none' },
devCommand: 'react-scripts start',
devVariables: { BROWSER: 'none' },
routes: [
{
src: '/static/(.*)',

View File

@@ -6,8 +6,8 @@ export default async function detectDocusaurus({
const hasDocusaurus = await hasDependency('docusaurus');
if (!hasDocusaurus) return false;
return {
buildCommand: ['docusaurus-build'],
buildCommand: 'docusaurus-build',
buildDirectory: 'build',
devCommand: ['docusaurus-start', '--port', '$PORT'],
devCommand: 'docusaurus-start --port $PORT',
};
}

View File

@@ -6,15 +6,8 @@ export default async function detectEleventy({
const hasEleventy = await hasDependency('@11ty/eleventy');
if (!hasEleventy) return false;
return {
buildCommand: ['npx', '@11ty/eleventy'],
buildCommand: 'npx @11ty/eleventy',
buildDirectory: '_site',
devCommand: [
'npx',
'@11ty/eleventy',
'--serve',
'--watch',
'--port',
'$PORT',
],
devCommand: 'npx @11ty/eleventy --serve --watch --port $PORT',
};
}

View File

@@ -6,9 +6,9 @@ export default async function detectEmber({
const hasEmber = await hasDependency('ember-cli');
if (!hasEmber) return false;
return {
buildCommand: ['ember', 'build'],
buildCommand: 'ember build',
buildDirectory: 'dist',
devCommand: ['ember', 'serve', '--port', '$PORT'],
devCommand: 'ember serve --port $PORT',
routes: [
{
handle: 'filesystem',

View File

@@ -8,9 +8,9 @@ export default async function detectGatsby({
return false;
}
return {
buildCommand: ['gatsby', 'build'],
buildCommand: 'gatsby build',
buildDirectory: 'public',
devCommand: ['gatsby', 'develop', '-p', '$PORT'],
devCommand: 'gatsby develop -p $PORT',
cachePattern: '.cache/**',
};
}

View File

@@ -8,8 +8,8 @@ export default async function detectGridsome({
return false;
}
return {
buildCommand: ['gridsome', 'build'],
buildCommand: 'gridsome build',
buildDirectory: 'dist',
devCommand: ['gridsome', 'develop', '-p', '$PORT'],
devCommand: 'gridsome develop -p $PORT',
};
}

View File

@@ -6,8 +6,8 @@ export default async function detectHexo({
const hasHexo = await hasDependency('hexo');
if (!hasHexo) return false;
return {
buildCommand: ['hexo', 'generate'],
buildCommand: 'hexo generate',
buildDirectory: 'public',
devCommand: ['hexo', 'server', '--port', '$PORT'],
devCommand: 'hexo server --port $PORT',
};
}

View File

@@ -19,8 +19,8 @@ export default async function detectHugo({
return false;
}
return {
buildCommand: ['hugo'],
buildCommand: 'hugo',
buildDirectory: config.publishDir || 'public',
devCommand: ['hugo', 'server', '-D', '-w', '-p', '$PORT'],
devCommand: 'hugo server -D -w -p $PORT',
};
}

View File

@@ -15,16 +15,8 @@ export default async function detectJekyll({
return false;
}
return {
buildCommand: ['jekyll', 'build'],
buildCommand: 'jekyll build',
buildDirectory: config.destination || '_site',
devCommand: [
'bundle',
'exec',
'jekyll',
'serve',
'--watch',
'--port',
'$PORT',
],
devCommand: 'bundle exec jekyll serve --watch --port $PORT',
};
}

View File

@@ -7,8 +7,8 @@ export default async function detectMiddleman({
if (!hasConfig) return false;
return {
buildCommand: ['bundle', 'exec', 'middleman', 'build'],
buildCommand: 'bundle exec middleman build',
buildDirectory: 'build',
devCommand: ['bundle', 'exec', 'middleman', 'server', '-p', '$PORT'],
devCommand: 'bundle exec middleman server -p $PORT',
};
}

View File

@@ -6,8 +6,8 @@ export default async function detectNext({
const hasNext = await hasDependency('next');
if (!hasNext) return false;
return {
buildCommand: ['next', 'build'],
buildCommand: 'next build',
buildDirectory: 'build',
devCommand: ['next', '-p', '$PORT'],
devCommand: 'next -p $PORT',
};
}

View File

@@ -6,9 +6,9 @@ export default async function detectPolymer({
const hasPolymer = await hasDependency('polymer-cli');
if (!hasPolymer) return false;
return {
buildCommand: ['polymer', 'build'],
buildCommand: 'polymer build',
buildDirectory: 'build',
devCommand: ['polymer', 'serve', '--port', '$PORT'],
devCommand: 'polymer serve --port $PORT',
routes: [
{
handle: 'filesystem',

View File

@@ -6,9 +6,9 @@ export default async function detectPreact({
const hasPreact = await hasDependency('preact-cli');
if (!hasPreact) return false;
return {
buildCommand: ['preact', 'build'],
buildCommand: 'preact build',
buildDirectory: 'build',
devCommand: ['preact', 'watch', '--port', '$PORT'],
devCommand: 'preact watch --port $PORT',
routes: [
{
handle: 'filesystem',

View File

@@ -6,9 +6,9 @@ export default async function detectSaber({
const hasSaber = await hasDependency('saber');
if (!hasSaber) return false;
return {
buildCommand: ['saber', 'build'],
buildCommand: 'saber build',
buildDirectory: 'public',
devCommand: ['saber', '--port', '$PORT'],
devCommand: 'saber --port $PORT',
routes: [
{
src: '/_saber/.*',

View File

@@ -6,8 +6,8 @@ export default async function detectSapper({
const hasSapper = await hasDependency('sapper');
if (!hasSapper) return false;
return {
buildCommand: ['sapper', 'export'],
buildCommand: 'sapper export',
buildDirectory: '__sapper__/export',
devCommand: ['sapper', 'dev', '--port', '$PORT'],
devCommand: 'sapper dev --port $PORT',
};
}

View File

@@ -6,17 +6,9 @@ export default async function detectStencil({
const hasStencil = await hasDependency('@stencil/core');
if (!hasStencil) return false;
return {
buildCommand: ['stencil', 'build'],
buildCommand: 'stencil build',
buildDirectory: 'www',
devCommand: [
'stencil',
'build',
'--dev',
'--watch',
'--serve',
'--port',
'$PORT',
],
devCommand: 'stencil build --dev --watch --serve --port $PORT',
routes: [
{
handle: 'filesystem',

View File

@@ -6,9 +6,9 @@ export default async function detectSvelte({
const hasSvelte = await hasDependency('sirv-cli');
if (!hasSvelte) return false;
return {
buildCommand: ['rollup', '-c'],
buildCommand: 'rollup -c',
buildDirectory: 'public',
devCommand: ['sirv', 'public', '--single', '--dev', ' --port', '$PORT'],
devCommand: 'sirv public --single --dev --port $PORT',
routes: [
{
handle: 'filesystem',

View File

@@ -6,9 +6,9 @@ export default async function detectUmiJS({
const hasUmi = await hasDependency('umi');
if (!hasUmi) return false;
return {
buildCommand: ['umi', 'build'],
buildCommand: 'umi build',
buildDirectory: 'dist',
devCommand: ['umi', 'dev', '--port', '$PORT'],
devCommand: 'umi dev --port $PORT',
routes: [
{
handle: 'filesystem',

View File

@@ -6,9 +6,9 @@ export default async function detectVue({
const hasVue = await hasDependency('@vue/cli-service');
if (!hasVue) return false;
return {
buildCommand: ['vue-cli-service', 'build'],
buildCommand: 'vue-cli-service build',
buildDirectory: 'dist',
devCommand: ['vue-cli-service', 'serve', '--port', '$PORT'],
devCommand: 'vue-cli-service serve --port $PORT',
routes: [
{
src: '^/[^/]*\\.(js|txt|ico|json)',

View File

@@ -155,7 +155,7 @@ export async function runNpmInstall(
commandArgs = args.filter(a => a !== '--prefer-offline');
await spawnAsync(
'npm',
commandArgs.concat(['install', '--unsafe-perm']),
commandArgs.concat(['install', '--no-audit', '--unsafe-perm']),
opts
);
} else {

View File

@@ -352,11 +352,11 @@ export interface DetectorParameters {
}
export interface DetectorOutput {
buildCommand: string[];
buildCommand: string;
buildDirectory: string;
buildEnv?: Env;
devCommand?: string[];
devEnv?: Env;
buildVariables?: Env;
devCommand?: string;
devVariables?: Env;
minNodeRange?: string;
cachePattern?: string;
routes?: Route[];

View File

@@ -68,7 +68,7 @@ test('detectDefaults() - angular', async () => {
const result = await detectDefaults({ fs });
if (!result) throw new Error('Expected result');
assert.equal(result.buildDirectory, 'dist');
assert.deepEqual(result.buildCommand, ['ng', 'build']);
assert.deepEqual(result.buildCommand, 'ng build');
});
test('detectDefaults() - brunch', async () => {
@@ -77,7 +77,7 @@ test('detectDefaults() - brunch', async () => {
const result = await detectDefaults({ fs });
if (!result) throw new Error('Expected result');
assert.equal(result.buildDirectory, 'public');
assert.deepEqual(result.buildCommand, ['brunch', 'build', '--production']);
assert.deepEqual(result.buildCommand, 'brunch build --production');
});
test('detectDefaults() - hugo', async () => {
@@ -86,7 +86,7 @@ test('detectDefaults() - hugo', async () => {
const result = await detectDefaults({ fs });
if (!result) throw new Error('Expected result');
assert.equal(result.buildDirectory, 'public');
assert.deepEqual(result.buildCommand, ['hugo']);
assert.deepEqual(result.buildCommand, 'hugo');
});
test('detectDefaults() - jekyll', async () => {
@@ -95,7 +95,7 @@ test('detectDefaults() - jekyll', async () => {
const result = await detectDefaults({ fs });
if (!result) throw new Error('Expected result');
assert.equal(result.buildDirectory, '_site');
assert.deepEqual(result.buildCommand, ['jekyll', 'build']);
assert.deepEqual(result.buildCommand, 'jekyll build');
});
test('detectDefaults() - middleman', async () => {
@@ -104,10 +104,5 @@ test('detectDefaults() - middleman', async () => {
const result = await detectDefaults({ fs });
if (!result) throw new Error('Expected result');
assert.equal(result.buildDirectory, 'build');
assert.deepEqual(result.buildCommand, [
'bundle',
'exec',
'middleman',
'build',
]);
assert.deepEqual(result.buildCommand, 'bundle exec middleman build');
});

View File

@@ -1,6 +1,6 @@
{
"name": "now",
"version": "16.6.0",
"version": "16.6.3",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Now",

View File

@@ -2,6 +2,7 @@
const fs = require('fs');
const { promisify } = require('util');
const { join, delimiter } = require('path');
const { homedir } = require('os');
const stat = promisify(fs.stat);
const unlink = promisify(fs.unlink);
@@ -39,7 +40,16 @@ function isGlobal() {
// See: https://git.io/fj4jD
function getNowPath() {
if (process.platform === 'win32') {
const path = join(process.env.LOCALAPPDATA, 'now-cli', 'now.exe');
const { LOCALAPPDATA, USERPROFILE, HOMEPATH } = process.env;
const home = homedir() || USERPROFILE || HOMEPATH;
let path;
if (LOCALAPPDATA) {
path = join(LOCALAPPDATA, 'now-cli', 'now.exe');
} else if (home) {
path = join(home, 'AppData', 'Local', 'now-cli', 'now.exe');
} else {
path = '';
}
return fs.existsSync(path) ? path : null;
}
@@ -48,7 +58,7 @@ function getNowPath() {
const paths = [
join(process.env.HOME || '/', 'bin'),
'/usr/local/bin',
'/usr/bin'
'/usr/bin',
];
for (const basePath of paths) {

View File

@@ -294,15 +294,11 @@ export default async function main(
parseEnv(argv['--env'])
);
// Enable debug mode for builders
const buildDebugEnv = debugEnabled ? { NOW_BUILDER_DEBUG: '1' } : {};
// Merge build env out of `build.env` from now.json, and `--build-env` args
const deploymentBuildEnv = Object.assign(
{},
parseEnv(localConfig.build && localConfig.build.env),
parseEnv(argv['--build-env']),
buildDebugEnv
parseEnv(argv['--build-env'])
);
// If there's any undefined values, then inherit them from this process

View File

@@ -2,13 +2,7 @@ import { NowConfig } from './util/dev/types';
export type ThenArg<T> = T extends Promise<infer U> ? U : T;
export interface Config extends NowConfig {
alias?: string[] | string;
aliases?: string[] | string;
name?: string;
type?: string;
scope?: string;
}
export type Config = NowConfig;
export interface NowContext {
argv: string[];

View File

@@ -6,12 +6,14 @@ import {
createDeployment,
createLegacyDeployment,
DeploymentOptions,
} from 'now-client/dist';
NowClientOptions,
} from 'now-client';
import wait from '../output/wait';
import { Output } from '../output';
// @ts-ignore
import Now from '../../util';
import { NowConfig } from '../dev/types';
import ua from '../ua';
export default async function processDeployment({
now,
@@ -21,9 +23,9 @@ export default async function processDeployment({
requestBody,
uploadStamp,
deployStamp,
legacy,
env,
isLegacy,
quiet,
force,
nowConfig,
}: {
now: Now;
@@ -33,27 +35,36 @@ export default async function processDeployment({
requestBody: DeploymentOptions;
uploadStamp: () => number;
deployStamp: () => number;
legacy: boolean;
env: any;
isLegacy: boolean;
quiet: boolean;
nowConfig?: NowConfig;
force?: boolean;
}) {
const { warn, log, debug, note } = output;
let bar: Progress | null = null;
const path0 = paths[0];
const opts: DeploymentOptions = {
...requestBody,
debug: now._debug,
const { env = {} } = requestBody;
const nowClientOptions: NowClientOptions = {
teamId: now.currentTeam,
apiUrl: now._apiUrl,
token: now._token,
debug: now._debug,
userAgent: ua,
path: paths[0],
force,
};
if (!legacy) {
if (!isLegacy) {
let queuedSpinner = null;
let buildSpinner = null;
let deploySpinner = null;
for await (const event of createDeployment(path0, opts, nowConfig)) {
for await (const event of createDeployment(
nowClientOptions,
requestBody,
nowConfig
)) {
if (event.type === 'hashes-calculated') {
hashes = event.payload;
}
@@ -110,7 +121,7 @@ export default async function processDeployment({
now._host = event.payload.url;
if (!quiet) {
const version = legacy ? `${chalk.grey('[v1]')} ` : '';
const version = isLegacy ? `${chalk.grey('[v1]')} ` : '';
log(`https://${event.payload.url} ${version}${deployStamp()}`);
} else {
process.stdout.write(`https://${event.payload.url}`);
@@ -176,7 +187,11 @@ export default async function processDeployment({
}
}
} else {
for await (const event of createLegacyDeployment(path0, opts, nowConfig)) {
for await (const event of createLegacyDeployment(
nowClientOptions,
requestBody,
nowConfig
)) {
if (event.type === 'hashes-calculated') {
hashes = event.payload;
}
@@ -224,7 +239,7 @@ export default async function processDeployment({
now._host = event.payload.url;
if (!quiet) {
const version = legacy ? `${chalk.grey('[v1]')} ` : '';
const version = isLegacy ? `${chalk.grey('[v1]')} ` : '';
log(`${event.payload.url} ${version}${deployStamp()}`);
} else {
process.stdout.write(`https://${event.payload.url}`);

View File

@@ -158,6 +158,14 @@ export function getBuildUtils(packages: string[]): string {
return `@now/build-utils@${version}`;
}
function parseVersionSafe(rawSpec: string) {
try {
return semver.parse(rawSpec);
} catch (e) {
return null;
}
}
export function filterPackage(
builderSpec: string,
distTag: string,
@@ -165,6 +173,17 @@ export function filterPackage(
) {
if (builderSpec in localBuilders) return false;
const parsed = npa(builderSpec);
const parsedVersion = parseVersionSafe(parsed.rawSpec);
// skip install of already installed runtime
if (
parsed.name &&
parsed.type === 'version' &&
parsedVersion &&
buildersPkg.dependencies &&
parsedVersion.version == buildersPkg.dependencies[parsed.name]
) {
return false;
}
if (
parsed.name &&
parsed.type === 'tag' &&

View File

@@ -83,12 +83,6 @@ async function createBuildProcess(
NOW_REGION: 'dev1',
};
// Builders won't show debug logs by default.
// The `NOW_BUILDER_DEBUG` env variable enables them.
if (debugEnabled) {
env.NOW_BUILDER_DEBUG = '1';
}
const buildProcess = fork(modulePath, [], {
cwd: workPath,
env,

View File

@@ -88,6 +88,17 @@ export default async function(
continue;
}
if (routeConfig.check && devServer) {
const { pathname = '/' } = url.parse(destPath);
const hasDestFile = await devServer.hasFilesystem(pathname);
if (!hasDestFile) {
// If the file is not found, `check: true` will
// behave the same as `continue: true`
reqPathname = destPath;
continue;
}
}
if (isURL(destPath)) {
found = {
found: true,

View File

@@ -9,9 +9,12 @@ import {
PackageJson,
BuilderFunctions,
} from '@now/build-utils';
import { NowConfig } from 'now-client';
import { NowRedirect, NowRewrite, NowHeader, Route } from '@now/routing-utils';
import { Output } from '../output';
export { NowConfig };
export interface DevServerOptions {
output: Output;
debug: boolean;
@@ -31,24 +34,6 @@ export interface BuildMatch extends BuildConfig {
export type RouteConfig = Route;
export interface NowConfig {
name?: string;
version?: number;
env?: EnvConfig;
build?: {
env?: EnvConfig;
};
builds?: BuildConfig[];
routes?: RouteConfig[];
files?: string[];
cleanUrls?: boolean;
rewrites?: NowRewrite[];
redirects?: NowRedirect[];
headers?: NowHeader[];
trailingSlash?: boolean;
functions?: BuilderFunctions;
}
export interface HttpHandler {
(req: http.IncomingMessage, res: http.ServerResponse): void;
}

View File

@@ -53,7 +53,6 @@ export default class Now extends EventEmitter {
nowConfig = {},
hasNowJson = false,
sessionAffinity = 'random',
atlas = false,
// Latest
name,
@@ -71,39 +70,21 @@ export default class Now extends EventEmitter {
) {
const opts = { output: this._output, hasNowJson };
const { log, warn, debug } = this._output;
const isBuilds = type === null;
const isLegacy = type !== null;
let files = [];
let hashes = {};
const relatives = {};
let engines;
let deployment;
let requestBody = {};
if (isBuilds) {
requestBody = {
token: this._token,
teamId: this.currentTeam,
env,
build,
public: wantsPublic || nowConfig.public,
name,
project,
meta,
regions,
force: forceNew,
};
if (target) {
requestBody.target = target;
}
} else if (type === 'npm') {
if (type === 'npm') {
files = await getNpmFiles(paths[0], pkg, nowConfig, opts);
// A `start` or `now-start` npm script, or a `server.js` file
// in the root directory of the deployment are required
if (
!isBuilds &&
isLegacy &&
!hasNpmStart(pkg) &&
!hasFile(paths[0], files, 'server.js')
) {
@@ -139,30 +120,30 @@ export default class Now extends EventEmitter {
const uploadStamp = stamp();
if (isBuilds) {
deployment = await processDeployment({
now: this,
output: this._output,
hashes,
paths,
requestBody,
uploadStamp,
deployStamp,
quiet,
nowConfig,
});
} else {
// Read `registry.npmjs.org` authToken from .npmrc
let authToken;
let requestBody = {
...nowConfig,
env,
build,
public: wantsPublic || nowConfig.public,
name,
project,
meta,
regions,
target: target || undefined,
};
if (type === 'npm' && forwardNpm) {
authToken =
(await readAuthToken(paths[0])) || (await readAuthToken(homedir()));
}
// Ignore specific items from Now.json
delete requestBody.scope;
delete requestBody.github;
if (isLegacy) {
// Read `registry.npmjs.org` authToken from .npmrc
const registryAuthToken =
type === 'npm' && forwardNpm
? (await readAuthToken(paths[0])) || (await readAuthToken(homedir()))
: undefined;
requestBody = {
token: this._token,
teamId: this.currentTeam,
env,
build,
meta,
@@ -172,31 +153,29 @@ export default class Now extends EventEmitter {
project,
description,
deploymentType: type,
registryAuthToken: authToken,
registryAuthToken,
engines,
scale,
sessionAffinity,
limits: nowConfig.limits,
atlas,
config: nowConfig,
functions: nowConfig.functions,
};
deployment = await processDeployment({
legacy: true,
now: this,
output: this._output,
hashes,
paths,
requestBody,
uploadStamp,
deployStamp,
quiet,
env,
nowConfig,
});
}
deployment = await processDeployment({
isLegacy,
now: this,
output: this._output,
hashes,
paths,
requestBody,
uploadStamp,
deployStamp,
quiet,
nowConfig,
force: forceNew,
});
// We report about files whose sizes are too big
let missingVersion = false;
@@ -228,7 +207,7 @@ export default class Now extends EventEmitter {
}
}
if (!isBuilds && !quiet && type === 'npm' && deployment.nodeVersion) {
if (isLegacy && !quiet && type === 'npm' && deployment.nodeVersion) {
if (engines && engines.node && !missingVersion) {
log(
chalk`Using Node.js {bold ${deployment.nodeVersion}} (requested: {dim \`${engines.node}\`})`

View File

@@ -4,8 +4,8 @@ import { filterPackage } from '../src/util/dev/builder-cache';
test('[dev-builder] filter install "latest", cached canary', async t => {
const buildersPkg = {
dependencies: {
'@now/build-utils': '0.0.1-canary.0'
}
'@now/build-utils': '0.0.1-canary.0',
},
};
const result = filterPackage('@now/build-utils', 'canary', buildersPkg);
t.is(result, true);
@@ -14,8 +14,8 @@ test('[dev-builder] filter install "latest", cached canary', async t => {
test('[dev-builder] filter install "canary", cached stable', async t => {
const buildersPkg = {
dependencies: {
'@now/build-utils': '0.0.1'
}
'@now/build-utils': '0.0.1',
},
};
const result = filterPackage(
'@now/build-utils@canary',
@@ -28,8 +28,8 @@ test('[dev-builder] filter install "canary", cached stable', async t => {
test('[dev-builder] filter install "latest", cached stable', async t => {
const buildersPkg = {
dependencies: {
'@now/build-utils': '0.0.1'
}
'@now/build-utils': '0.0.1',
},
};
const result = filterPackage('@now/build-utils', 'latest', buildersPkg);
t.is(result, false);
@@ -38,8 +38,8 @@ test('[dev-builder] filter install "latest", cached stable', async t => {
test('[dev-builder] filter install "canary", cached canary', async t => {
const buildersPkg = {
dependencies: {
'@now/build-utils': '0.0.1-canary.0'
}
'@now/build-utils': '0.0.1-canary.0',
},
};
const result = filterPackage(
'@now/build-utils@canary',
@@ -52,8 +52,8 @@ test('[dev-builder] filter install "canary", cached canary', async t => {
test('[dev-builder] filter install URL, cached stable', async t => {
const buildersPkg = {
dependencies: {
'@now/build-utils': '0.0.1'
}
'@now/build-utils': '0.0.1',
},
};
const result = filterPackage('https://tarball.now.sh', 'latest', buildersPkg);
t.is(result, true);
@@ -62,8 +62,8 @@ test('[dev-builder] filter install URL, cached stable', async t => {
test('[dev-builder] filter install URL, cached canary', async t => {
const buildersPkg = {
dependencies: {
'@now/build-utils': '0.0.1-canary.0'
}
'@now/build-utils': '0.0.1-canary.0',
},
};
const result = filterPackage('https://tarball.now.sh', 'canary', buildersPkg);
t.is(result, true);
@@ -72,8 +72,8 @@ test('[dev-builder] filter install URL, cached canary', async t => {
test('[dev-builder] filter install "latest", cached URL - stable', async t => {
const buildersPkg = {
dependencies: {
'@now/build-utils': 'https://tarball.now.sh'
}
'@now/build-utils': 'https://tarball.now.sh',
},
};
const result = filterPackage('@now/build-utils', 'latest', buildersPkg);
t.is(result, true);
@@ -82,9 +82,49 @@ test('[dev-builder] filter install "latest", cached URL - stable', async t => {
test('[dev-builder] filter install "latest", cached URL - canary', async t => {
const buildersPkg = {
dependencies: {
'@now/build-utils': 'https://tarball.now.sh'
}
'@now/build-utils': 'https://tarball.now.sh',
},
};
const result = filterPackage('@now/build-utils', 'canary', buildersPkg);
t.is(result, true);
});
test('[dev-builder] filter install not bundled version, cached same version', async t => {
const buildersPkg = {
dependencies: {
'not-bundled-package': '0.0.1',
},
};
const result = filterPackage('not-bundled-package@0.0.1', '_', buildersPkg);
t.is(result, false);
});
test('[dev-builder] filter install not bundled version, cached different version', async t => {
const buildersPkg = {
dependencies: {
'not-bundled-package': '0.0.9',
},
};
const result = filterPackage('not-bundled-package@0.0.1', '_', buildersPkg);
t.is(result, true);
});
test('[dev-builder] filter install not bundled stable, cached version', async t => {
const buildersPkg = {
dependencies: {
'not-bundled-package': '0.0.1',
},
};
const result = filterPackage('not-bundled-package', '_', buildersPkg);
t.is(result, true);
});
test('[dev-builder] filter install not bundled tagged, cached tagged', async t => {
const buildersPkg = {
dependencies: {
'not-bundled-package': '16.9.0-alpha.0',
},
};
const result = filterPackage('not-bundled-package@alpha', '_', buildersPkg);
t.is(result, true);
});

View File

@@ -0,0 +1,19 @@
{
"routes": [
{
"src": "/blog/(.*)",
"check": true,
"dest": "/blog?post=$1"
},
{
"src": "/(.*)",
"check": true,
"dest": "/src/$1"
},
{
"src": "/(.*)",
"check": true,
"dest": "/fake/$1"
}
]
}

View File

@@ -0,0 +1 @@
Blog Home

View File

@@ -154,6 +154,22 @@ function testFixtureStdio(directory, fn) {
};
}
test(
'[now dev] validate routes that use `check: true`',
testFixtureStdio('routes-check-true', async (t, port) => {
const result = await fetchWithRetry(
`http://localhost:${port}/blog/post`,
3
);
const response = await result;
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /Blog Home/gm);
})
);
test('[now dev] validate builds', async t => {
const directory = fixture('invalid-builds');
const output = await exec(directory);
@@ -291,10 +307,10 @@ test(
await testPath(200, '/sub', 'Sub Index Page');
await testPath(200, '/sub/another', 'Sub Another Page');
await testPath(200, '/style.css', 'body { color: green }');
await testPath(301, '/index.html', '', { Location: '/' });
await testPath(301, '/about.html', '', { Location: '/about' });
await testPath(301, '/sub/index.html', '', { Location: '/sub' });
await testPath(301, '/sub/another.html', '', { Location: '/sub/another' });
await testPath(308, '/index.html', '', { Location: '/' });
await testPath(308, '/about.html', '', { Location: '/about' });
await testPath(308, '/sub/index.html', '', { Location: '/sub' });
await testPath(308, '/sub/another.html', '', { Location: '/sub/another' });
})
);
@@ -308,10 +324,10 @@ test(
await testPath(200, '/sub/', 'Sub Index Page');
await testPath(200, '/sub/another/', 'Sub Another Page');
await testPath(200, '/style.css/', 'body { color: green }');
await testPath(301, '/index.html', '', { Location: '/' });
await testPath(301, '/about.html', '', { Location: '/about/' });
await testPath(301, '/sub/index.html', '', { Location: '/sub/' });
await testPath(301, '/sub/another.html', '', {
await testPath(308, '/index.html', '', { Location: '/' });
await testPath(308, '/about.html', '', { Location: '/about/' });
await testPath(308, '/sub/index.html', '', { Location: '/sub/' });
await testPath(308, '/sub/another.html', '', {
Location: '/sub/another/',
});
}
@@ -328,9 +344,9 @@ test(
await testPath(200, '/sub/index.html/', 'Sub Index Page');
await testPath(200, '/sub/another.html/', 'Sub Another Page');
await testPath(200, '/style.css/', 'body { color: green }');
await testPath(307, '/about.html', '', { Location: '/about.html/' });
await testPath(307, '/sub', '', { Location: '/sub/' });
await testPath(307, '/sub/another.html', '', {
await testPath(308, '/about.html', '', { Location: '/about.html/' });
await testPath(308, '/sub', '', { Location: '/sub/' });
await testPath(308, '/sub/another.html', '', {
Location: '/sub/another.html/',
});
})
@@ -346,9 +362,9 @@ test(
await testPath(200, '/sub/index.html', 'Sub Index Page');
await testPath(200, '/sub/another.html', 'Sub Another Page');
await testPath(200, '/style.css', 'body { color: green }');
await testPath(307, '/about.html/', '', { Location: '/about.html' });
await testPath(307, '/sub/', '', { Location: '/sub' });
await testPath(307, '/sub/another.html/', '', {
await testPath(308, '/about.html/', '', { Location: '/about.html' });
await testPath(308, '/sub/', '', { Location: '/sub' });
await testPath(308, '/sub/another.html/', '', {
Location: '/sub/another.html',
});
})

View File

@@ -122,11 +122,6 @@ module.exports = async session => {
'single-dotfile': {
'.testing': 'i am a dotfile',
},
'config-alias-property': {
'now.json':
'{ "alias": "test.now.sh", "builds": [ { "src": "*.html", "use": "@now/static" } ] }',
'index.html': '<span>test alias</span',
},
'config-scope-property-email': {
'now.json': `{ "scope": "${session}@zeit.pub", "builds": [ { "src": "*.html", "use": "@now/static" } ], "version": 2 }`,
'index.html': '<span>test scope email</span',
@@ -204,7 +199,7 @@ fs.writeFileSync(
'index.js',
fs
.readFileSync('index.js', 'utf8')
.replace('BUILD_ENV_DEBUG', process.env.NOW_BUILDER_DEBUG),
.replace('BUILD_ENV_DEBUG', process.env.NOW_BUILDER_DEBUG ? 'on' : 'off'),
);
`,
'index.js': `
@@ -463,6 +458,18 @@ CMD ["node", "index.js"]`,
},
}),
},
'github-and-scope-config': {
'index.txt': 'I Am a Website!',
'now.json': JSON.stringify({
scope: 'i-do-not-exist',
github: {
autoAlias: true,
autoJobCancelation: true,
enabled: true,
silent: true
}
})
}
};
for (const typeName of Object.keys(spec)) {

View File

@@ -1427,35 +1427,6 @@ test('ensure we render a prompt when deploying home directory', async t => {
t.true(stderr.includes('> Aborted'));
});
test('ensure the `alias` property is not sent to the API', async t => {
const directory = fixture('config-alias-property');
const { stdout, stderr, exitCode } = await execa(
binaryPath,
[directory, '--public', '--name', session, ...defaultArgs, '--force'],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
// Ensure the exit code is right
t.is(exitCode, 0);
// Test if the output is really a URL
const { href, host } = new URL(stdout);
t.is(host.split('-')[0], session);
// Send a test request to the deployment
const response = await fetch(href);
const contentType = response.headers.get('content-type');
t.is(contentType, 'text/html; charset=utf-8');
});
test('ensure the `scope` property works with email', async t => {
const directory = fixture('config-scope-property-email');
@@ -1995,7 +1966,7 @@ test('use `--debug` CLI flag', async t => {
// get the content
const response = await fetch(href);
const content = await response.text();
t.is(content.trim(), '1');
t.is(content.trim(), 'off');
});
test('try to deploy non-existing path', async t => {

View File

@@ -919,35 +919,6 @@ test('ensure we render a prompt when deploying home directory', async t => {
t.true(stderr.includes('> Aborted'));
});
test('ensure the `alias` property is not sent to the API', async t => {
const directory = fixture('config-alias-property');
const { stdout, stderr, exitCode } = await execa(
binaryPath,
[directory, '--public', '--name', session, ...defaultArgs, '--force'],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
// Ensure the exit code is right
t.is(exitCode, 0);
// Test if the output is really a URL
const { href, host } = new URL(stdout);
t.is(host.split('-')[0], session);
// Send a test request to the deployment
const response = await fetch(href);
const contentType = response.headers.get('content-type');
t.is(contentType, 'text/html; charset=utf-8');
});
test('ensure the `scope` property works with email', async t => {
const directory = fixture('config-scope-property-email');
@@ -1456,7 +1427,7 @@ test('use `--debug` CLI flag', async t => {
// get the content
const response = await fetch(href);
const content = await response.text();
t.is(content.trim(), '1');
t.is(content.trim(), 'off');
});
test('try to deploy non-existing path', async t => {
@@ -2056,7 +2027,6 @@ test('deploy a Lambda with a specific runtime', async t => {
t.is(build.use, 'now-php@0.0.7', JSON.stringify(build, null, 2));
});
// We need to skip this test until `now-php` supports Runtime version 3
test('fail to deploy a Lambda with a specific runtime but without a locked version', async t => {
const directory = fixture('lambda-with-invalid-runtime');
const output = await execute([directory]);
@@ -2069,6 +2039,13 @@ test('fail to deploy a Lambda with a specific runtime but without a locked versi
);
});
test('ensure `github` and `scope` are not sent to the API', async t => {
const directory = fixture('github-and-scope-config');
const output = await execute([directory]);
t.is(output.exitCode, 0, formatOutput(output));
});
test.after.always(async () => {
// Make sure the token gets revoked
await execa(binaryPath, ['logout', ...defaultArgs]);

View File

@@ -1,8 +1,8 @@
{
"name": "now-client",
"version": "5.2.4",
"main": "dist/src/index.js",
"typings": "dist/src/index.d.ts",
"version": "6.0.0",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"homepage": "https://zeit.co",
"license": "MIT",
"files": [

View File

@@ -8,7 +8,13 @@ import {
isAliasAssigned,
isAliasError,
} from './utils/ready-state';
import { Deployment, DeploymentBuild } from './types';
import { createDebug } from './utils';
import {
Dictionary,
Deployment,
NowClientOptions,
DeploymentBuild,
} from './types';
interface DeploymentStatus {
type: string;
@@ -16,22 +22,22 @@ interface DeploymentStatus {
}
/* eslint-disable */
export default async function* checkDeploymentStatus(
export async function* checkDeploymentStatus(
deployment: Deployment,
token: string,
version: number | undefined,
teamId: string | undefined,
debug: Function,
apiUrl?: string
clientOptions: NowClientOptions
): AsyncIterableIterator<DeploymentStatus> {
const { version } = deployment;
const { token, teamId, apiUrl, userAgent } = clientOptions;
const debug = createDebug(clientOptions.debug);
let deploymentState = deployment;
let allBuildsCompleted = false;
const buildsState: { [key: string]: DeploymentBuild } = {};
const buildsState: Dictionary<DeploymentBuild> = {};
const apiDeployments = getApiDeploymentsUrl({
version,
builds: deployment.builds,
functions: deployment.functions
functions: deployment.functions,
});
debug(`Using ${version ? `${version}.0` : '2.0'} API for status checks`);
@@ -54,7 +60,7 @@ export default async function* checkDeploymentStatus(
teamId ? `?teamId=${teamId}` : ''
}`,
token,
{ apiUrl }
{ apiUrl, userAgent }
);
const data = await buildsData.json();
@@ -91,7 +97,8 @@ export default async function* checkDeploymentStatus(
`${apiDeployments}/${deployment.id || deployment.deploymentId}${
teamId ? `?teamId=${teamId}` : ''
}`,
token
token,
{ apiUrl, userAgent }
);
const deploymentUpdate = await deploymentData.json();

View File

@@ -3,26 +3,22 @@ import { readdir as readRootFolder, lstatSync } from 'fs-extra';
import readdir from 'recursive-readdir';
import { relative, join, isAbsolute } from 'path';
import hashes, { mapToObject } from './utils/hashes';
import uploadAndDeploy from './upload';
import { upload } from './upload';
import { getNowIgnore, createDebug, parseNowJSON } from './utils';
import { DeploymentError } from './errors';
import {
CreateDeploymentFunction,
DeploymentOptions,
NowJsonOptions,
} from './types';
import { NowConfig, NowClientOptions, DeploymentOptions } from './types';
export { EVENTS } from './utils';
export default function buildCreateDeployment(
version: number
): CreateDeploymentFunction {
export default function buildCreateDeployment(version: number) {
return async function* createDeployment(
path: string | string[],
options: DeploymentOptions = {},
nowConfig?: NowJsonOptions
clientOptions: NowClientOptions,
deploymentOptions: DeploymentOptions,
nowConfig: NowConfig = {}
): AsyncIterableIterator<any> {
const debug = createDebug(options.debug);
const { path } = clientOptions;
const debug = createDebug(clientOptions.debug);
const cwd = process.cwd();
debug('Creating deployment...');
@@ -38,9 +34,9 @@ export default function buildCreateDeployment(
});
}
if (typeof options.token !== 'string') {
if (typeof clientOptions.token !== 'string') {
debug(
`Error: 'token' is expected to be a string. Received ${typeof options.token}`
`Error: 'token' is expected to be a string. Received ${typeof clientOptions.token}`
);
throw new DeploymentError({
@@ -49,7 +45,8 @@ export default function buildCreateDeployment(
});
}
const isDirectory = !Array.isArray(path) && lstatSync(path).isDirectory();
clientOptions.isDirectory =
!Array.isArray(path) && lstatSync(path).isDirectory();
let rootFiles: string[];
@@ -69,7 +66,7 @@ export default function buildCreateDeployment(
});
}
if (isDirectory && !Array.isArray(path)) {
if (clientOptions.isDirectory && !Array.isArray(path)) {
debug(`Provided 'path' is a directory. Reading subpaths... `);
rootFiles = await readRootFolder(path);
debug(`Read ${rootFiles.length} subpaths`);
@@ -90,7 +87,7 @@ export default function buildCreateDeployment(
debug('Building file tree...');
if (isDirectory && !Array.isArray(path)) {
if (clientOptions.isDirectory && !Array.isArray(path)) {
// Directory path
const dirContents = await readdir(path, ignores);
const relativeFileList = dirContents.map(filePath =>
@@ -156,15 +153,14 @@ export default function buildCreateDeployment(
// from getting confused about a deployment that renders 404.
if (
fileList.length === 0 ||
fileList.every((item): boolean => {
if (!item) {
return true;
}
const segments = item.split('/');
return segments[segments.length - 1].startsWith('.');
})
fileList.every(item =>
item
? item
.split('/')
.pop()!
.startsWith('.')
: true
)
) {
debug(
`Deployment path has no files (or only dotfiles). Yielding a warning event`
@@ -181,39 +177,24 @@ export default function buildCreateDeployment(
debug(`Yielding a 'hashes-calculated' event with ${files.size} hashes`);
yield { type: 'hashes-calculated', payload: mapToObject(files) };
const {
token,
teamId,
force,
defaultName,
debug: debug_,
apiUrl,
...metadata
} = options;
if (clientOptions.apiUrl) {
debug(`Using provided API URL: ${clientOptions.apiUrl}`);
}
if (apiUrl) {
debug(`Using provided API URL: ${apiUrl}`);
if (clientOptions.userAgent) {
debug(`Using provided user agent: ${clientOptions.userAgent}`);
}
debug(`Setting platform version to ${version}`);
metadata.version = version;
const deploymentOpts = {
debug: debug_,
totalFiles: files.size,
nowConfig,
token,
isDirectory,
path,
teamId,
force,
defaultName,
metadata,
apiUrl,
};
deploymentOptions.version = version;
debug(`Creating the deployment and starting upload...`);
for await (const event of uploadAndDeploy(files, deploymentOpts)) {
for await (const event of upload(
files,
nowConfig,
clientOptions,
deploymentOptions
)) {
debug(`Yielding a '${event.type}' event`);
yield event;
}

View File

@@ -5,51 +5,41 @@ import {
createDebug,
getApiDeploymentsUrl,
} from './utils';
import checkDeploymentStatus from './deployment-status';
import { checkDeploymentStatus } from './check-deployment-status';
import { generateQueryString } from './utils/query-string';
import { Deployment, DeploymentOptions, NowJsonOptions } from './types';
import { isReady, isAliasAssigned } from './utils/ready-state';
export interface Options {
metadata: DeploymentOptions;
totalFiles: number;
path: string | string[];
token: string;
teamId?: string;
force?: boolean;
isDirectory?: boolean;
defaultName?: string;
preflight?: boolean;
debug?: boolean;
nowConfig?: NowJsonOptions;
apiUrl?: string;
}
import {
Deployment,
DeploymentOptions,
NowConfig,
NowClientOptions,
} from './types';
async function* createDeployment(
metadata: DeploymentOptions,
files: Map<string, DeploymentFile>,
options: Options,
debug: Function
clientOptions: NowClientOptions,
deploymentOptions: DeploymentOptions
): AsyncIterableIterator<{ type: string; payload: any }> {
const preparedFiles = prepareFiles(files, options);
const apiDeployments = getApiDeploymentsUrl(metadata);
const debug = createDebug(clientOptions.debug);
const preparedFiles = prepareFiles(files, clientOptions);
const apiDeployments = getApiDeploymentsUrl(deploymentOptions);
debug('Sending deployment creation API request');
try {
const dpl = await fetch(
`${apiDeployments}${generateQueryString(options)}`,
options.token,
`${apiDeployments}${generateQueryString(clientOptions)}`,
clientOptions.token,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...metadata,
...deploymentOptions,
files: preparedFiles,
}),
apiUrl: options.apiUrl,
apiUrl: clientOptions.apiUrl,
userAgent: clientOptions.userAgent,
}
);
@@ -85,87 +75,88 @@ async function* createDeployment(
}
}
const getDefaultName = (
path: string | string[] | undefined,
isDirectory: boolean | undefined,
function getDefaultName(
files: Map<string, DeploymentFile>,
debug: Function
): string => {
clientOptions: NowClientOptions
): string {
const debug = createDebug(clientOptions.debug);
const { isDirectory, path } = clientOptions;
if (isDirectory && typeof path === 'string') {
debug('Provided path is a directory. Using last segment as default name');
const segments = path.split('/');
return segments[segments.length - 1];
return path.split('/').pop()!;
} else {
debug(
'Provided path is not a directory. Using last segment of the first file as default name'
);
const filePath = Array.from(files.values())[0].names[0];
const segments = filePath.split('/');
return segments[segments.length - 1];
return filePath.split('/').pop()!;
}
};
}
export default async function* deploy(
export async function* deploy(
files: Map<string, DeploymentFile>,
options: Options
nowConfig: NowConfig,
clientOptions: NowClientOptions,
deploymentOptions: DeploymentOptions
): AsyncIterableIterator<{ type: string; payload: any }> {
const debug = createDebug(options.debug);
const nowJsonMetadata = options.nowConfig || {};
delete nowJsonMetadata.github;
delete nowJsonMetadata.scope;
const meta = options.metadata || {};
const metadata = { ...nowJsonMetadata, ...meta };
const debug = createDebug(clientOptions.debug);
// Check if we should default to a static deployment
if (!metadata.version && !metadata.name) {
metadata.version = 2;
metadata.name =
options.totalFiles === 1
? 'file'
: getDefaultName(options.path, options.isDirectory, files, debug);
if (!deploymentOptions.version && !deploymentOptions.name) {
deploymentOptions.version = 2;
deploymentOptions.name =
files.size === 1 ? 'file' : getDefaultName(files, clientOptions);
if (metadata.name === 'file') {
if (deploymentOptions.name === 'file') {
debug('Setting deployment name to "file" for single-file deployment');
}
}
if (options.totalFiles === 1 && !metadata.builds && !metadata.routes) {
if (
files.size === 1 &&
!deploymentOptions.builds &&
!deploymentOptions.routes
) {
debug(`Assigning '/' route for single file deployment`);
const filePath = Array.from(files.values())[0].names[0];
const segments = filePath.split('/');
metadata.routes = [
deploymentOptions.routes = [
{
src: '/',
dest: `/${segments[segments.length - 1]}`,
dest: `/${filePath.split('/').pop()}`,
},
];
}
if (!metadata.name) {
metadata.name =
options.defaultName ||
getDefaultName(options.path, options.isDirectory, files, debug);
debug('No name provided. Defaulting to', metadata.name);
if (!deploymentOptions.name) {
deploymentOptions.name =
clientOptions.defaultName || getDefaultName(files, clientOptions);
debug('No name provided. Defaulting to', deploymentOptions.name);
}
if (metadata.version === 1 && !metadata.deploymentType) {
debug(`Setting 'type' for 1.0 deployment to '${nowJsonMetadata.type}'`);
metadata.deploymentType = nowJsonMetadata.type;
if (
deploymentOptions.version === 1 &&
!deploymentOptions.deploymentType &&
nowConfig.type
) {
debug(`Setting 'type' for 1.0 deployment to '${nowConfig.type}'`);
deploymentOptions.deploymentType = nowConfig.type.toUpperCase() as DeploymentOptions['deploymentType'];
}
if (metadata.version === 1) {
if (deploymentOptions.version === 1 && !deploymentOptions.config) {
debug(`Writing 'config' values for 1.0 deployment`);
const nowConfig = { ...nowJsonMetadata };
delete nowConfig.version;
deploymentOptions.config = { ...nowConfig };
delete deploymentOptions.config.version;
}
metadata.config = {
...nowConfig,
...metadata.config,
};
if (
deploymentOptions.version === 1 &&
!deploymentOptions.forceNew &&
clientOptions.force
) {
debug(`Setting 'forceNew' for 1.0 deployment`);
deploymentOptions.forceNew = clientOptions.force;
}
let deployment: Deployment | undefined;
@@ -173,10 +164,9 @@ export default async function* deploy(
try {
debug('Creating deployment');
for await (const event of createDeployment(
metadata,
files,
options,
debug
clientOptions,
deploymentOptions
)) {
if (event.type === 'created') {
debug('Deployment created');
@@ -203,11 +193,7 @@ export default async function* deploy(
debug('Waiting for deployment to be ready...');
for await (const event of checkDeploymentStatus(
deployment,
options.token,
metadata.version,
options.teamId,
debug,
options.apiUrl
clientOptions
)) {
yield event;
}

View File

@@ -1,18 +1,25 @@
import { BuilderFunctions } from '@now/build-utils';
import { Builder, BuilderFunctions } from '@now/build-utils';
import { NowHeader, Route, NowRedirect, NowRewrite } from '@now/routing-utils';
export interface Route {
src: string;
dest: string;
headers?: {
[key: string]: string;
};
status?: number;
methods?: string[];
export interface Dictionary<T> {
[key: string]: T;
}
export interface Build {
src: string;
use: string;
/**
* Options for `now-client` or
* properties that should not
* be part of the payload.
*/
export interface NowClientOptions {
token: string;
path: string | string[];
debug?: boolean;
teamId?: string;
apiUrl?: string;
force?: boolean;
userAgent?: string;
defaultName?: string;
isDirectory?: boolean;
}
export interface Deployment {
@@ -20,13 +27,11 @@ export interface Deployment {
deploymentId?: string;
url: string;
name: string;
meta: {
[key: string]: string | number | boolean;
};
meta: Dictionary<string | number | boolean>;
version: number;
regions: string[];
routes: Route[];
builds?: Build[];
builds?: Builder[];
functions?: BuilderFunctions;
plan: string;
public: boolean;
@@ -47,13 +52,9 @@ export interface Deployment {
| 'ERROR';
createdAt: string;
createdIn: string;
env: {
[key: string]: string;
};
env: Dictionary<string>;
build: {
env: {
[key: string]: string;
};
env: Dictionary<string>;
};
target: string;
alias: string[];
@@ -91,51 +92,69 @@ export interface DeploymentGithubData {
autoJobCancelation: boolean;
}
export interface DeploymentOptions {
interface LegacyNowConfig {
type?: string;
aliases?: string | string[];
}
export interface NowConfig extends LegacyNowConfig {
name?: string;
version?: number;
env?: Dictionary<string>;
build?: {
env?: Dictionary<string>;
};
builds?: Builder[];
routes?: Route[];
files?: string[];
cleanUrls?: boolean;
rewrites?: NowRewrite[];
redirects?: NowRedirect[];
headers?: NowHeader[];
trailingSlash?: boolean;
functions?: BuilderFunctions;
github?: DeploymentGithubData;
scope?: string;
alias?: string | string[];
}
interface LegacyDeploymentOptions {
project?: string;
forceNew?: boolean;
description?: string;
registryAuthToken?: string;
engines?: Dictionary<string>;
sessionAffinity?: 'ip' | 'key' | 'random';
deploymentType?: 'NPM' | 'STATIC' | 'DOCKER';
scale?: Dictionary<{
min?: number;
max?: number | 'auto';
}>;
limits?: {
duration?: number;
maxConcurrentReqs?: number;
timeout?: number;
};
// Can't be NowConfig, since we don't
// include all legacy types here
config?: Dictionary<any>;
}
/**
* Options that will be sent to the API.
*/
export interface DeploymentOptions extends LegacyDeploymentOptions {
version?: number;
regions?: string[];
routes?: Route[];
builds?: Build[];
builds?: Builder[];
functions?: BuilderFunctions;
env?: {
[key: string]: string;
};
env?: Dictionary<string>;
build?: {
env: {
[key: string]: string;
};
env: Dictionary<string>;
};
target?: string;
token?: string | null;
teamId?: string;
force?: boolean;
name?: string;
defaultName?: string;
isDirectory?: boolean;
path?: string | string[];
github?: DeploymentGithubData;
scope?: string;
public?: boolean;
forceNew?: boolean;
deploymentType?: 'NPM' | 'STATIC' | 'DOCKER';
registryAuthToken?: string;
engines?: { [key: string]: string };
sessionAffinity?: 'ip' | 'random';
config?: { [key: string]: any };
debug?: boolean;
apiUrl?: string;
meta?: Dictionary<string>;
}
export interface NowJsonOptions {
github?: DeploymentGithubData;
scope?: string;
type?: 'NPM' | 'STATIC' | 'DOCKER';
version?: number;
files?: string[];
}
export type CreateDeploymentFunction = (
path: string | string[],
options?: DeploymentOptions,
nowConfig?: NowJsonOptions
) => AsyncIterableIterator<any>;

View File

@@ -4,8 +4,9 @@ import retry from 'async-retry';
import { Sema } from 'async-sema';
import { DeploymentFile } from './utils/hashes';
import { fetch, API_FILES, createDebug } from './utils';
import { DeploymentError } from '.';
import deploy, { Options } from './deploy';
import { DeploymentError } from './errors';
import { deploy } from './deploy';
import { NowConfig, NowClientOptions, DeploymentOptions } from './types';
const isClientNetworkError = (err: Error | DeploymentError) => {
if (err.message) {
@@ -24,12 +25,14 @@ const isClientNetworkError = (err: Error | DeploymentError) => {
return false;
};
export default async function* upload(
export async function* upload(
files: Map<string, DeploymentFile>,
options: Options
nowConfig: NowConfig,
clientOptions: NowClientOptions,
deploymentOptions: DeploymentOptions
): AsyncIterableIterator<any> {
const { token, teamId, debug: isDebug, apiUrl } = options;
const debug = createDebug(isDebug);
const { token, teamId, apiUrl, userAgent } = clientOptions;
const debug = createDebug(clientOptions.debug);
if (!files && !token && !teamId) {
debug(`Neither 'files', 'token' nor 'teamId are present. Exiting`);
@@ -40,7 +43,12 @@ export default async function* upload(
debug('Determining necessary files for upload...');
for await (const event of deploy(files, options)) {
for await (const event of deploy(
files,
nowConfig,
clientOptions,
deploymentOptions
)) {
if (event.type === 'error') {
if (event.payload.code === 'missing_files') {
missingFiles = event.payload.missing;
@@ -105,8 +113,9 @@ export default async function* upload(
body: stream,
teamId,
apiUrl,
userAgent,
},
isDebug
clientOptions.debug
);
if (res.status === 200) {
@@ -185,7 +194,12 @@ export default async function* upload(
try {
debug('Starting deployment creation');
for await (const event of deploy(files, options)) {
for await (const event of deploy(
files,
nowConfig,
clientOptions,
deploymentOptions
)) {
if (event.type === 'alias-assigned') {
debug('Deployment is ready');
return yield event;

View File

@@ -1,12 +1,11 @@
import { DeploymentFile } from './hashes';
import { parse as parseUrl } from 'url';
import fetch_ from 'node-fetch';
import fetch_, { RequestInit } from 'node-fetch';
import { join, sep } from 'path';
import qs from 'querystring';
import ignore from 'ignore';
import { pkgVersion } from '../pkg';
import { Options } from '../deploy';
import { NowJsonOptions, DeploymentOptions } from '../types';
import { NowClientOptions, DeploymentOptions, NowConfig } from '../types';
import { Sema } from 'async-sema';
import { readFile } from 'fs-extra';
const semaphore = new Sema(10);
@@ -44,7 +43,7 @@ export function getApiDeploymentsUrl(
return '/v11/now/deployments';
}
export async function parseNowJSON(filePath?: string): Promise<NowJsonOptions> {
export async function parseNowJSON(filePath?: string): Promise<NowConfig> {
if (!filePath) {
return {};
}
@@ -111,10 +110,18 @@ export async function getNowIgnore(path: string | string[]): Promise<any> {
return { ig, ignores };
}
interface FetchOpts extends RequestInit {
apiUrl?: string;
method?: string;
teamId?: string;
headers?: { [key: string]: any };
userAgent?: string;
}
export const fetch = async (
url: string,
token: string,
opts: any = {},
opts: FetchOpts = {},
debugEnabled?: boolean
): Promise<any> => {
semaphore.acquire();
@@ -133,11 +140,14 @@ export const fetch = async (
delete opts.teamId;
}
const userAgent = opts.userAgent || `now-client-v${pkgVersion}`;
delete opts.userAgent;
opts.headers = {
...opts.headers,
authorization: `Bearer ${token}`,
accept: 'application/json',
'user-agent': `now-client-v${pkgVersion}`,
'user-agent': userAgent,
};
debug(`${opts.method || 'GET'} ${url}`);
@@ -160,7 +170,7 @@ const isWin = process.platform.includes('win');
export const prepareFiles = (
files: Map<string, DeploymentFile>,
options: Options
clientOptions: NowClientOptions
): PreparedFile[] => {
const preparedFiles = [...files.keys()].reduce(
(acc: PreparedFile[], sha: string): PreparedFile[] => {
@@ -171,10 +181,10 @@ export const prepareFiles = (
for (const name of file.names) {
let fileName: string;
if (options.isDirectory) {
if (clientOptions.isDirectory) {
// Directory
fileName = options.path
? name.substring(options.path.length + 1)
fileName = clientOptions.path
? name.substring(clientOptions.path.length + 1)
: name;
} else {
// Array of files or single file

View File

@@ -1,13 +1,16 @@
import { Options } from '../deploy';
import { URLSearchParams } from 'url';
import { NowClientOptions } from '../types';
export const generateQueryString = (options: Options): string => {
if (options.force && options.teamId) {
return `?teamId=${options.teamId}&forceNew=1`;
} else if (options.teamId) {
return `?teamId=${options.teamId}`;
} else if (options.force) {
return `?forceNew=1`;
export function generateQueryString(clientOptions: NowClientOptions): string {
const options = new URLSearchParams();
if (clientOptions.teamId) {
options.set('teamId', clientOptions.teamId);
}
return '';
};
if (clientOptions.force) {
options.set('forceNew', '1');
}
return Array.from(options.entries()).length ? `?${options.toString()}` : '';
}

View File

@@ -28,10 +28,12 @@ describe('create v2 deployment', () => {
it('will display an empty deployment warning', async () => {
for await (const event of createDeployment(
path.resolve(__dirname, 'fixtures', 'v2'),
{
token,
name: 'now-client-tests-v2',
path: path.resolve(__dirname, 'fixtures', 'v2'),
},
{
name: 'now-clien-tests-v2',
}
)) {
if (event.type === 'warning') {
@@ -47,9 +49,11 @@ describe('create v2 deployment', () => {
it('will report correct file count event', async () => {
for await (const event of createDeployment(
path.resolve(__dirname, 'fixtures', 'v2'),
{
token,
path: path.resolve(__dirname, 'fixtures', 'v2'),
},
{
name: 'now-client-tests-v2',
}
)) {
@@ -66,9 +70,11 @@ describe('create v2 deployment', () => {
it('will create a v2 deployment', async () => {
for await (const event of createDeployment(
path.resolve(__dirname, 'fixtures', 'v2'),
{
token,
path: path.resolve(__dirname, 'fixtures', 'v2'),
},
{
name: 'now-client-tests-v2',
}
)) {
@@ -82,9 +88,11 @@ describe('create v2 deployment', () => {
it('will create a v2 deployment with correct file permissions', async () => {
for await (const event of createDeployment(
path.resolve(__dirname, 'fixtures', 'v2-file-permissions'),
{
token,
path: path.resolve(__dirname, 'fixtures', 'v2-file-permissions'),
},
{
name: 'now-client-tests-v2',
}
)) {
@@ -104,10 +112,12 @@ describe('create v2 deployment', () => {
it('will create a v2 deployment and ignore files specified in .nowignore', async () => {
for await (const event of createDeployment(
path.resolve(__dirname, 'fixtures', 'nowignore'),
{
token,
name: 'now-client-tests-v2-ignore',
path: path.resolve(__dirname, 'fixtures', 'nowignore'),
},
{
name: 'now-client-tests-v2',
}
)) {
if (event.type === 'ready') {

View File

@@ -29,9 +29,11 @@ describe('create v1 deployment', () => {
it('will create a v1 static deployment', async () => {
for await (const event of createLegacyDeployment(
path.resolve(__dirname, 'fixtures', 'v1', 'static'),
{
token,
path: path.resolve(__dirname, 'fixtures', 'v1', 'static'),
},
{
name: 'now-client-tests-v1-static',
}
)) {
@@ -47,9 +49,11 @@ describe('create v1 deployment', () => {
it('will create a v1 npm deployment', async () => {
for await (const event of createLegacyDeployment(
path.resolve(__dirname, 'fixtures', 'v1', 'npm'),
{
token,
path: path.resolve(__dirname, 'fixtures', 'v1', 'npm'),
},
{
name: 'now-client-tests-v1-npm',
}
)) {
@@ -65,9 +69,11 @@ describe('create v1 deployment', () => {
it('will create a v1 Docker deployment', async () => {
for await (const event of createLegacyDeployment(
path.resolve(__dirname, 'fixtures', 'v1', 'docker'),
{
token,
path: path.resolve(__dirname, 'fixtures', 'v1', 'docker'),
},
{
name: 'now-client-tests-v1-docker',
}
)) {

View File

@@ -10,10 +10,15 @@ describe('path handling', () => {
it('will fali with a relative path', async () => {
try {
await createDeployment('./fixtures/v2/now.json', {
token,
name: 'now-client-tests-v2',
});
await createDeployment(
{
token,
path: './fixtures/v2/now.json',
},
{
name: 'now-client-tests-v2',
}
);
} catch (e) {
expect(e.code).toEqual('invalid_path');
}
@@ -21,10 +26,15 @@ describe('path handling', () => {
it('will fali with an array of relative paths', async () => {
try {
await createDeployment(['./fixtures/v2/now.json'], {
token,
name: 'now-client-tests-v2',
});
await createDeployment(
{
token,
path: ['./fixtures/v2/now.json'],
},
{
name: 'now-client-tests-v2',
}
);
} catch (e) {
expect(e.code).toEqual('invalid_path');
}

View File

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

View File

@@ -1,5 +1,4 @@
import { ChildProcess, fork } from 'child_process';
import url from 'url'
import {
pathExists,
readFile,
@@ -60,8 +59,8 @@ import {
import {
convertRedirects,
convertRewrites
} from '@now/routing-utils/dist/superstatic'
convertRewrites,
} from '@now/routing-utils/dist/superstatic';
interface BuildParamsMeta {
isDev: boolean | undefined;
@@ -77,7 +76,7 @@ interface BuildParamsType extends BuildOptions {
}
export const version = 2;
const htmlContentType = 'text/html; charset=utf-8';
const nowDevChildProcesses = new Set<ChildProcess>();
['SIGINT', 'SIGTERM'].forEach(signal => {
@@ -353,7 +352,6 @@ export const build = async ({
await unlinkFile(path.join(entryPath, '.npmrc'));
}
const exportedPageRoutes: Route[] = [];
const lambdas: { [key: string]: Lambda } = {};
const prerenders: { [key: string]: Prerender | FileFsRef } = {};
const staticPages: { [key: string]: FileFsRef } = {};
@@ -484,18 +482,14 @@ export const build = async ({
return;
}
const staticRoute = path.join(entryDirectory, page);
const staticRoute = path.join(entryDirectory, pathname);
staticPages[staticRoute] = staticPageFiles[page];
staticPages[staticRoute].contentType = htmlContentType;
if (isDynamicRoute(pathname)) {
dynamicPages.push(routeName);
return;
}
exportedPageRoutes.push({
src: `^${path.join('/', entryDirectory, pathname)}$`,
dest: path.join('/', staticRoute),
});
});
const pageKeys = Object.keys(pages);
@@ -743,14 +737,9 @@ export const build = async ({
if (htmlFsRef == null || jsonFsRef == null) {
throw new Error('invariant: htmlFsRef != null && jsonFsRef != null');
}
const outputPathPageHtml = outputPathPage.concat('.html');
prerenders[outputPathPageHtml] = htmlFsRef;
htmlFsRef.contentType = htmlContentType;
prerenders[outputPathPage] = htmlFsRef;
prerenders[outputPathData] = jsonFsRef;
exportedPageRoutes.push({
src: path.posix.join('/', outputPathPage),
dest: outputPathPageHtml,
});
} else {
const lambda = lambdas[outputSrcPathPage];
if (lambda == null) {
@@ -832,7 +821,7 @@ export const build = async ({
let dynamicPrefix = path.join('/', entryDirectory);
dynamicPrefix = dynamicPrefix === '/' ? '' : dynamicPrefix;
const routesManifest = await getRoutesManifest(entryPath, realNextVersion)
const routesManifest = await getRoutesManifest(entryPath, realNextVersion);
const dynamicRoutes = await getDynamicRoutes(
entryPath,
@@ -842,25 +831,20 @@ export const build = async ({
routesManifest
).then(arr =>
arr.map(route => {
// make sure .html is added to dest for now until
// outputting static files to clean routes is available
if (staticPages[`${route.dest}.html`.substr(1)]) {
route.dest = `${route.dest}.html`;
}
route.src = route.src.replace('^', `^${dynamicPrefix}`);
return route;
})
);
const rewrites: Route[] = []
const redirects: Route[] = []
const rewrites: Route[] = [];
const redirects: Route[] = [];
if (routesManifest) {
switch(routesManifest.version) {
switch (routesManifest.version) {
case 1: {
redirects.push(...convertRedirects(routesManifest.redirects))
rewrites.push(...convertRewrites(routesManifest.rewrites))
break
redirects.push(...convertRedirects(routesManifest.redirects));
rewrites.push(...convertRewrites(routesManifest.rewrites));
break;
}
default: {
// update MIN_ROUTES_MANIFEST_VERSION in ./utils.ts
@@ -872,24 +856,6 @@ export const build = async ({
}
}
const topRoutes = [
// Before we handle static files we need to set proper caching headers
{
// This ensures we only match known emitted-by-Next.js files and not
// user-emitted files which may be missing a hash in their filename.
src: path.join(
'/',
entryDirectory,
'_next/static/(?:[^/]+/pages|chunks|runtime|css|media)/.+'
),
// Next.js assets contain a hash or entropy in their filenames, so they
// are guaranteed to be unique and cacheable indefinitely.
headers: { 'cache-control': 'public,max-age=31536000,immutable' },
continue: true,
},
{ src: path.join('/', entryDirectory, '_next(?!/data(?:/|$))(?:/.*)?') },
]
return {
output: {
...publicDirectoryFiles,
@@ -901,17 +867,27 @@ export const build = async ({
...staticDirectoryFiles,
},
routes: [
...topRoutes,
// redirects take the highest priority
...redirects,
...rewrites,
// we need to re-apply the routes above rewrites in-case the are
// rewriting to one of those routes
...topRoutes,
// Static exported pages (.html rewrites)
...exportedPageRoutes,
// Before we handle static files we need to set proper caching headers
{
// This ensures we only match known emitted-by-Next.js files and not
// user-emitted files which may be missing a hash in their filename.
src: path.join(
'/',
entryDirectory,
'_next/static/(?:[^/]+/pages|chunks|runtime|css|media)/.+'
),
// Next.js assets contain a hash or entropy in their filenames, so they
// are guaranteed to be unique and cacheable indefinitely.
headers: { 'cache-control': 'public,max-age=31536000,immutable' },
continue: true,
},
{ src: path.join('/', entryDirectory, '_next(?!/data(?:/|$))(?:/.*)?') },
// Next.js page lambdas, `static/` folder, reserved assets, and `public/`
// folder
{ handle: 'filesystem' },
...rewrites,
// Dynamic routes
...dynamicRoutes,
...dynamicDataRoutes,
@@ -934,7 +910,7 @@ export const build = async ({
export const prepareCache = async ({
workPath,
entrypoint,
}: PrepareCacheOptions) => {
}: PrepareCacheOptions): Promise<Files> => {
debug('Preparing cache...');
const entryDirectory = path.dirname(entrypoint);
const entryPath = path.join(workPath, entryDirectory);
@@ -954,8 +930,6 @@ export const prepareCache = async ({
const cache = {
...(await glob(path.join(cacheEntrypoint, 'node_modules/**'), workPath)),
...(await glob(path.join(cacheEntrypoint, '.next/cache/**'), workPath)),
...(await glob(path.join(cacheEntrypoint, 'package-lock.json'), workPath)),
...(await glob(path.join(cacheEntrypoint, 'yarn.lock'), workPath)),
};
debug('Cache file manifest produced');
return cache;

View File

@@ -293,37 +293,36 @@ async function getRoutes(
}
export type Rewrite = {
source: string,
destination: string,
}
source: string;
destination: string;
};
export type Redirect = Rewrite & {
statusCode?: number
}
statusCode?: number;
};
type RoutesManifestRegex = {
regex: string,
regexKeys: string[]
}
regex: string;
regexKeys: string[];
};
export type RoutesManifest = {
redirects: (Redirect & RoutesManifestRegex)[],
rewrites: (Rewrite & RoutesManifestRegex)[],
redirects: (Redirect & RoutesManifestRegex)[];
rewrites: (Rewrite & RoutesManifestRegex)[];
dynamicRoutes: {
page: string,
regex: string,
}[],
version: number
}
page: string;
regex: string;
}[];
version: number;
};
export async function getRoutesManifest(
entryPath: string,
nextVersion?: string,
): Promise< RoutesManifest | undefined> {
const shouldHaveManifest = (
nextVersion && semver.gte(nextVersion, '9.1.4-canary.0')
)
if (!shouldHaveManifest) return
nextVersion?: string
): Promise<RoutesManifest | undefined> {
const shouldHaveManifest =
nextVersion && semver.gte(nextVersion, '9.1.4-canary.0');
if (!shouldHaveManifest) return;
const pathRoutesManifest = path.join(
entryPath,
@@ -338,12 +337,12 @@ export async function getRoutesManifest(
if (shouldHaveManifest && !hasRoutesManifest) {
throw new Error(
`A routes-manifest.json couldn't be found. This could be due to a failure during the build`
)
);
}
const routesManifest: RoutesManifest = require(pathRoutesManifest)
const routesManifest: RoutesManifest = require(pathRoutesManifest);
return routesManifest
return routesManifest;
}
export async function getDynamicRoutes(

View File

@@ -14,7 +14,7 @@ it(
const {
buildResult: { output },
} = await runBuildLambda(path.join(__dirname, 'standard'));
expect(output['index.html']).toBeDefined();
expect(output['index']).toBeDefined();
expect(output.goodbye).toBeDefined();
const filePaths = Object.keys(output);
const serverlessError = filePaths.some(filePath =>
@@ -274,7 +274,7 @@ it(
buildResult: { output },
} = await runBuildLambda(path.join(__dirname, 'serverless-config-object'));
expect(output['index.html']).toBeDefined();
expect(output['index']).toBeDefined();
expect(output.goodbye).toBeDefined();
const filePaths = Object.keys(output);
const serverlessError = filePaths.some(filePath =>
@@ -308,7 +308,7 @@ it(
buildResult: { output },
} = await runBuildLambda(path.join(__dirname, 'serverless-no-config'));
expect(output['index.html']).toBeDefined();
expect(output['index']).toBeDefined();
expect(output.goodbye).toBeDefined();
const filePaths = Object.keys(output);
const serverlessError = filePaths.some(filePath =>
@@ -344,7 +344,7 @@ it(
path.join(__dirname, 'serverless-no-config-build')
);
expect(output['index.html']).toBeDefined();
expect(output['index']).toBeDefined();
const filePaths = Object.keys(output);
const serverlessError = filePaths.some(filePath =>
filePath.match(/_error/)

View File

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

View File

@@ -393,10 +393,9 @@ export async function build({
return { output: lambda, watch };
}
export async function prepareCache({ workPath }: PrepareCacheOptions) {
return {
...(await glob('node_modules/**', workPath)),
...(await glob('package-lock.json', workPath)),
...(await glob('yarn.lock', workPath)),
};
export async function prepareCache({
workPath,
}: PrepareCacheOptions): Promise<Files> {
const cache = await glob('node_modules/**', workPath);
return cache;
}

View File

@@ -1,6 +1,6 @@
{
"name": "@now/routing-utils",
"version": "1.3.4-canary.2",
"version": "1.4.0",
"description": "ZEIT Now routing utilities",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -20,7 +20,7 @@
"test-unit": "jest --env node --verbose --runInBand"
},
"dependencies": {
"path-to-regexp": "3.1.0"
"path-to-regexp": "6.1.0"
},
"devDependencies": {
"ajv": "^6.0.0",

View File

@@ -17,6 +17,7 @@ import {
} from './superstatic';
export { getCleanUrls } from './superstatic';
export { mergeRoutes } from './merge';
export function isHandler(route: Route): route is Handler {
return typeof (route as Handler).handle !== 'undefined';

View File

@@ -0,0 +1,104 @@
import { Route, MergeRoutesProps } from './types';
import { isHandler } from './index';
interface BuilderToRoute {
[use: string]: Route[];
}
interface BuilderRoutes {
[entrypoint: string]: BuilderToRoute;
}
function getCheckAndContinue(
routes: Route[]
): { checks: Route[]; continues: Route[]; others: Route[] } {
const checks: Route[] = [];
const continues: Route[] = [];
const others: Route[] = [];
for (const route of routes) {
if (isHandler(route)) {
// Should never happen, only here to make TS happy
others.push(route);
} else if (route.check) {
checks.push(route);
} else if (route.continue) {
continues.push(route);
} else {
others.push(route);
}
}
return { checks, continues, others };
}
export function mergeRoutes({ userRoutes, builds }: MergeRoutesProps): Route[] {
const usersRoutesBefore: Route[] = [];
const usersRoutesAfter: Route[] = [];
const builderRoutes: BuilderRoutes = {};
const builderRoutesBefore: Route[] = [];
const builderRoutesAfter: Route[] = [];
let foundUserDefinedFilesystem = false;
(userRoutes || []).forEach(route => {
if (!foundUserDefinedFilesystem) {
if (isHandler(route) && route.handle === 'filesystem') {
foundUserDefinedFilesystem = true;
} else {
usersRoutesBefore.push(route);
}
} else {
usersRoutesAfter.push(route);
}
});
// Convert build results to object mapping
for (const build of builds) {
if (build.routes) {
if (!builderRoutes[build.entrypoint]) {
builderRoutes[build.entrypoint] = {};
}
builderRoutes[build.entrypoint][build.use] = build.routes.map(route => {
//route.built = true; // TODO: is this necessary?
return route;
});
}
}
const sortedPaths = Object.keys(builderRoutes).sort();
sortedPaths.forEach(path => {
const br = builderRoutes[path];
const sortedBuilders = Object.keys(br).sort();
sortedBuilders.forEach(use => {
let isBefore = true;
br[use].forEach(route => {
if (isBefore) {
if (isHandler(route) && route.handle === 'filesystem') {
isBefore = false;
} else {
builderRoutesBefore.push(route);
}
} else {
builderRoutesAfter.push(route);
}
});
});
});
const builderBefore = getCheckAndContinue(builderRoutesBefore);
const builderAfter = getCheckAndContinue(builderRoutesAfter);
const outputRoutes: Route[] = [];
outputRoutes.push(...builderBefore.continues);
outputRoutes.push(...usersRoutesBefore);
outputRoutes.push(...builderBefore.checks);
outputRoutes.push(...builderBefore.others);
if (usersRoutesAfter.length > 0 || builderRoutesAfter.length > 0) {
outputRoutes.push({ handle: 'filesystem' });
}
outputRoutes.push(...builderAfter.continues);
outputRoutes.push(...usersRoutesAfter);
outputRoutes.push(...builderAfter.checks);
outputRoutes.push(...builderAfter.others);
return outputRoutes;
}

View File

@@ -43,6 +43,9 @@ export const routesSchema = {
continue: {
type: 'boolean',
},
check: {
type: 'boolean',
},
status: {
type: 'integer',
minimum: 100,

View File

@@ -3,7 +3,7 @@
* See https://github.com/firebase/superstatic#configuration
*/
import pathToRegexp from 'path-to-regexp';
import { pathToRegexp, Key } from 'path-to-regexp';
import { Route, NowRedirect, NowRewrite, NowHeader } from './types';
export function getCleanUrls(
@@ -30,12 +30,12 @@ export function convertCleanUrls(
routes.push({
src: '^/(?:(.+)/)?index(?:\\.html)?/?$',
headers: { Location: loc },
status: 301,
status: 308,
});
routes.push({
src: '^/(.*)\\.html/?$',
headers: { Location: loc },
status: 301,
status: 308,
});
}
return routes;
@@ -45,21 +45,22 @@ export function convertRedirects(redirects: NowRedirect[]): Route[] {
return redirects.map(r => {
const { src, segments } = sourceToRegex(r.source);
const loc = replaceSegments(segments, r.destination);
return {
const route: Route = {
src,
headers: { Location: loc },
status: r.statusCode || 307,
status: r.statusCode || 308,
};
return route;
});
}
export function convertRewrites(rewrites: NowRewrite[]): Route[] {
const routes: Route[] = rewrites.map(r => {
return rewrites.map(r => {
const { src, segments } = sourceToRegex(r.source);
const dest = replaceSegments(segments, r.destination);
return { src, dest, continue: true };
const route: Route = { src, dest, check: true };
return route;
});
return routes;
}
export function convertHeaders(headers: NowHeader[]): Route[] {
@@ -68,11 +69,12 @@ export function convertHeaders(headers: NowHeader[]): Route[] {
h.headers.forEach(kv => {
obj[kv.key] = kv.value;
});
return {
const route: Route = {
src: h.source,
headers: obj,
continue: true,
};
return route;
});
}
@@ -82,20 +84,20 @@ export function convertTrailingSlash(enable: boolean): Route[] {
routes.push({
src: '^/(.*[^\\/])$',
headers: { Location: '/$1/' },
status: 307,
status: 308,
});
} else {
routes.push({
src: '^/(.*)\\/$',
headers: { Location: '/$1' },
status: 307,
status: 308,
});
}
return routes;
}
function sourceToRegex(source: string): { src: string; segments: string[] } {
const keys: pathToRegexp.Key[] = [];
const keys: Key[] = [];
const r = pathToRegexp(source, keys, { strict: true });
const segments = keys.map(k => k.name).filter(isString);
return { src: r.source, segments };

View File

@@ -17,6 +17,7 @@ export type Source = {
headers?: { [name: string]: string };
methods?: string[];
continue?: boolean;
check?: boolean;
status?: number;
};
@@ -35,6 +36,17 @@ export interface GetRoutesProps {
nowConfig: NowConfig;
}
export interface MergeRoutesProps {
userRoutes?: Route[] | null | undefined;
builds: Build[];
}
export interface Build {
use: string;
entrypoint: string;
routes?: Route[];
}
export interface NowConfig {
name?: string;
version?: number;

View File

@@ -377,6 +377,28 @@ describe('normalizeRoutes', () => {
);
});
test('fails if check is not boolean', () => {
assertError(
[
// @ts-ignore
{
check: 'false',
},
],
[
{
dataPath: '[0].check',
keyword: 'type',
message: 'should be boolean',
params: {
type: 'boolean',
},
schemaPath: '#/items/properties/check/type',
},
]
);
});
test('fails if status is not number', () => {
assertError(
[
@@ -479,12 +501,12 @@ describe('getTransformedRoutes', () => {
{
src: '^/(?:(.+)/)?index(?:\\.html)?/?$',
headers: { Location: '/$1' },
status: 301,
status: 308,
},
{
src: '^/(.*)\\.html/?$',
headers: { Location: '/$1' },
status: 301,
status: 308,
},
{
src: '^/help$',
@@ -492,7 +514,7 @@ describe('getTransformedRoutes', () => {
status: 302,
},
{ handle: 'filesystem' },
{ src: '^/v1$', dest: '/v2/api.py', continue: true },
{ src: '^/v1$', dest: '/v2/api.py', check: true },
];
assert.deepEqual(actual.error, null);
assert.deepEqual(actual.routes, expected);

View File

@@ -0,0 +1,310 @@
const { deepEqual } = require('assert');
const { mergeRoutes } = require('../dist/merge');
test('mergeRoutes simple', () => {
const userRoutes = [
{ src: '/user1', dest: '/u1' },
{ src: '/user2', dest: '/u2' },
];
const builds = [
{
use: '@now/node',
entrypoint: 'api/home.js',
routes: [{ src: '/node1', dest: '/n1' }, { src: '/node2', dest: '/n2' }],
},
{
use: '@now/python',
entrypoint: 'api/users.py',
routes: [
{ src: '/python1', dest: '/py1' },
{ src: '/python2', dest: '/py2' },
],
},
];
const actual = mergeRoutes({ userRoutes, builds });
const expected = [
{ dest: '/u1', src: '/user1' },
{ dest: '/u2', src: '/user2' },
{ dest: '/n1', src: '/node1' },
{ dest: '/n2', src: '/node2' },
{ dest: '/py1', src: '/python1' },
{ dest: '/py2', src: '/python2' },
];
deepEqual(actual, expected);
});
test('mergeRoutes handle filesystem user routes', () => {
const userRoutes = [
{ src: '/user1', dest: '/u1' },
{ handle: 'filesystem' },
{ src: '/user2', dest: '/u2' },
];
const builds = [
{
use: '@now/node',
entrypoint: 'api/home.js',
routes: [{ src: '/node1', dest: '/n1' }, { src: '/node2', dest: '/n2' }],
},
{
use: '@now/python',
entrypoint: 'api/users.py',
routes: [
{ src: '/python1', dest: '/py1' },
{ src: '/python2', dest: '/py2' },
],
},
];
const actual = mergeRoutes({ userRoutes, builds });
const expected = [
{ dest: '/u1', src: '/user1' },
{ dest: '/n1', src: '/node1' },
{ dest: '/n2', src: '/node2' },
{ dest: '/py1', src: '/python1' },
{ dest: '/py2', src: '/python2' },
{ handle: 'filesystem' },
{ dest: '/u2', src: '/user2' },
];
deepEqual(actual, expected);
});
test('mergeRoutes handle filesystem build routes', () => {
const userRoutes = [
{ src: '/user1', dest: '/u1' },
{ src: '/user2', dest: '/u2' },
];
const builds = [
{
use: '@now/node',
entrypoint: 'api/home.js',
routes: [
{ src: '/node1', dest: '/n1' },
{ handle: 'filesystem' },
{ src: '/node2', dest: '/n2' },
],
},
{
use: '@now/python',
entrypoint: 'api/users.py',
routes: [
{ src: '/python1', dest: '/py1' },
{ handle: 'filesystem' },
{ src: '/python2', dest: '/py2' },
],
},
];
const actual = mergeRoutes({ userRoutes, builds });
const expected = [
{ dest: '/u1', src: '/user1' },
{ dest: '/u2', src: '/user2' },
{ dest: '/n1', src: '/node1' },
{ dest: '/py1', src: '/python1' },
{ handle: 'filesystem' },
{ dest: '/n2', src: '/node2' },
{ dest: '/py2', src: '/python2' },
];
deepEqual(actual, expected);
});
test('mergeRoutes handle filesystem both user and builds', () => {
const userRoutes = [
{ src: '/user1', dest: '/u1' },
{ handle: 'filesystem' },
{ src: '/user2', dest: '/u2' },
];
const builds = [
{
use: '@now/node',
entrypoint: 'api/home.js',
routes: [
{ src: '/node1', dest: '/n1' },
{ handle: 'filesystem' },
{ src: '/node2', dest: '/n2' },
],
},
{
use: '@now/python',
entrypoint: 'api/users.py',
routes: [
{ src: '/python1', dest: '/py1' },
{ handle: 'filesystem' },
{ src: '/python2', dest: '/py2' },
],
},
];
const actual = mergeRoutes({ userRoutes, builds });
const expected = [
{ dest: '/u1', src: '/user1' },
{ dest: '/n1', src: '/node1' },
{ dest: '/py1', src: '/python1' },
{ handle: 'filesystem' },
{ dest: '/u2', src: '/user2' },
{ dest: '/n2', src: '/node2' },
{ dest: '/py2', src: '/python2' },
];
deepEqual(actual, expected);
});
test('mergeRoutes continue true', () => {
const userRoutes = [
{ src: '/user1', dest: '/u1' },
{ src: '/user2', dest: '/u2', continue: true },
{ src: '/user3', dest: '/u3' },
];
const builds = [
{
use: '@now/node',
entrypoint: 'api/home.js',
routes: [
{ src: '/node1', dest: '/n1' },
{ src: '/node3', dest: '/n2', continue: true },
{ src: '/node3', dest: '/n3' },
],
},
{
use: '@now/python',
entrypoint: 'api/users.py',
routes: [
{ src: '/python1', dest: '/py1' },
{ src: '/python2', dest: '/py2', continue: true },
{ src: '/python3', dest: '/py3' },
],
},
];
const actual = mergeRoutes({ userRoutes, builds });
const expected = [
{ continue: true, dest: '/n2', src: '/node3' },
{ continue: true, dest: '/py2', src: '/python2' },
{ dest: '/u1', src: '/user1' },
{ continue: true, dest: '/u2', src: '/user2' },
{ dest: '/u3', src: '/user3' },
{ dest: '/n1', src: '/node1' },
{ dest: '/n3', src: '/node3' },
{ dest: '/py1', src: '/python1' },
{ dest: '/py3', src: '/python3' },
];
deepEqual(actual, expected);
});
test('mergeRoutes check true', () => {
const userRoutes = [
{ src: '/user1', dest: '/u1' },
{ src: '/user2', dest: '/u2' },
{ src: '/user3', dest: '/u3' },
];
const builds = [
{
use: '@now/node',
entrypoint: 'api/home.js',
routes: [
{ src: '/node1', dest: '/n1' },
{ src: '/node3', dest: '/n2', check: true },
{ src: '/node3', dest: '/n3' },
],
},
{
use: '@now/python',
entrypoint: 'api/users.py',
routes: [
{ src: '/python1', dest: '/py1' },
{ src: '/python2', dest: '/py2', check: true },
{ src: '/python3', dest: '/py3' },
],
},
];
const actual = mergeRoutes({ userRoutes, builds });
const expected = [
{ dest: '/u1', src: '/user1' },
{ dest: '/u2', src: '/user2' },
{ dest: '/u3', src: '/user3' },
{ check: true, dest: '/n2', src: '/node3' },
{ check: true, dest: '/py2', src: '/python2' },
{ dest: '/n1', src: '/node1' },
{ dest: '/n3', src: '/node3' },
{ dest: '/py1', src: '/python1' },
{ dest: '/py3', src: '/python3' },
];
deepEqual(actual, expected);
});
test('mergeRoutes check true, continue true, handle filesystem middle', () => {
const userRoutes = [
{ src: '/user1', dest: '/u1', continue: true },
{ src: '/user2', dest: '/u2' },
{ handle: 'filesystem' },
{ src: '/user3', dest: '/u3' },
];
const builds = [
{
use: '@now/node',
entrypoint: 'api/home.js',
routes: [
{ src: '/node1', dest: '/n1', continue: true },
{ src: '/node3', dest: '/n2', check: true },
{ handle: 'filesystem' },
{ src: '/node3', dest: '/n3' },
],
},
{
use: '@now/python',
entrypoint: 'api/users.py',
routes: [
{ src: '/python1', dest: '/py1', check: true },
{ src: '/python2', dest: '/py2', continue: true },
{ handle: 'filesystem' },
{ src: '/python3', dest: '/py3' },
],
},
];
const actual = mergeRoutes({ userRoutes, builds });
const expected = [
{ continue: true, dest: '/n1', src: '/node1' },
{ continue: true, dest: '/py2', src: '/python2' },
{ continue: true, dest: '/u1', src: '/user1' },
{ dest: '/u2', src: '/user2' },
{ check: true, dest: '/n2', src: '/node3' },
{ check: true, dest: '/py1', src: '/python1' },
{ handle: 'filesystem' },
{ dest: '/u3', src: '/user3' },
{ dest: '/n3', src: '/node3' },
{ dest: '/py3', src: '/python3' },
];
deepEqual(actual, expected);
});
test('mergeRoutes check true, continue true, handle filesystem top', () => {
const userRoutes = [{ handle: 'filesystem' }, { src: '/user1', dest: '/u1' }];
const builds = [
{
use: '@now/node',
entrypoint: 'api/home.js',
routes: [
{ handle: 'filesystem' },
{ src: '/node1', dest: '/n1' },
{ src: '/node2', dest: '/n2', continue: true },
{ src: '/node3', dest: '/n3', check: true },
],
},
{
use: '@now/python',
entrypoint: 'api/users.py',
routes: [
{ handle: 'filesystem' },
{ src: '/python1', dest: '/py1' },
{ src: '/python2', dest: '/py2', check: true },
{ src: '/python3', dest: '/py3', continue: true },
],
},
];
const actual = mergeRoutes({ userRoutes, builds });
const expected = [
{ handle: 'filesystem' },
{ continue: true, dest: '/n2', src: '/node2' },
{ continue: true, dest: '/py3', src: '/python3' },
{ dest: '/u1', src: '/user1' },
{ check: true, dest: '/n3', src: '/node3' },
{ check: true, dest: '/py2', src: '/python2' },
{ dest: '/n1', src: '/node1' },
{ dest: '/py1', src: '/python1' },
];
deepEqual(actual, expected);
});

View File

@@ -58,12 +58,12 @@ test('convertCleanUrls true', () => {
{
src: '^/(?:(.+)/)?index(?:\\.html)?/?$',
headers: { Location: '/$1' },
status: 301,
status: 308,
},
{
src: '^/(.*)\\.html/?$',
headers: { Location: '/$1' },
status: 301,
status: 308,
},
];
deepEqual(actual, expected);
@@ -94,12 +94,12 @@ test('convertCleanUrls true, trailingSlash true', () => {
{
src: '^/(?:(.+)/)?index(?:\\.html)?/?$',
headers: { Location: '/$1/' },
status: 301,
status: 308,
},
{
src: '^/(.*)\\.html/?$',
headers: { Location: '/$1/' },
status: 301,
status: 308,
},
];
deepEqual(actual, expected);
@@ -148,6 +148,7 @@ test('convertCleanUrls false', () => {
test('convertRedirects', () => {
const actual = convertRedirects([
{ source: '/some/old/path', destination: '/some/new/path' },
{ source: '/next(\\.js)?', destination: 'https://nextjs.org' },
{
source: '/firebase/(.*)',
destination: 'https://www.firebase.com',
@@ -164,22 +165,27 @@ test('convertRedirects', () => {
{
src: '^\\/some\\/old\\/path$',
headers: { Location: '/some/new/path' },
status: 307,
status: 308,
},
{
src: '^\\/firebase\\/(.*)$',
src: '^\\/next(\\.js)?$',
headers: { Location: 'https://nextjs.org' },
status: 308,
},
{
src: '^\\/firebase(?:\\/(.*))$',
headers: { Location: 'https://www.firebase.com' },
status: 302,
},
{
src: '^\\/projects\\/([^\\/]+?)\\/([^\\/]+?)$',
src: '^\\/projects(?:\\/([^\\/#\\?]+?))(?:\\/([^\\/#\\?]+?))$',
headers: { Location: '/projects.html?id=$1&action=$2' },
status: 307,
status: 308,
},
{
src: '^\\/old\\/([^\\/]+?)\\/path$',
src: '^\\/old(?:\\/([^\\/#\\?]+?))\\/path$',
headers: { Location: '/new/path/$1' },
status: 307,
status: 308,
},
];
@@ -187,6 +193,7 @@ test('convertRedirects', () => {
const mustMatch = [
['/some/old/path'],
['/next', '/next.js'],
['/firebase/one', '/firebase/2', '/firebase/-', '/firebase/dir/sub'],
['/projects/one/edit', '/projects/two/edit'],
['/old/one/path', '/old/two/path'],
@@ -194,6 +201,7 @@ test('convertRedirects', () => {
const mustNotMatch = [
['/nope'],
['/nextAjs', '/nextjs'],
['/fire', '/firebasejumper/two'],
['/projects/edit', '/projects/two/three/delete', '/projects'],
['/old/path', '/old/two/foo', '/old'],
@@ -210,16 +218,16 @@ test('convertRewrites', () => {
]);
const expected = [
{ src: '^\\/some\\/old\\/path$', dest: '/some/new/path', continue: true },
{ src: '^\\/some\\/old\\/path$', dest: '/some/new/path', check: true },
{
src: '^\\/firebase\\/(.*)$',
src: '^\\/firebase(?:\\/(.*))$',
dest: 'https://www.firebase.com',
continue: true,
check: true,
},
{
src: '^\\/projects\\/([^\\/]+?)\\/edit$',
src: '^\\/projects(?:\\/([^\\/#\\?]+?))\\/edit$',
dest: '/projects.html?id=$1',
continue: true,
check: true,
},
];
@@ -302,7 +310,7 @@ test('convertTrailingSlash enabled', () => {
{
src: '^/(.*[^\\/])$',
headers: { Location: '/$1/' },
status: 307,
status: 308,
},
];
deepEqual(actual, expected);
@@ -320,7 +328,7 @@ test('convertTrailingSlash disabled', () => {
{
src: '^/(.*)\\/$',
headers: { Location: '/$1' },
status: 307,
status: 308,
},
];
deepEqual(actual, expected);

View File

@@ -1 +1,5 @@
dist/
# bypass all ignored files for the cache fixtures
# because they contain node_modules and package-lock.json files
!test/cache-fixtures/**

View File

@@ -1,6 +1,6 @@
{
"name": "@now/static-build",
"version": "0.13.0",
"version": "0.13.1",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://zeit.co/docs/runtimes#official-runtimes/static-builds",
@@ -14,7 +14,8 @@
},
"scripts": {
"build": "./build.sh",
"test-integration-once": "jest --env node --verbose --runInBand",
"test-unit": "jest --env node --verbose --runInBand test/unit.test.js",
"test-integration-once": "jest --env node --verbose --runInBand test/integration.test.js",
"prepublishOnly": "./build.sh"
},
"devDependencies": {

View File

@@ -44,6 +44,7 @@ export const frameworks: Framework[] = [
return [];
}
},
cachePattern: '.cache/**',
},
{
name: 'Hexo',
@@ -312,4 +313,5 @@ export interface Framework {
getOutputDirName: (dirPrefix: string) => Promise<string>;
defaultRoutes?: Route[] | ((dirPrefix: string) => Promise<Route[]>);
minNodeRange?: string;
cachePattern?: string;
}

View File

@@ -18,6 +18,7 @@ import {
getNodeVersion,
getSpawnOptions,
Files,
FileFsRef,
Route,
BuildOptions,
Config,
@@ -149,6 +150,24 @@ async function getFrameworkRoutes(
return routes;
}
function getPkg(entrypoint: string, workPath: string) {
if (path.basename(entrypoint) !== 'package.json') {
return null;
}
const pkgPath = path.join(workPath, entrypoint);
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as PackageJson;
return pkg;
}
function getFramework(pkg: PackageJson) {
const dependencies = Object.assign({}, pkg.dependencies, pkg.devDependencies);
const framework = frameworks.find(
({ dependency }) => dependencies[dependency || '']
);
return framework;
}
export async function build({
files,
entrypoint,
@@ -168,11 +187,9 @@ export async function build({
(config && (config.distDir as string)) || 'dist'
);
const entrypointName = path.basename(entrypoint);
const pkg = getPkg(entrypoint, workPath);
if (entrypointName === 'package.json') {
const pkgPath = path.join(workPath, entrypoint);
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as PackageJson;
if (pkg) {
const gemfilePath = path.join(workPath, 'Gemfile');
const requirementsPath = path.join(workPath, 'requirements.txt');
@@ -230,15 +247,7 @@ export async function build({
// `public` is the default for zero config
distPath = path.join(workPath, path.dirname(entrypoint), 'public');
const dependencies = Object.assign(
{},
pkg.dependencies,
pkg.devDependencies
);
framework = frameworks.find(
({ dependency }) => dependencies[dependency || '']
);
framework = getFramework(pkg);
}
if (framework) {
@@ -377,7 +386,7 @@ export async function build({
return { routes, watch, output, distPath };
}
if (!config.zeroConfig && entrypointName.endsWith('.sh')) {
if (!config.zeroConfig && entrypoint.endsWith('.sh')) {
debug(`Running build script "${entrypoint}"`);
const nodeVersion = await getNodeVersion(entrypointDir, undefined, config);
const spawnOpts = getSpawnOptions(meta, nodeVersion);
@@ -403,10 +412,24 @@ export async function build({
throw new Error(message);
}
export async function prepareCache({ workPath }: PrepareCacheOptions) {
return {
...(await glob('node_modules/**', workPath)),
...(await glob('package-lock.json', workPath)),
...(await glob('yarn.lock', workPath)),
};
export async function prepareCache({
entrypoint,
workPath,
}: PrepareCacheOptions): Promise<Files> {
// default cache paths
const defaultCacheFiles = await glob('node_modules/**', workPath);
// framework specific cache paths
let frameworkCacheFiles: { [path: string]: FileFsRef } | null = null;
const pkg = getPkg(entrypoint, workPath);
if (pkg) {
const framework = getFramework(pkg);
if (framework && framework.cachePattern) {
frameworkCacheFiles = await glob(framework.cachePattern, workPath);
}
}
return { ...defaultCacheFiles, ...frameworkCacheFiles };
}

View File

@@ -0,0 +1 @@
file2

View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"gatsby": "^2.13.3"
}
}

View File

@@ -0,0 +1,25 @@
const { prepareCache } = require('../dist');
const path = require('path');
describe('prepareCache', () => {
test('should cache node_modules', async () => {
const files = await prepareCache({
workPath: path.resolve(__dirname, './cache-fixtures/default'),
entrypoint: 'index.js',
});
expect(files['node_modules/file']).toBeDefined();
expect(files['index.js']).toBeUndefined();
});
test('should cache node_modules and `.cache` folder for gatsby deployments', async () => {
const files = await prepareCache({
workPath: path.resolve(__dirname, './cache-fixtures/gatsby'),
entrypoint: 'package.json',
});
expect(files['node_modules/file2']).toBeDefined();
expect(files['.cache/file']).toBeDefined();
expect(files['package.json']).toBeUndefined();
});
});

View File

@@ -8610,10 +8610,10 @@ path-to-regexp@2.2.1:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.2.1.tgz#90b617025a16381a879bc82a38d4e8bdeb2bcf45"
integrity sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==
path-to-regexp@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.1.0.tgz#f45a9cc4dc6331ae8f131e0ce4fde8607f802367"
integrity sha512-PtHLisEvUOepjc+sStXxJ/pDV/s5UBTOKWJY2SOz3e6E/iN/jLknY9WL72kTwRrwXDUbZTEAtSnJbz2fF127DA==
path-to-regexp@6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.1.0.tgz#0b18f88b7a0ce0bfae6a25990c909ab86f512427"
integrity sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==
path-to-regexp@^1.0.0, path-to-regexp@^1.7.0:
version "1.7.0"