Compare commits

..

33 Commits

Author SHA1 Message Date
Steven
6e402bffc3 Publish Canary
- now@17.0.4-canary.1
 - @now/go@1.0.4-canary.2
 - @now/next@2.3.14-canary.0
2020-02-14 15:05:00 -05:00
Steven
e41d0f8e68 [now-go] Fix /api/go.sum detection (#3795)
Follow up to #3793 

The `go.mod` and `go.sum` file must both be moved.
2020-02-14 19:58:44 +00:00
Steven
1a046744f2 [now-go] Add support for root go.mod in api directory (#3793)
Some users wish to put all Go related code in the `/api` directory and the root is reserved for a frontend framework.

This PR adds support for `/api/go.mod` which will act the same as `/go.mod` by moving the file to the root.
2020-02-14 17:32:20 +00:00
Andy
a9bf011f2c [api] Add rewrite to frameworks API (#3794)
* [api] Add rewrite to frameworks API

* Adjust now.json

* Change output file
2020-02-14 00:30:58 +01:00
JJ Kasper
60c76b3290 [now-next] Add support for using the fallback file for dynamic SSG pages (#3752)
This adds support for using the fallback file for dynamic (lazy) SSG pages to allow showing a loading state while the cache is populated with data. We can add tests for this behavior once the below referenced PR is shipped on canary

x-ref: https://github.com/zeit/next.js/pull/10424

~Update: added tests now that fallback support has landed in Next.js although it appears `now-proxy` might need to be updated to revalidate the fallback correctly since the initial fallback is always being shown for lazy SSG pages with these changes~

Looks like this is working properly in `now-proxy` and we were overriding the prerender output with the static page output, thanks @juancampa for helping investigate! 🙏
2020-02-13 01:06:55 +00:00
Steven
c7ff1e7044 [now-cli] Update ZEIT Logo (#3790)
This PR updates the logo to match https://zeit.co/home
2020-02-12 10:17:54 -05:00
Andy Bitz
e2deaef54a Publish Canary
- @now/frameworks@0.0.10-canary.0
2020-02-12 14:46:11 +01:00
Andy
b2c7386c83 [frameworks] Adjust Next.js build command placeholder (#3788) 2020-02-12 14:34:09 +01:00
Steven
3dce84c3cf Publish Canary
- @now/go@1.0.4-canary.1
2020-02-11 16:46:21 -05:00
Steven
443b1ac158 [now-go] Fix path segments with square brackets (#3782)
Previously, `@now/go` would fail when using [Path Segments](https://zeit.co/docs/v2/serverless-functions/introduction#path-segments) since `go build` [doesn't support](https://stackoverflow.com/q/60140859/266535) files that begin with square bracket.

This PR changes the build logic so that the entrypoint file is renamed if if begins with a square bracket.

The output file is not renamed because v3 builders can't rename outputs which works great for this feature.
2020-02-11 21:41:57 +00:00
Andy Bitz
be3dca3d94 Publish Canary
- @now/build-utils@2.0.0-canary.1
 - now@17.0.4-canary.0
 - @now/go@1.0.4-canary.0
 - @now/python@1.1.4-canary.0
2020-02-11 16:50:13 +01:00
Andy Bitz
cb135e4329 [now-build-utils] Bump version 2020-02-11 16:50:01 +01:00
Andy
55c73b68bb [now-build-utils][now-cli] Refactor builds and routes detection (#3784)
* [now-build-utils] Refactor detect builds

* Refactor routes detection

* [now-cli] Adjust `now dev`

* Adjust tests

* Adjust integrations test

* Remove unused file

* Replace reducw with for-loop

* Add cache for absolute paths

* Filter while looping

* Add fallback for entrypoint with buildCommand
2020-02-11 16:47:39 +01:00
Steven
eb793ba139 [tests] Add windows to CI unit tests (#3727)
This adds initial support for Windows in GitHub Actions CI tests.

There is still work to be done to prevent certain tests from hanging in CI so those are skipped for now.
2020-02-11 01:16:14 +00:00
Steven
00908e8ebe [now-python] Fix path segments with square brackets (#3781)
Previously, python would fail when using [Path Segments](https://zeit.co/docs/v2/serverless-functions/introduction#path-segments) as seen in #3729.

This PR changes the import logic so that the entrypoint file is [imported as a relative file path](https://stackoverflow.com/a/67692/266535) instead of by module name.
2020-02-10 21:01:31 +00:00
Leo Lamprecht
5ce7ff86a4 Minor tweaks for CONTRIBUTING.md file (#3778)
* Minor tweaks for CONTRIBUTING.md file

* Update CONTRIBUTING.md

* Update CONTRIBUTING.md
2020-02-10 18:34:00 +01:00
Steven
4b34ee2993 [examples] Remove outdated jekyll build section from README (#3780)
We used to require a `package.json` but we have since introduced [Advanced Project Settings](https://zeit.co/blog/advanced-project-settings) which will ask the user for a build command during the first deploy.
2020-02-10 15:31:52 +00:00
Andi
646bd288ba [now-go] Update go to version 1.13.7 (#3750)
Updates go to the latest version 1.13.7 (https://golang.org/doc/devel/release.html#go1.13). This allows users to use the new error functionality that was introduced in go 1.13 (https://blog.golang.org/go1.13-errors) and also includes a couple of security fixes.

Looking at the last PR that updated the version (https://github.com/zeit/now-builders/pull/247/files) this should be as simple as updating the version number.

P.S. I tried to follow the contribution guidelines, but `yarn lint` fails with `exit code 1`/`295 problems (33 errors, 262 warnings)` and `yarn test` returns `error Command "test" not found` on master. But `yarn test-unit` shows no errors.
2020-02-08 01:47:47 +00:00
Steven
8aba6f1ff8 Publish Stable
- now@17.0.3
2020-02-07 20:01:17 -05:00
Steven
35e5e328aa Publish Canary
- now@17.0.3-canary.1
2020-02-07 19:18:47 -05:00
Andy
bdd4953d62 [now-cli] Consider root directory for now.json (#3764)
* [now-cli] Consider root directory for now.json

* Adjust message check in test

* Fallback if config does not exist as well

* Assign localConfig later and add debug

* Prefere now.json from root directory

* Comment

* Adjust test and add warning
2020-02-08 01:04:34 +01:00
Steven
e938b298e2 [now-cli] Fix port assignment for now dev (#3769)
This fixes a regression from #3749 where the PORT env var was removed. This is necessary so frameworks like create-react-app and gatsby can proxy to `now dev`.

Fixes #3761
2020-02-07 19:03:56 -05:00
Andy
b67f324e10 [api] Add hasDetectors property (#3770) 2020-02-08 00:29:11 +01:00
Andy Bitz
1dfa286af5 Publish Canary
- now@17.0.3-canary.0
2020-02-07 21:14:50 +01:00
Andy
d4391bd4cc [now-cli] Fix chalk template for legacy (#3768)
* [now-cli] Fix chalk template for legacy

* Update packages/now-cli/src/commands/deploy/legacy.ts

Co-Authored-By: Nathan Rajlich <n@n8.io>

Co-authored-by: Nathan Rajlich <n@n8.io>
2020-02-07 21:14:11 +01:00
luc
53eb71f26d Publish Stable
- now@17.0.2
 - @now/routing-utils@1.5.3
2020-02-06 19:18:35 +01:00
luc
92ffd654b5 Publish Canary
- now@17.0.2-canary.1
2020-02-06 18:53:36 +01:00
Luc
36b83f1606 [now-cli] Use npx or yarn to execute dev command (#3760)
1. Use bundled yarn to run `yarn bin`
2. Skip `yarn bin` if `npx` is used
2020-02-06 17:47:39 +00:00
Steven
7a79f620c0 [tests] Fix changelog script (#3751)
Previously, the changelog script was looking for the last "Publish Stable" commit, but it should really be looking for the last Stable release of Now CLI.

This PR updates the changelog script so that it fetches the latest GH Release (which should be Now CLI) and then compares that to the HEAD.
2020-02-06 14:35:15 +00:00
Steven
f3b286ecf3 Publish Canary
- now@17.0.2-canary.0
 - @now/routing-utils@1.5.3-canary.0
2020-02-06 09:12:19 -05:00
Steven
adf31c3fcc [now-routing-utils] Change behavior of trailingSlash: true redirects (#3745)
This PR changes the behavior of `trailingSlash: true` after we received feedback that files should not be redirected with a trailing slash. This matches the behavior of `serve` and `serve-handler`.

### Examples 
* `/index.html` => serve
* `/assets/style.css` => serve
* `/assets` => redirect to `/assets/`
* `/assets/style` => redirect to `/assets/style/` 

### Additional

In order to avoid duplicate content, this PR also adds redirects to files without a trailing slash.

* `/about.html/` => redirect to `/about.html`
* `/assets/style.css/` => redirect to `/assets/style.css`


Fixes #3731
2020-02-06 09:07:46 -05:00
luc
deacdfc47c Publish Stable
- now@17.0.1
2020-02-06 01:07:15 +01:00
Luc
ac9badbe9e [now-cli] Always output deployment url in stdout (#3753) 2020-02-06 00:58:24 +01:00
70 changed files with 1942 additions and 1411 deletions

View File

@@ -97,13 +97,9 @@ Sometimes you want to test changes to a Builder against an existing project, may
## Add a New Framework
You can add support for a new framework by creating a Pull Request for this repository by following the steps below.
You can add support for a new Framework by creating a Pull Request for this repository and following the steps below:
1. Add the framework to the `@now/frameworks` package.
The file is located in `packages/frameworks/frameworks.json`. You can copy the structure of an existing one and adjust the required fields. Note that the `settings` property either contains a `value` or a `placeholder`. The `value` property is used when something is not configurable, the `placeholder` is used when something is configurable and can be changed with configuration. An example would be the output directory for Hugo, it's `public` by default but can be changed through its config file, so we use `placeholder` with an explanation of what can be used.
2. Add an example to the `examples/` directory.
The name of the directory should equal the slug of the framework used in `@now/frameworks`.
The `.github/EXAMPLE_README_TEMPLATE.md` file can be used to create a README for the example.
3. Update the `@now/static-build` package
The files `packages/now-static-build/src/frameworks.ts` has to be extended. You can add default routes that will always be applied to projects that use this framework or specify some paths that will be cached to speed up the build process.
4. After your PR has been merged and released other users can use `now init` to get the example and deploy it to ZEIT Now.
1. Add the Framework to the `@now/frameworks` package: The file is located in `packages/frameworks/frameworks.json`. You can copy the structure of an existing one and adjust the required fields. Note that the `settings` property either contains a `value` or a `placeholder`. The `value` property is used when something is not configurable, the `placeholder` is used when something is configurable and can be changed with configuration. An example would be the Output Directory for Hugo, it's `public` by default but can be changed through its config file, so we use `placeholder` with an explanation of what can be used.
2. Add an example to the `examples/` directory: The name of the directory should equal the slug of the framework used in `@now/frameworks`. The `.github/EXAMPLE_README_TEMPLATE.md` file can be used to create a `README.md` file for the example.
3. Update the `@now/static-build` package: The file `packages/now-static-build/src/frameworks.ts` has to be extended. You can add default routes that will always be applied to projects that use this Framework or specify some paths that will be cached to speed up the build process.
4. After your Pull Request has been merged and released, other users can select the example on the ZEIT Now dashboard and deploy it.

View File

@@ -15,7 +15,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest, macos-latest, windows-latest]
node: [10, 12]
runs-on: ${{ matrix.os }}
steps:
@@ -23,7 +23,8 @@ jobs:
- run: git fetch origin master --depth=10
- run: git fetch origin ${{ github.ref }} --depth=10
- uses: actions/setup-node@v1
- run: yarn install && yarn run build
- run: yarn install
- run: yarn run build
- run: yarn run test-lint
- run: yarn run test-unit --clean false
- name: Upload Artifact
@@ -41,7 +42,8 @@ jobs:
- uses: actions/checkout@v2
- run: git fetch origin master --depth=10
- run: git fetch origin ${{ github.ref }} --depth=10
- run: yarn install && yarn run build
- run: yarn install
- run: yarn run build
- run: yarn test-integration-once --clean false
test-now-cli:
@@ -57,8 +59,11 @@ jobs:
- uses: actions/checkout@v2
- run: git fetch origin master --depth=10
- run: git fetch origin ${{ github.ref }} --depth=10
- run: curl -L -O https://github.com/gohugoio/hugo/releases/download/v0.56.0/hugo_0.56.0_macOS-64bit.tar.gz && tar -xzf hugo_0.56.0_macOS-64bit.tar.gz && mv ./hugo packages/now-cli/test/dev/fixtures/08-hugo/
- run: yarn install && yarn run build
- name: Install Hugo
if: matrix.os == 'macos-latest'
run: curl -L -O https://github.com/gohugoio/hugo/releases/download/v0.56.0/hugo_0.56.0_macOS-64bit.tar.gz && tar -xzf hugo_0.56.0_macOS-64bit.tar.gz && mv ./hugo packages/now-cli/test/dev/fixtures/08-hugo/
- run: yarn install
- run: yarn run build
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node }}
@@ -77,8 +82,11 @@ jobs:
- uses: actions/checkout@v2
- run: git fetch origin master --depth=10
- run: git fetch origin ${{ github.ref }} --depth=10
- run: curl -L -O https://github.com/gohugoio/hugo/releases/download/v0.56.0/hugo_0.56.0_macOS-64bit.tar.gz && tar -xzf hugo_0.56.0_macOS-64bit.tar.gz && mv ./hugo packages/now-cli/test/dev/fixtures/08-hugo/
- run: yarn install && yarn run build
- name: Install Hugo
if: matrix.os == 'macos-latest'
run: curl -L -O https://github.com/gohugoio/hugo/releases/download/v0.56.0/hugo_0.56.0_macOS-64bit.tar.gz && tar -xzf hugo_0.56.0_macOS-64bit.tar.gz && mv ./hugo packages/now-cli/test/dev/fixtures/08-hugo/
- run: yarn install
- run: yarn run build
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node }}

View File

@@ -1,4 +1,4 @@
![now](https://assets.zeit.co/image/upload/v1542240976/repositories/now-cli/now-cli-repo-banner-v3.png)
![now](https://assets.zeit.co/image/upload/v1581518533/repositories/now-cli/v4.png)
[![CI Status](https://badgen.net/github/checks/zeit/now?label=CI)](https://github.com/zeit/now/actions?workflow=CI)
[![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/zeit)

View File

@@ -2,17 +2,19 @@ import { NowRequest, NowResponse } from '@now/node';
import { withApiHandler } from './_lib/util/with-api-handler';
import frameworkList, { Framework } from '../packages/frameworks';
const frameworks: Framework[] = (frameworkList as Framework[]).map(
framework => {
delete framework.detectors;
const frameworks = (frameworkList as Framework[]).map(frameworkItem => {
const framework = {
...frameworkItem,
hasDetectors: Boolean(frameworkItem.detectors),
detectors: undefined,
};
if (framework.logo) {
framework.logo = `https://res.cloudinary.com/zeit-inc/image/fetch/${framework.logo}`;
}
return framework;
if (framework.logo) {
framework.logo = `https://res.cloudinary.com/zeit-inc/image/fetch/${framework.logo}`;
}
);
return framework;
});
export default withApiHandler(async function(
req: NowRequest,

View File

@@ -25,18 +25,3 @@ You can deploy your new Jekyll project with a single command from your terminal
```shell
$ now
```
### Build Command
The default build command is `jekyll build`.
If you wish to change the build command, add a `package.json` file with the following:
```json
{
"private": true,
"scripts": {
"build": "jekyll build"
}
}
```

View File

@@ -1,9 +1,8 @@
{
"redirects": [
"rewrites": [
{
"source": "/",
"destination": "https://zeit.co/new",
"statusCode": 307
"destination": "/api/frameworks"
}
],
"env": {

View File

@@ -17,7 +17,7 @@
},
"settings": {
"buildCommand": {
"placeholder": "`next build` or `build` from `package.json`"
"placeholder": "`build` from `package.json` or `next build`"
},
"devCommand": {
"value": "next dev --port $PORT"

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,432 +0,0 @@
import { parse as parsePath, extname } from 'path';
import { Route, Source } from '@now/routing-utils';
import { Builder } from './types';
import { getIgnoreApiFilter, sortFiles } from './detect-builders';
function escapeName(name: string) {
const special = '[]^$.|?*+()'.split('');
for (const char of special) {
name = name.replace(new RegExp(`\\${char}`, 'g'), `\\${char}`);
}
return name;
}
function joinPath(...segments: string[]) {
const joinedPath = segments.join('/');
return joinedPath.replace(/\/{2,}/g, '/');
}
function concatArrayOfText(texts: string[]): string {
if (texts.length <= 2) {
return texts.join(' and ');
}
const last = texts.pop();
return `${texts.join(', ')}, and ${last}`;
}
// Takes a filename or foldername, strips the extension
// gets the part between the "[]" brackets.
// It will return `null` if there are no brackets
// and therefore no segment.
function getSegmentName(segment: string): string | null {
const { name } = parsePath(segment);
if (name.startsWith('[') && name.endsWith(']')) {
return name.slice(1, -1);
}
return null;
}
function createRouteFromPath(
filePath: string,
featHandleMiss: boolean,
cleanUrls: boolean
): { route: Source; isDynamic: boolean } {
const parts = filePath.split('/');
let counter = 1;
const query: string[] = [];
let isDynamic = false;
const srcParts = parts.map((segment, i): string => {
const name = getSegmentName(segment);
const isLast = i === parts.length - 1;
if (name !== null) {
// We can't use `URLSearchParams` because `$` would get escaped
query.push(`${name}=$${counter++}`);
isDynamic = true;
return `([^/]+)`;
} else if (isLast) {
const { name: fileName, ext } = parsePath(segment);
const isIndex = fileName === 'index';
const prefix = isIndex ? '\\/' : '';
const names = [
isIndex ? prefix : `${fileName}\\/`,
prefix + escapeName(fileName),
featHandleMiss && cleanUrls
? ''
: prefix + escapeName(fileName) + escapeName(ext),
].filter(Boolean);
// Either filename with extension, filename without extension
// or nothing when the filename is `index`.
// When `cleanUrls: true` then do *not* add the filename with extension.
return `(${names.join('|')})${isIndex ? '?' : ''}`;
}
return segment;
});
const { name: fileName, ext } = parsePath(filePath);
const isIndex = fileName === 'index';
const queryString = `${query.length ? '?' : ''}${query.join('&')}`;
const src = isIndex
? `^/${srcParts.slice(0, -1).join('/')}${srcParts.slice(-1)[0]}$`
: `^/${srcParts.join('/')}$`;
let route: Source;
if (featHandleMiss) {
const extensionless = ext ? filePath.slice(0, -ext.length) : filePath;
route = {
src,
dest: `/${extensionless}${queryString}`,
check: true,
};
} else {
route = {
src,
dest: `/${filePath}${queryString}`,
};
}
return { route, isDynamic };
}
// Check if the path partially matches and has the same
// name for the path segment at the same position
function partiallyMatches(pathA: string, pathB: string): boolean {
const partsA = pathA.split('/');
const partsB = pathB.split('/');
const long = partsA.length > partsB.length ? partsA : partsB;
const short = long === partsA ? partsB : partsA;
let index = 0;
for (const segmentShort of short) {
const segmentLong = long[index];
const nameLong = getSegmentName(segmentLong);
const nameShort = getSegmentName(segmentShort);
// If there are no segments or the paths differ we
// return as they are not matching
if (segmentShort !== segmentLong && (!nameLong || !nameShort)) {
return false;
}
if (nameLong !== nameShort) {
return true;
}
index += 1;
}
return false;
}
// Counts how often a path occurs when all placeholders
// got resolved, so we can check if they have conflicts
function pathOccurrences(filePath: string, files: string[]): string[] {
const getAbsolutePath = (unresolvedPath: string): string => {
const { dir, name } = parsePath(unresolvedPath);
const parts = joinPath(dir, name).split('/');
return parts.map(part => part.replace(/\[.*\]/, '1')).join('/');
};
const currentAbsolutePath = getAbsolutePath(filePath);
return files.reduce((prev: string[], file: string): string[] => {
const absolutePath = getAbsolutePath(file);
if (absolutePath === currentAbsolutePath) {
prev.push(file);
} else if (partiallyMatches(filePath, file)) {
prev.push(file);
}
return prev;
}, []);
}
// Checks if a placeholder with the same name is used
// multiple times inside the same path
function getConflictingSegment(filePath: string): string | null {
const segments = new Set<string>();
for (const segment of filePath.split('/')) {
const name = getSegmentName(segment);
if (name !== null && segments.has(name)) {
return name;
}
if (name) {
segments.add(name);
}
}
return null;
}
function sortFilesBySegmentCount(fileA: string, fileB: string): number {
const lengthA = fileA.split('/').length;
const lengthB = fileB.split('/').length;
if (lengthA > lengthB) {
return -1;
}
if (lengthA < lengthB) {
return 1;
}
// Paths that have the same segment length but
// less placeholders are preferred
const countSegments = (prev: number, segment: string) =>
getSegmentName(segment) ? prev + 1 : 0;
const segmentLengthA = fileA.split('/').reduce(countSegments, 0);
const segmentLengthB = fileB.split('/').reduce(countSegments, 0);
if (segmentLengthA > segmentLengthB) {
return 1;
}
if (segmentLengthA < segmentLengthB) {
return -1;
}
return 0;
}
interface ApiRoutesResult {
defaultRoutes: Source[] | null;
dynamicRoutes: Source[] | null;
error: { [key: string]: string } | null;
}
interface RoutesResult {
defaultRoutes: Route[] | null;
redirectRoutes: Route[] | null;
error: { [key: string]: string } | null;
}
async function detectApiRoutes(
files: string[],
builders: Builder[],
featHandleMiss: boolean,
cleanUrls: boolean
): Promise<ApiRoutesResult> {
if (!files || files.length === 0) {
return {
defaultRoutes: null,
dynamicRoutes: null,
error: null,
};
}
// The deepest routes need to be
// the first ones to get handled
const sortedFiles = files
.filter(getIgnoreApiFilter(builders))
.sort(sortFiles)
.sort(sortFilesBySegmentCount);
const defaultRoutes: Source[] = [];
const dynamicRoutes: Source[] = [];
for (const file of sortedFiles) {
// We only consider every file in the api directory
// as we will strip extensions as well as resolving "[segments]"
if (
!file.startsWith('api/') &&
!builders.some(b => b.src === file && b.config && b.config.functions)
) {
continue;
}
const conflictingSegment = getConflictingSegment(file);
if (conflictingSegment) {
return {
defaultRoutes: null,
dynamicRoutes: null,
error: {
code: 'conflicting_path_segment',
message:
`The segment "${conflictingSegment}" occurs more than ` +
`one time in your path "${file}". Please make sure that ` +
`every segment in a path is unique`,
},
};
}
const occurrences = pathOccurrences(file, sortedFiles).filter(
name => name !== file
);
if (occurrences.length > 0) {
const messagePaths = concatArrayOfText(
occurrences.map(name => `"${name}"`)
);
return {
defaultRoutes: null,
dynamicRoutes: null,
error: {
code: 'conflicting_file_path',
message:
`Two or more files have conflicting paths or names. ` +
`Please make sure path segments and filenames, without their extension, are unique. ` +
`The path "${file}" has conflicts with ${messagePaths}`,
},
};
}
const out = createRouteFromPath(file, featHandleMiss, cleanUrls);
if (out.isDynamic) {
dynamicRoutes.push(out.route);
}
defaultRoutes.push(out.route);
}
return { defaultRoutes, dynamicRoutes, error: null };
}
function getPublicBuilder(builders: Builder[]): Builder | null {
const builder = builders.find(
builder =>
builder.use === '@now/static' &&
/^.*\/\*\*\/\*$/.test(builder.src) &&
builder.config &&
builder.config.zeroConfig === true
);
return builder || null;
}
export function detectOutputDirectory(builders: Builder[]): string | null {
// TODO: We eventually want to save the output directory to
// builder.config.outputDirectory so it is only detected once
const publicBuilder = getPublicBuilder(builders);
return publicBuilder ? publicBuilder.src.replace('/**/*', '') : null;
}
export function detectApiDirectory(builders: Builder[]): string | null {
// TODO: We eventually want to save the api directory to
// builder.config.apiDirectory so it is only detected once
const found = builders.some(
b => b.config && b.config.zeroConfig && b.src.startsWith('api/')
);
return found ? 'api' : null;
}
export function detectApiExtensions(builders: Builder[]): Set<string> {
return new Set<string>(
builders
.filter(
b =>
b.config && b.config.zeroConfig && b.src && b.src.startsWith('api/')
)
.map(b => extname(b.src))
.filter(Boolean)
);
}
export async function detectRoutes(
files: string[],
builders: Builder[],
featHandleMiss = false,
cleanUrls = false,
trailingSlash?: boolean
): Promise<RoutesResult> {
const result = await detectApiRoutes(
files,
builders,
featHandleMiss,
cleanUrls
);
const { dynamicRoutes, defaultRoutes: allRoutes, error } = result;
if (error) {
return { defaultRoutes: null, redirectRoutes: null, error };
}
const directory = detectOutputDirectory(builders);
const defaultRoutes: Route[] = [];
const redirectRoutes: Route[] = [];
if (allRoutes && allRoutes.length > 0) {
const hasApiRoutes = allRoutes.some(
r => r.dest && r.dest.startsWith('/api/')
);
if (featHandleMiss) {
defaultRoutes.push({ handle: 'miss' });
const extSet = detectApiExtensions(builders);
if (extSet.size > 0) {
const exts = Array.from(extSet)
.map(ext => ext.slice(1))
.join('|');
const extGroup = `(?:\\.(?:${exts}))`;
if (cleanUrls) {
redirectRoutes.push({
src: `^/(api(?:.+)?)/index${extGroup}?/?$`,
headers: { Location: trailingSlash ? '/$1/' : '/$1' },
status: 308,
});
redirectRoutes.push({
src: `^/api/(.+)${extGroup}/?$`,
headers: { Location: trailingSlash ? '/api/$1/' : '/api/$1' },
status: 308,
});
} else {
defaultRoutes.push({
src: `^/api/(.+)${extGroup}$`,
dest: '/api/$1',
check: true,
});
}
}
if (dynamicRoutes) {
defaultRoutes.push(...dynamicRoutes);
}
if (hasApiRoutes) {
defaultRoutes.push({
src: '^/api(/.*)?$',
status: 404,
continue: true,
});
}
} else {
defaultRoutes.push(...allRoutes);
if (hasApiRoutes) {
defaultRoutes.push({
status: 404,
src: '^/api(/.*)?$',
});
}
}
}
if (!featHandleMiss && directory) {
defaultRoutes.push({
src: '/(.*)',
dest: `/${directory}/$1`,
});
}
return { defaultRoutes, redirectRoutes, error };
}

View File

@@ -64,12 +64,11 @@ export {
};
export {
detectRoutes,
detectBuilders,
detectOutputDirectory,
detectApiDirectory,
detectApiExtensions,
} from './detect-routes';
export { detectBuilders } from './detect-builders';
} from './detect-builders';
export { detectFramework } from './detect-framework';
export { DetectorFilesystem } from './detectors/filesystem';
export { readConfigFile } from './fs/read-config-file';

View File

@@ -4,7 +4,7 @@ const {
packAndDeploy,
testDeployment,
} = require('../../../test/lib/deployment/test-deployment');
const { glob, detectBuilders, detectRoutes } = require('../');
const { glob, detectBuilders } = require('../');
jest.setTimeout(4 * 60 * 1000);
@@ -110,8 +110,7 @@ it('Test `detectBuilders` and `detectRoutes`', async () => {
},
];
const { builders } = await detectBuilders(files, pkg);
const { defaultRoutes } = await detectRoutes(files, builders);
const { builders, defaultRoutes } = await detectBuilders(files, pkg);
const nowConfig = { builds: builders, routes: defaultRoutes, probes };
await fs.writeFile(
@@ -126,7 +125,7 @@ it('Test `detectBuilders` and `detectRoutes`', async () => {
expect(deployment).toBeDefined();
});
it('Test `detectBuilders` and `detectRoutes` with `index` files', async () => {
it('Test `detectBuilders` with `index` files', async () => {
const fixture = path.join(__dirname, 'fixtures', '02-zero-config-api');
const pkg = await fs.readJSON(path.join(fixture, 'package.json'));
const fileList = await glob('**', fixture);
@@ -192,8 +191,7 @@ it('Test `detectBuilders` and `detectRoutes` with `index` files', async () => {
},
];
const { builders } = await detectBuilders(files, pkg);
const { defaultRoutes } = await detectRoutes(files, builders);
const { builders, defaultRoutes } = await detectBuilders(files, pkg);
const nowConfig = { builds: builders, routes: defaultRoutes, probes };
await fs.writeFile(

View File

@@ -1,5 +1,5 @@
import { Source, Route } from '@now/routing-utils';
import { detectBuilders, detectRoutes } from '../src';
import { detectBuilders } from '../src';
import {
detectOutputDirectory,
detectApiDirectory,
@@ -499,7 +499,7 @@ describe('Test `detectBuilders`', () => {
});
expect(errors!.length).toBe(1);
expect(errors![0].code).toBe('invalid_function_source');
expect(errors![0].code).toBe('unused_function');
});
it('do not allow empty functions', async () => {
@@ -648,14 +648,14 @@ describe('Test `detectBuilders`', () => {
const files = ['dist/index.html', 'dist/style.css'];
const { builders } = await detectBuilders(files, null, { projectSettings });
const { builders, defaultRoutes } = await detectBuilders(files, null, {
projectSettings,
});
expect(builders!.length).toBe(1);
expect(builders![0].src).toBe('dist/**/*');
expect(builders![0].use).toBe('@now/static');
const { defaultRoutes } = await detectRoutes(files, builders!);
expect(defaultRoutes!.length).toBe(1);
expect((defaultRoutes![0] as any).src).toBe('/(.*)');
expect((defaultRoutes![0] as any).dest).toBe('/dist/$1');
@@ -668,14 +668,14 @@ describe('Test `detectBuilders`', () => {
const files = ['api/user.ts', 'output/index.html', 'output/style.css'];
const { builders } = await detectBuilders(files, null, { projectSettings });
const { builders, defaultRoutes } = await detectBuilders(files, null, {
projectSettings,
});
expect(builders!.length).toBe(2);
expect(builders![1].src).toBe('output/**/*');
expect(builders![1].use).toBe('@now/static');
const { defaultRoutes } = await detectRoutes(files, builders!);
expect(defaultRoutes!.length).toBe(3);
expect((defaultRoutes![1] as any).status).toBe(404);
expect((defaultRoutes![2] as any).src).toBe('/(.*)');
@@ -819,8 +819,7 @@ it('Test `detectRoutes`', async () => {
{
const files = ['api/user.go', 'api/team.js', 'api/package.json'];
const { builders } = await detectBuilders(files);
const { defaultRoutes } = await detectRoutes(files, builders!);
const { defaultRoutes } = await detectBuilders(files);
expect(defaultRoutes!.length).toBe(3);
expect((defaultRoutes![0] as any).dest).toBe('/api/team.js');
expect((defaultRoutes![1] as any).dest).toBe('/api/user.go');
@@ -831,41 +830,36 @@ it('Test `detectRoutes`', async () => {
{
const files = ['api/user.go', 'api/user.js'];
const { builders } = await detectBuilders(files);
const { error } = await detectRoutes(files, builders!);
expect(error!.code).toBe('conflicting_file_path');
const { errors } = await detectBuilders(files);
expect(errors![0]!.code).toBe('conflicting_file_path');
}
{
const files = ['api/[user].go', 'api/[team]/[id].js'];
const { builders } = await detectBuilders(files);
const { error } = await detectRoutes(files, builders!);
expect(error!.code).toBe('conflicting_file_path');
const { errors } = await detectBuilders(files);
expect(errors![0]!.code).toBe('conflicting_file_path');
}
{
const files = ['api/[team]/[team].js'];
const { builders } = await detectBuilders(files);
const { error } = await detectRoutes(files, builders!);
expect(error!.code).toBe('conflicting_path_segment');
const { errors } = await detectBuilders(files);
expect(errors![0]!.code).toBe('conflicting_path_segment');
}
{
const files = ['api/date/index.js', 'api/date/index.go'];
const { builders } = await detectBuilders(files);
const { defaultRoutes, error } = await detectRoutes(files, builders!);
const { defaultRoutes, errors } = await detectBuilders(files);
expect(defaultRoutes).toBe(null);
expect(error!.code).toBe('conflicting_file_path');
expect(errors![0]!.code).toBe('conflicting_file_path');
}
{
const files = ['api/[endpoint].js', 'api/[endpoint]/[id].js'];
const { builders } = await detectBuilders(files);
const { defaultRoutes } = await detectRoutes(files, builders!);
const { defaultRoutes } = await detectBuilders(files);
expect(defaultRoutes!.length).toBe(3);
}
@@ -876,8 +870,7 @@ it('Test `detectRoutes`', async () => {
'api/[endpoint]/[id].js',
];
const { builders } = await detectBuilders(files);
const { defaultRoutes } = await detectRoutes(files, builders!);
const { defaultRoutes } = await detectBuilders(files);
expect((defaultRoutes![2] as any).status).toBe(404);
expect((defaultRoutes![2] as any).src).toBe('^/api(/.*)?$');
expect((defaultRoutes![3] as any).src).toBe('/(.*)');
@@ -892,8 +885,7 @@ it('Test `detectRoutes`', async () => {
};
const files = ['public/index.html', 'api/[endpoint].js'];
const { builders } = await detectBuilders(files, pkg);
const { defaultRoutes } = await detectRoutes(files, builders!);
const { defaultRoutes } = await detectBuilders(files, pkg);
expect((defaultRoutes![1] as any).status).toBe(404);
expect((defaultRoutes![1] as any).src).toBe('^/api(/.*)?$');
expect(defaultRoutes!.length).toBe(2);
@@ -902,8 +894,7 @@ it('Test `detectRoutes`', async () => {
{
const files = ['public/index.html'];
const { builders } = await detectBuilders(files);
const { defaultRoutes } = await detectRoutes(files, builders!);
const { defaultRoutes } = await detectBuilders(files);
expect(defaultRoutes!.length).toBe(1);
}
@@ -911,8 +902,7 @@ it('Test `detectRoutes`', async () => {
{
const files = ['api/date/index.js', 'api/date.js'];
const { builders } = await detectBuilders(files);
const { defaultRoutes } = await detectRoutes(files, builders!);
const { defaultRoutes } = await detectBuilders(files);
expect(defaultRoutes!.length).toBe(3);
expect((defaultRoutes![0] as any).src).toBe(
@@ -928,8 +918,7 @@ it('Test `detectRoutes`', async () => {
{
const files = ['api/date.js', 'api/[date]/index.js'];
const { builders } = await detectBuilders(files);
const { defaultRoutes } = await detectRoutes(files, builders!);
const { defaultRoutes } = await detectBuilders(files);
expect(defaultRoutes!.length).toBe(3);
expect((defaultRoutes![0] as any).src).toBe(
@@ -953,8 +942,7 @@ it('Test `detectRoutes`', async () => {
'api/food.ts',
'api/ts/gold.ts',
];
const { builders } = await detectBuilders(files);
const { defaultRoutes } = await detectRoutes(files, builders!);
const { builders, defaultRoutes } = await detectBuilders(files);
expect(builders!.length).toBe(4);
expect(builders![0].use).toBe('@now/node');
@@ -969,8 +957,7 @@ it('Test `detectRoutes`', async () => {
const functions = { 'api/user.php': { runtime: 'now-php@0.0.8' } };
const files = ['api/user.php'];
const { builders } = await detectBuilders(files, null, { functions });
const { defaultRoutes } = await detectRoutes(files, builders!);
const { defaultRoutes } = await detectBuilders(files, null, { functions });
expect(defaultRoutes!.length).toBe(2);
expect((defaultRoutes![0] as any).dest).toBe('/api/user.php');
@@ -983,12 +970,9 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
{
const files = ['api/user.go', 'api/team.js', 'api/package.json'];
const { builders } = await detectBuilders(files);
const { defaultRoutes } = await detectRoutes(
files,
builders!,
featHandleMiss
);
const { defaultRoutes } = await detectBuilders(files, null, {
featHandleMiss,
});
expect(defaultRoutes).toStrictEqual([
{ handle: 'miss' },
{
@@ -1007,49 +991,40 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
{
const files = ['api/user.go', 'api/user.js'];
const { builders } = await detectBuilders(files);
const { error } = await detectRoutes(files, builders!, featHandleMiss);
expect(error!.code).toBe('conflicting_file_path');
const { errors } = await detectBuilders(files, null, { featHandleMiss });
expect(errors![0]!.code).toBe('conflicting_file_path');
}
{
const files = ['api/[user].go', 'api/[team]/[id].js'];
const { builders } = await detectBuilders(files);
const { error } = await detectRoutes(files, builders!, featHandleMiss);
expect(error!.code).toBe('conflicting_file_path');
const { errors } = await detectBuilders(files, null, { featHandleMiss });
expect(errors![0]!.code).toBe('conflicting_file_path');
}
{
const files = ['api/[team]/[team].js'];
const { builders } = await detectBuilders(files);
const { error } = await detectRoutes(files, builders!, featHandleMiss);
expect(error!.code).toBe('conflicting_path_segment');
const { errors } = await detectBuilders(files, null, { featHandleMiss });
expect(errors![0]!.code).toBe('conflicting_path_segment');
}
{
const files = ['api/date/index.js', 'api/date/index.go'];
const { builders } = await detectBuilders(files);
const { defaultRoutes, error } = await detectRoutes(
files,
builders!,
featHandleMiss
);
const { defaultRoutes, errors } = await detectBuilders(files, null, {
featHandleMiss,
});
expect(defaultRoutes).toBe(null);
expect(error!.code).toBe('conflicting_file_path');
expect(errors![0]!.code).toBe('conflicting_file_path');
}
{
const files = ['api/[endpoint].js', 'api/[endpoint]/[id].js'];
const { builders } = await detectBuilders(files);
const { defaultRoutes } = await detectRoutes(
files,
builders!,
featHandleMiss
);
const { defaultRoutes } = await detectBuilders(files, null, {
featHandleMiss,
});
expect(defaultRoutes).toStrictEqual([
{ handle: 'miss' },
{
@@ -1082,12 +1057,9 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
'api/[endpoint]/[id].js',
];
const { builders } = await detectBuilders(files);
const { defaultRoutes } = await detectRoutes(
files,
builders!,
featHandleMiss
);
const { defaultRoutes } = await detectBuilders(files, null, {
featHandleMiss,
});
expect(defaultRoutes).toStrictEqual([
{ handle: 'miss' },
{
@@ -1126,12 +1098,9 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
const files = ['public/index.html', 'api/[endpoint].js'];
const { builders } = await detectBuilders(files, pkg);
const { defaultRoutes } = await detectRoutes(
files,
builders!,
featHandleMiss
);
const { defaultRoutes } = await detectBuilders(files, pkg, {
featHandleMiss,
});
expect(defaultRoutes).toStrictEqual([
{ handle: 'miss' },
{
@@ -1155,24 +1124,18 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
{
const files = ['public/index.html'];
const { builders } = await detectBuilders(files);
const { defaultRoutes } = await detectRoutes(
files,
builders!,
featHandleMiss
);
const { defaultRoutes } = await detectBuilders(files, null, {
featHandleMiss,
});
expect(defaultRoutes).toStrictEqual([]);
}
{
const files = ['api/date/index.js', 'api/date.js'];
const { builders } = await detectBuilders(files);
const { defaultRoutes } = await detectRoutes(
files,
builders!,
featHandleMiss
);
const { defaultRoutes } = await detectBuilders(files, null, {
featHandleMiss,
});
expect(defaultRoutes).toStrictEqual([
{ handle: 'miss' },
{
@@ -1191,12 +1154,9 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
{
const files = ['api/date.js', 'api/[date]/index.js'];
const { builders } = await detectBuilders(files);
const { defaultRoutes } = await detectRoutes(
files,
builders!,
featHandleMiss
);
const { defaultRoutes } = await detectBuilders(files, null, {
featHandleMiss,
});
expect(defaultRoutes).toStrictEqual([
{ handle: 'miss' },
{
@@ -1226,12 +1186,9 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
'api/food.ts',
'api/ts/gold.ts',
];
const { builders } = await detectBuilders(files);
const { defaultRoutes } = await detectRoutes(
files,
builders!,
featHandleMiss
);
const { defaultRoutes } = await detectBuilders(files, null, {
featHandleMiss,
});
expect(defaultRoutes).toStrictEqual([
{ handle: 'miss' },
@@ -1249,12 +1206,10 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
const functions = { 'api/user.php': { runtime: 'now-php@0.0.8' } };
const files = ['api/user.php'];
const { builders } = await detectBuilders(files, null, { functions });
const { defaultRoutes } = await detectRoutes(
files,
builders!,
featHandleMiss
);
const { defaultRoutes } = await detectBuilders(files, null, {
functions,
featHandleMiss,
});
expect(defaultRoutes).toStrictEqual([
{ handle: 'miss' },
{
@@ -1268,8 +1223,11 @@ it('Test `detectRoutes` with `featHandleMiss=true`', async () => {
});
it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async () => {
const featHandleMiss = true;
const cleanUrls = true;
const options = {
featHandleMiss: true,
cleanUrls: true,
};
const testHeaders = (redirectRoutes: Route[] | null) => {
if (!redirectRoutes || redirectRoutes.length === 0) {
throw new Error('Expected one redirect but found none');
@@ -1281,12 +1239,10 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
{
const files = ['api/user.go', 'api/team.js', 'api/package.json'];
const { builders } = await detectBuilders(files);
const { defaultRoutes, redirectRoutes } = await detectRoutes(
const { defaultRoutes, redirectRoutes } = await detectBuilders(
files,
builders!,
featHandleMiss,
cleanUrls
null,
options
);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([
@@ -1328,65 +1284,43 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
{
const files = ['api/user.go', 'api/user.js'];
const { builders } = await detectBuilders(files);
const { error } = await detectRoutes(
files,
builders!,
featHandleMiss,
cleanUrls
);
expect(error!.code).toBe('conflicting_file_path');
const { errors } = await detectBuilders(files, null, options);
expect(errors![0]!.code).toBe('conflicting_file_path');
}
{
const files = ['api/[user].go', 'api/[team]/[id].js'];
const { builders } = await detectBuilders(files);
const { error } = await detectRoutes(
files,
builders!,
featHandleMiss,
cleanUrls
);
expect(error!.code).toBe('conflicting_file_path');
const { errors } = await detectBuilders(files, null, options);
expect(errors![0]!.code).toBe('conflicting_file_path');
}
{
const files = ['api/[team]/[team].js'];
const { builders } = await detectBuilders(files);
const { error } = await detectRoutes(
files,
builders!,
featHandleMiss,
cleanUrls
);
expect(error!.code).toBe('conflicting_path_segment');
const { errors } = await detectBuilders(files, null, options);
expect(errors![0]!.code).toBe('conflicting_path_segment');
}
{
const files = ['api/date/index.js', 'api/date/index.go'];
const { builders } = await detectBuilders(files);
const { defaultRoutes, error } = await detectRoutes(
const { defaultRoutes, errors } = await detectBuilders(
files,
builders!,
featHandleMiss,
cleanUrls
null,
options
);
expect(defaultRoutes).toBe(null);
expect(error!.code).toBe('conflicting_file_path');
expect(errors![0]!.code).toBe('conflicting_file_path');
}
{
const files = ['api/[endpoint].js', 'api/[endpoint]/[id].js'];
const { builders } = await detectBuilders(files);
const { defaultRoutes, redirectRoutes } = await detectRoutes(
const { defaultRoutes, redirectRoutes } = await detectBuilders(
files,
builders!,
featHandleMiss,
cleanUrls
null,
options
);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([
@@ -1416,12 +1350,10 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
'api/[endpoint]/[id].js',
];
const { builders } = await detectBuilders(files);
const { defaultRoutes, redirectRoutes } = await detectRoutes(
const { defaultRoutes, redirectRoutes } = await detectBuilders(
files,
builders!,
featHandleMiss,
cleanUrls
null,
options
);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([
@@ -1457,12 +1389,10 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
const files = ['public/index.html', 'api/[endpoint].js'];
const { builders } = await detectBuilders(files, pkg);
const { defaultRoutes, redirectRoutes } = await detectRoutes(
const { defaultRoutes, redirectRoutes } = await detectBuilders(
files,
builders!,
featHandleMiss,
cleanUrls
pkg,
options
);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([
@@ -1483,25 +1413,17 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
{
const files = ['public/index.html'];
const { builders } = await detectBuilders(files);
const { defaultRoutes } = await detectRoutes(
files,
builders!,
featHandleMiss,
cleanUrls
);
const { defaultRoutes } = await detectBuilders(files, null, options);
expect(defaultRoutes).toStrictEqual([]);
}
{
const files = ['api/date/index.js', 'api/date.js'];
const { builders } = await detectBuilders(files);
const { defaultRoutes, redirectRoutes } = await detectRoutes(
const { defaultRoutes, redirectRoutes } = await detectBuilders(
files,
builders!,
featHandleMiss,
cleanUrls
null,
options
);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([
@@ -1517,12 +1439,10 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
{
const files = ['api/date.js', 'api/[date]/index.js'];
const { builders } = await detectBuilders(files);
const { defaultRoutes, redirectRoutes } = await detectRoutes(
const { defaultRoutes, redirectRoutes } = await detectBuilders(
files,
builders!,
featHandleMiss,
cleanUrls
null,
options
);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([
@@ -1549,12 +1469,10 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
'api/food.ts',
'api/ts/gold.ts',
];
const { builders } = await detectBuilders(files);
const { defaultRoutes, redirectRoutes } = await detectRoutes(
const { defaultRoutes, redirectRoutes } = await detectBuilders(
files,
builders!,
featHandleMiss,
cleanUrls
null,
options
);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([
@@ -1568,12 +1486,10 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
const functions = { 'api/user.php': { runtime: 'now-php@0.0.8' } };
const files = ['api/user.php'];
const { builders } = await detectBuilders(files, null, { functions });
const { defaultRoutes, redirectRoutes } = await detectRoutes(
const { defaultRoutes, redirectRoutes } = await detectBuilders(
files,
builders!,
featHandleMiss,
cleanUrls
null,
{ functions, ...options }
);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([
@@ -1584,9 +1500,12 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`', async ()
});
it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingSlash=true`', async () => {
const featHandleMiss = true;
const cleanUrls = true;
const trailingSlash = true;
const options = {
featHandleMiss: true,
cleanUrls: true,
trailingSlash: true,
};
const testHeaders = (redirectRoutes: Route[] | null) => {
if (!redirectRoutes || redirectRoutes.length === 0) {
throw new Error('Expected one redirect but found none');
@@ -1598,13 +1517,10 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
{
const files = ['api/user.go', 'api/team.js', 'api/package.json'];
const { builders } = await detectBuilders(files);
const { defaultRoutes, redirectRoutes } = await detectRoutes(
const { defaultRoutes, redirectRoutes } = await detectBuilders(
files,
builders!,
featHandleMiss,
cleanUrls,
trailingSlash
null,
options
);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([
@@ -1649,13 +1565,10 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
{
const files = ['api/[endpoint].js', 'api/[endpoint]/[id].js'];
const { builders } = await detectBuilders(files);
const { defaultRoutes, redirectRoutes } = await detectRoutes(
const { defaultRoutes, redirectRoutes } = await detectBuilders(
files,
builders!,
featHandleMiss,
cleanUrls,
trailingSlash
null,
options
);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([
@@ -1685,13 +1598,10 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
'api/[endpoint]/[id].js',
];
const { builders } = await detectBuilders(files);
const { defaultRoutes, redirectRoutes } = await detectRoutes(
const { defaultRoutes, redirectRoutes } = await detectBuilders(
files,
builders!,
featHandleMiss,
cleanUrls,
trailingSlash
null,
options
);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([
@@ -1727,13 +1637,10 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
const files = ['public/index.html', 'api/[endpoint].js'];
const { builders } = await detectBuilders(files, pkg);
const { defaultRoutes, redirectRoutes } = await detectRoutes(
const { defaultRoutes, redirectRoutes } = await detectBuilders(
files,
builders!,
featHandleMiss,
cleanUrls,
trailingSlash
pkg,
options
);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([
@@ -1754,13 +1661,10 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
{
const files = ['api/date/index.js', 'api/date.js'];
const { builders } = await detectBuilders(files);
const { defaultRoutes, redirectRoutes } = await detectRoutes(
const { defaultRoutes, redirectRoutes } = await detectBuilders(
files,
builders!,
featHandleMiss,
cleanUrls,
trailingSlash
null,
options
);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([
@@ -1776,13 +1680,10 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
{
const files = ['api/date.js', 'api/[date]/index.js'];
const { builders } = await detectBuilders(files);
const { defaultRoutes, redirectRoutes } = await detectRoutes(
const { defaultRoutes, redirectRoutes } = await detectBuilders(
files,
builders!,
featHandleMiss,
cleanUrls,
trailingSlash
null,
options
);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([
@@ -1809,13 +1710,10 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
'api/food.ts',
'api/ts/gold.ts',
];
const { builders } = await detectBuilders(files);
const { defaultRoutes, redirectRoutes } = await detectRoutes(
const { defaultRoutes, redirectRoutes } = await detectBuilders(
files,
builders!,
featHandleMiss,
cleanUrls,
trailingSlash
null,
options
);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([
@@ -1829,13 +1727,10 @@ it('Test `detectRoutes` with `featHandleMiss=true`, `cleanUrls=true`, `trailingS
const functions = { 'api/user.php': { runtime: 'now-php@0.0.8' } };
const files = ['api/user.php'];
const { builders } = await detectBuilders(files, null, { functions });
const { defaultRoutes, redirectRoutes } = await detectRoutes(
const { defaultRoutes, redirectRoutes } = await detectBuilders(
files,
builders!,
featHandleMiss,
cleanUrls,
trailingSlash
null,
{ functions, ...options }
);
testHeaders(redirectRoutes);
expect(defaultRoutes).toStrictEqual([

View File

@@ -11,6 +11,10 @@ const {
} = require('../dist');
it('should re-create symlinks properly', async () => {
if (process.platform === 'win32') {
console.log('Skipping test on windows');
return;
}
const files = await glob('**', path.join(__dirname, 'symlinks'));
assert.equal(Object.keys(files).length, 2);
@@ -29,6 +33,10 @@ it('should re-create symlinks properly', async () => {
});
it('should create zip files with symlinks properly', async () => {
if (process.platform === 'win32') {
console.log('Skipping test on windows');
return;
}
const files = await glob('**', path.join(__dirname, 'symlinks'));
assert.equal(Object.keys(files).length, 2);

View File

@@ -1,4 +1,4 @@
![now](https://assets.zeit.co/image/upload/v1542240976/repositories/now-cli/now-cli-repo-banner-v3.png)
![now](https://assets.zeit.co/image/upload/v1581518533/repositories/now-cli/v4.png)
[![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/zeit)

View File

@@ -1,6 +1,6 @@
{
"name": "now",
"version": "17.0.0",
"version": "17.0.4-canary.1",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Now",
@@ -12,7 +12,7 @@
},
"scripts": {
"preinstall": "node ./scripts/preinstall.js",
"test-unit": "nyc ava test/*unit.js --serial --fail-fast --verbose",
"test-unit": "nyc ava test/unit.js test/dev-builder.unit.js test/dev-router.unit.js test/dev-server.unit.js --serial --fail-fast --verbose",
"test-integration": "ava test/integration.js --serial --fail-fast",
"test-integration-v1": "ava test/integration-v1.js --serial --fail-fast",
"test-integration-now-dev": "ava test/dev/integration.js --serial --fail-fast --verbose",

View File

@@ -186,7 +186,7 @@ export default async ctx => {
contextName,
output,
stats,
localConfig || {},
localConfig,
parts.latestArgs
);
}

View File

@@ -53,6 +53,7 @@ import { inputRootDirectory } from '../../util/input/input-root-directory';
import validatePaths, {
validateRootDirectory,
} from '../../util/validate-paths';
import { readLocalConfig } from '../../util/config/files';
const addProcessEnv = async (log, env) => {
let val;
@@ -94,7 +95,6 @@ const printDeploymentStatus = async (
},
deployStamp,
isClipboardEnabled,
quiet,
isFile
) => {
const isProdDeployment = target === 'production';
@@ -144,11 +144,6 @@ const printDeploymentStatus = async (
.catch(error => output.debug(`Error copying to clipboard: ${error}`));
}
// write to stdout
if (quiet) {
process.stdout.write(`https://${deploymentUrl}`);
}
output.print(
prependEmoji(
`${isProdDeployment ? 'Production' : 'Preview'}: ${chalk.bold(
@@ -244,13 +239,6 @@ export default async function main(
const { isFile, path } = pathValidation;
const autoConfirm = argv['--confirm'] || isFile;
// build `meta`
const meta = Object.assign(
{},
parseMeta(localConfig.meta),
parseMeta(argv['--meta'])
);
// --no-scale
if (argv['--no-scale']) {
warn(`The option --no-scale is only supported on Now 1.0 deployments`);
@@ -266,7 +254,125 @@ export default async function main(
);
}
if (localConfig && localConfig.name) {
const client = new Client({
apiUrl: ctx.apiUrl,
token: ctx.authConfig.token,
debug: debugEnabled,
});
// retrieve `project` and `org` from .now
const link = await getLinkedProject(output, client, path);
if (link.status === 'error') {
return link.exitCode;
}
let { org, project, status } = link;
let newProjectName = null;
let rootDirectory = project ? project.rootDirectory : null;
if (status === 'not_linked') {
const shouldStartSetup =
autoConfirm ||
(await confirm(
`Set up and deploy ${chalk.cyan(`${toHumanPath(path)}`)}?`,
true
));
if (!shouldStartSetup) {
output.print(`Aborted. Project not set up.\n`);
return 0;
}
org = await selectOrg(
output,
'Which scope do you want to deploy to?',
client,
ctx.config.currentTeam,
autoConfirm
);
// We use `localConfig` here to read the name
// even though the `now.json` file can change
// afterwards, this is fine since the property
// will be deprecated and can be replaced with
// user input.
const detectedProjectName = getProjectName({
argv,
nowConfig: localConfig || {},
isFile,
paths,
});
const projectOrNewProjectName = await inputProject(
output,
client,
org,
detectedProjectName,
autoConfirm
);
if (typeof projectOrNewProjectName === 'string') {
newProjectName = projectOrNewProjectName;
rootDirectory = await inputRootDirectory(path, output, autoConfirm);
} else {
project = projectOrNewProjectName;
rootDirectory = project.rootDirectory;
// we can already link the project
await linkFolderToProject(
output,
path,
{
projectId: project.id,
orgId: org.id,
},
project.name,
org.slug
);
status = 'linked';
}
}
const sourcePath = rootDirectory ? join(path, rootDirectory) : path;
if (
rootDirectory &&
(await validateRootDirectory(
output,
path,
sourcePath,
project
? `To change your project settings, go to https://zeit.co/${org.slug}/${project.name}/settings`
: ''
)) === false
) {
return 1;
}
// If Root Directory is used we'll try to read the config
// from there instead and use it if it exists.
if (rootDirectory) {
const rootDirectoryConfig = readLocalConfig(sourcePath);
if (rootDirectoryConfig) {
debug(`Read local config from root directory (${rootDirectory})`);
localConfig = rootDirectoryConfig;
} else if (localConfig) {
output.print(
`${prependEmoji(
`The ${highlight(
'now.json'
)} file should be inside of the provided root directory.`,
emoji('warning')
)}\n`
);
}
}
localConfig = localConfig || {};
if (localConfig.name) {
output.print(
`${prependEmoji(
`The ${code('name')} property in ${highlight(
@@ -316,6 +422,13 @@ export default async function main(
}
}
// build `meta`
const meta = Object.assign(
{},
parseMeta(localConfig.meta),
parseMeta(argv['--meta'])
);
// Merge dotenv config, `env` from now.json, and `--env` / `-e` arguments
const deploymentEnv = Object.assign(
{},
@@ -373,97 +486,6 @@ export default async function main(
target = 'production';
}
const client = new Client({
apiUrl: ctx.apiUrl,
token: ctx.authConfig.token,
debug: debugEnabled,
});
// retrieve `project` and `org` from .now
const link = await getLinkedProject(output, client, path);
if (link.status === 'error') {
return link.exitCode;
}
let { org, project, status } = link;
let newProjectName = null;
let rootDirectory = project ? project.rootDirectory : null;
if (status === 'not_linked') {
const shouldStartSetup =
autoConfirm ||
(await confirm(
`Set up and deploy ${chalk.cyan(`${toHumanPath(path)}`)}?`,
true
));
if (!shouldStartSetup) {
output.print(`Aborted. Project not set up.\n`);
return 0;
}
org = await selectOrg(
output,
'Which scope do you want to deploy to?',
client,
ctx.config.currentTeam,
autoConfirm
);
const detectedProjectName = getProjectName({
argv,
nowConfig: localConfig,
isFile,
paths,
});
const projectOrNewProjectName = await inputProject(
output,
client,
org,
detectedProjectName,
autoConfirm
);
if (typeof projectOrNewProjectName === 'string') {
newProjectName = projectOrNewProjectName;
rootDirectory = await inputRootDirectory(path, output, autoConfirm);
} else {
project = projectOrNewProjectName;
rootDirectory = project.rootDirectory;
// we can already link the project
await linkFolderToProject(
output,
path,
{
projectId: project.id,
orgId: org.id,
},
project.name,
org.slug
);
status = 'linked';
}
}
const sourcePath = rootDirectory ? join(path, rootDirectory) : path;
if (
rootDirectory &&
(await validateRootDirectory(
output,
path,
sourcePath,
project
? `To change your project settings, go to https://zeit.co/${org.slug}/${project.name}/settings`
: ''
)) === false
) {
return 1;
}
const currentTeam = org.type === 'team' ? org.id : undefined;
const now = new Now({ apiUrl, token, debug: debugEnabled, currentTeam });
let deployStamp = stamp();
@@ -659,7 +681,6 @@ export default async function main(
deployment,
deployStamp,
!argv['--no-clipboard'],
quiet,
isFile
);
}

View File

@@ -852,7 +852,7 @@ async function sync({
const { url } = now;
const dcs =
deploymentType !== 'static' && deployment.scale
? chalk` ({bold ${Object.keys(deployment.scale).join(', ')})`
? ` (${ chalk.bold(Object.keys(deployment.scale).join(', ')) })`
: '';
if (isTTY) {

View File

@@ -83,6 +83,7 @@ export default async function processDeployment({
deployStamp,
force,
nowConfig,
quiet,
} = args;
const { debug } = output;
@@ -179,6 +180,10 @@ export default async function processDeployment({
printInspectUrl(output, event.payload.url, deployStamp, org.slug);
if (quiet) {
process.stdout.write(`https://${event.payload.url}`);
}
if (queuedSpinner === null) {
queuedSpinner =
event.payload.readyState === 'QUEUED'

View File

@@ -7,7 +7,6 @@ import { delimiter, dirname, join } from 'path';
import { fork, ChildProcess } from 'child_process';
import { createFunction } from '@zeit/fun';
import { Builder, File, Lambda, FileBlob, FileFsRef } from '@now/build-utils';
import which from 'which';
import plural from 'pluralize';
import minimatch from 'minimatch';
import _treeKill from 'tree-kill';
@@ -44,12 +43,6 @@ interface BuildMessageResult extends BuildMessage {
const treeKill = promisify(_treeKill);
let nodeBinPromise: Promise<string>;
async function getNodeBin(): Promise<string> {
return which.sync('node', { nothrow: true }) || process.execPath;
}
async function createBuildProcess(
match: BuildMatch,
buildEnv: EnvConfig,
@@ -57,13 +50,8 @@ async function createBuildProcess(
output: Output,
yarnPath?: string
): Promise<ChildProcess> {
if (!nodeBinPromise) {
nodeBinPromise = getNodeBin();
}
const [execPath, modulePath] = await Promise.all([
nodeBinPromise,
builderModulePathPromise,
]);
const { execPath } = process;
const modulePath = await builderModulePathPromise;
// Ensure that `node` is in the builder's `PATH`
let PATH = `${dirname(execPath)}${delimiter}${process.env.PATH}`;

View File

@@ -11,7 +11,7 @@ import { randomBytes } from 'crypto';
import serveHandler from 'serve-handler';
import { watch, FSWatcher } from 'chokidar';
import { parse as parseDotenv } from 'dotenv';
import { basename, dirname, extname, join, delimiter } from 'path';
import { basename, dirname, extname, join } from 'path';
import { getTransformedRoutes, HandleValue } from '@now/routing-utils';
import directoryTemplate from 'serve-handler/src/directory';
import getPort from 'get-port';
@@ -24,10 +24,8 @@ import {
FileFsRef,
PackageJson,
detectBuilders,
detectRoutes,
detectApiDirectory,
detectApiExtensions,
execAsync,
spawnCommand,
} from '@now/build-utils';
@@ -549,10 +547,19 @@ export default class DevServer {
const featHandleMiss = true; // enable for zero config
const { projectSettings, cleanUrls, trailingSlash } = config;
let { builders, warnings, errors } = await detectBuilders(files, pkg, {
let {
builders,
warnings,
errors,
defaultRoutes,
redirectRoutes,
} = await detectBuilders(files, pkg, {
tag: getDistTag(cliVersion) === 'canary' ? 'canary' : 'latest',
functions: config.functions,
...(projectSettings ? { projectSettings } : {}),
featHandleMiss,
cleanUrls,
trailingSlash,
});
if (errors) {
@@ -569,32 +576,15 @@ export default class DevServer {
builders = builders.filter(filterFrontendBuilds);
}
const {
defaultRoutes,
redirectRoutes,
error: routesError,
} = await detectRoutes(
files,
builders,
featHandleMiss,
cleanUrls,
trailingSlash
);
config.builds = config.builds || [];
config.builds.push(...builders);
if (routesError) {
this.output.error(routesError.message);
await this.exit();
} else {
const routes: RouteConfig[] = [];
const { routes: nowConfigRoutes } = config;
routes.push(...(redirectRoutes || []));
routes.push(...(nowConfigRoutes || []));
routes.push(...(defaultRoutes || []));
config.routes = routes;
}
const routes: RouteConfig[] = [];
const { routes: nowConfigRoutes } = config;
routes.push(...(redirectRoutes || []));
routes.push(...(nowConfigRoutes || []));
routes.push(...(defaultRoutes || []));
config.routes = routes;
}
}
@@ -1633,18 +1623,14 @@ export default class DevServer {
}
async runDevCommand() {
if (!this.devCommand) return;
const { devCommand, cwd } = this;
const cwd = this.cwd;
const { stdout: yarnBinStdout } = await execAsync('yarn', ['bin'], {
cwd,
});
const yarnBinPath = yarnBinStdout.trim();
if (!devCommand) {
return;
}
this.output.log(
`Running Dev Command ${chalk.cyan.bold(`${this.devCommand}`)}`
`Running Dev Command ${chalk.cyan.bold(`${devCommand}`)}`
);
const port = await getPort();
@@ -1653,16 +1639,19 @@ export default class DevServer {
...process.env,
...this.buildEnv,
NOW_REGION: 'dev1',
PORT: `${port}`,
};
const devCommand = this.devCommand
// This is necesary so that the dev command in the Project
// will work cross-platform (especially Windows).
let command = devCommand
.replace(/\$PORT/g, `${port}`)
.replace(/%PORT%/g, `${port}`);
this.output.debug(
`Starting dev command with parameters : ${JSON.stringify({
cwd: this.cwd,
devCommand,
cwd,
command,
port,
})}`
);
@@ -1671,17 +1660,21 @@ export default class DevServer {
.then(() => true)
.catch(() => false);
if (!isNpxAvailable) {
env.PATH = `${yarnBinPath}${delimiter}${env.PATH}`;
if (isNpxAvailable) {
command = `npx --no-install ${command}`;
} else {
const isYarnAvailable = await which('yarn')
.then(() => true)
.catch(() => false);
if (isYarnAvailable) {
command = `yarn run --silent ${command}`;
}
}
this.output.debug('Spawning dev command');
this.output.debug(`PATH is ${env.PATH}`);
this.output.debug(`Spawning dev command: ${command}`);
const p = spawnCommand(
isNpxAvailable ? `npx --no-install ${devCommand}` : devCommand,
{ stdio: 'inherit', cwd, env }
);
const p = spawnCommand(command, { stdio: 'inherit', cwd, env });
p.on('exit', () => {
this.devProcessPort = undefined;

View File

@@ -5,7 +5,7 @@ import execa from 'execa';
import fs from 'fs-extra';
import fetch from 'node-fetch';
import listen from 'async-listen';
import { request, createServer } from 'http';
import { createServer } from 'http';
import createOutput from '../src/util/output';
import DevServer from '../src/util/dev/server';
import { installBuilders, getBuildUtils } from '../src/util/dev/builder-cache';
@@ -13,12 +13,28 @@ import parseListen from '../src/util/dev/parse-listen';
async function runNpmInstall(fixturePath) {
if (await fs.exists(path.join(fixturePath, 'package.json'))) {
return execa('yarn', ['install'], { cwd: fixturePath });
return execa('yarn', ['install'], { cwd: fixturePath, shell: true });
}
}
const skipOnWindows = new Set([
'now-dev-default-builds-and-routes',
'now-dev-static-routes',
'now-dev-static-build-routing',
'now-dev-directory-listing',
'now-dev-api-with-public',
'now-dev-api-with-static',
'now-dev-custom-404',
]);
function testFixture(name, fn) {
return async t => {
if (process.platform === 'win32' && skipOnWindows.has(name)) {
console.log(`Skipping test "${name}" on Windows.`);
t.is(true, true);
return;
}
let server;
const fixturePath = path.join(__dirname, 'fixtures', 'unit', name);
@@ -68,14 +84,6 @@ function validateResponseHeaders(t, res, podId = null) {
}
}
function get(url) {
return new Promise((resolve, reject) => {
request(url, resolve)
.on('error', reject)
.end();
});
}
test(
'[DevServer] Maintains query when invoking lambda',
testFixture('now-dev-query-invoke', async (t, server) => {
@@ -138,16 +146,23 @@ test(
test(
'[DevServer] Allow `cache-control` to be overwritten',
testFixture('now-dev-headers', async (t, server) => {
const res = await get(
const res = await fetch(
`${server.address}/?name=cache-control&value=immutable`
);
t.is(res.headers['cache-control'], 'immutable');
t.is(res.headers.get('cache-control'), 'immutable');
})
);
test(
'[DevServer] Sends `etag` header for static files',
testFixture('now-dev-headers', async (t, server) => {
if (process.platform === 'win32') {
console.log(
'Skipping "etag" test on windows since it yields a different result.'
);
t.is(true, true);
return;
}
const res = await fetch(`${server.address}/foo.txt`);
t.is(res.headers.get('etag'), '"d263af8ab880c0b97eb6c5c125b5d44f9e5addd9"');
t.is(await res.text(), 'hi\n');
@@ -400,12 +415,14 @@ test('[DevServer] parseListen()', t => {
t.deepEqual(parseListen('0.0.0.0'), [3000, '0.0.0.0']);
t.deepEqual(parseListen('127.0.0.1:3005'), [3005, '127.0.0.1']);
t.deepEqual(parseListen('tcp://127.0.0.1:5000'), [5000, '127.0.0.1']);
t.deepEqual(parseListen('unix:/home/user/server.sock'), [
'/home/user/server.sock',
]);
t.deepEqual(parseListen('pipe:\\\\.\\pipe\\PipeName'), [
'\\\\.\\pipe\\PipeName',
]);
if (process.platform !== 'win32') {
t.deepEqual(parseListen('unix:/home/user/server.sock'), [
'/home/user/server.sock',
]);
t.deepEqual(parseListen('pipe:\\\\.\\pipe\\PipeName'), [
'\\\\.\\pipe\\PipeName',
]);
}
let err;
try {

View File

@@ -0,0 +1 @@
SKIP_PREFLIGHT_CHECK=true

View File

@@ -22,3 +22,4 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
!yarn.lock
!.env

View File

@@ -109,13 +109,14 @@ function validateResponseHeaders(t, res) {
async function exec(directory, args = []) {
return execa(binaryPath, ['dev', directory, ...args], {
reject: false,
shell: true,
env: { __NOW_SKIP_DEV_COMMAND: 1 },
});
}
async function runNpmInstall(fixturePath) {
if (await fs.exists(path.join(fixturePath, 'package.json'))) {
return execa('yarn', ['install'], { cwd: fixturePath });
return execa('yarn', ['install'], { cwd: fixturePath, shell: true });
}
}
@@ -123,6 +124,7 @@ async function getPackedBuilderPath(builderDirName) {
const packagePath = path.join(__dirname, '..', '..', '..', builderDirName);
const output = await execa('npm', ['pack'], {
cwd: packagePath,
shell: true,
});
if (output.exitCode !== 0 || output.stdout.trim() === '') {
@@ -160,6 +162,7 @@ async function testFixture(directory, opts = {}, args = []) {
{
reject: false,
detached: true,
shell: true,
stdio: 'pipe',
...opts,
env: { ...opts.env, __NOW_SKIP_DEV_COMMAND: 1 },
@@ -226,6 +229,7 @@ function testFixtureStdio(directory, fn) {
let printedOutput = false;
dev = execa(binaryPath, ['dev', dir, '-l', port], {
shell: true,
env: { __NOW_SKIP_DEV_COMMAND: 1 },
});
@@ -638,7 +642,7 @@ test(
await testPath(200, '/about/', 'About Page');
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(200, '/style.css', 'body { color: green }');
await testPath(308, '/index.html', '', { Location: '/' });
await testPath(308, '/about.html', '', { Location: '/about/' });
await testPath(308, '/sub/index.html', '', { Location: '/sub/' });
@@ -653,17 +657,15 @@ test(
'[now dev] test trailingSlash true serve correct content',
testFixtureStdio('test-trailing-slash', async (t, port, testPath) => {
await testPath(200, '/', 'Index Page');
await testPath(200, '/index.html/', 'Index Page');
await testPath(200, '/about.html/', 'About Page');
await testPath(200, '/index.html', 'Index Page');
await testPath(200, '/about.html', 'About Page');
await testPath(200, '/sub/', 'Sub Index Page');
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(308, '/about.html', '', { Location: '/about.html/' });
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(308, '/about.html/', '', { Location: '/about.html' });
await testPath(308, '/style.css/', '', { Location: '/style.css' });
await testPath(308, '/sub', '', { Location: '/sub/' });
await testPath(308, '/sub/another.html', '', {
Location: '/sub/another.html/',
});
})
);
@@ -772,17 +774,17 @@ test('[now dev] 03-aurelia', async t => {
await tester(t);
});
// test(
// '[now dev] 04-create-react-app-node',
// testFixtureStdio('create-react-app', async(t, port) => {
// const response = await fetch(`http://localhost:${port}`);
test(
'[now dev] 04-create-react-app',
testFixtureStdio('04-create-react-app', async (t, port) => {
const response = await fetch(`http://localhost:${port}`);
// validateResponseHeaders(t, response);
validateResponseHeaders(t, response);
// const body = await response.text();
// t.regex(body, /React App/gm);
// })
// );
const body = await response.text();
t.regex(body, /React App/gm);
})
);
test('[now dev] 05-gatsby', async t => {
if (shouldSkip(t, '05-gatsby', '>^6.14.0 || ^8.10.0 || >=9.10.0')) return;

View File

@@ -487,6 +487,14 @@ CMD ["node", "index.js"]`,
},
'project-root-directory': {
'src/index.html': '<h1>I am a website.</h1>',
'src/now.json': JSON.stringify({
rewrites: [
{
source: '/i-do-exist',
destination: '/',
},
],
}),
},
};

View File

@@ -2279,17 +2279,19 @@ test('should prefill "project name" prompt with now.json `name`', async t => {
let isDeprecated = false;
await waitForPrompt(now, chunk => {
if (chunk.includes('The `name` property in now.json is deprecated')) {
now.stderr.on('data', data => {
if (
data.toString().includes('The `name` property in now.json is deprecated')
) {
isDeprecated = true;
}
});
await waitForPrompt(now, chunk => {
return /Set up and deploy [^?]+\?/.test(chunk);
});
now.stdin.write('yes\n');
t.is(isDeprecated, true);
await waitForPrompt(now, chunk =>
chunk.includes('Which scope do you want to deploy to?')
);
@@ -2318,6 +2320,8 @@ test('should prefill "project name" prompt with now.json `name`', async t => {
const output = await now;
t.is(output.exitCode, 0, formatOutput(output));
t.is(isDeprecated, true);
// clean up
await remove(path.join(directory, 'now.json'));
});
@@ -2448,9 +2452,14 @@ test('use `rootDirectory` from project when deploying', async t => {
const secondResult = await execute([directory, '--public']);
t.is(secondResult.exitCode, 0, formatOutput(secondResult));
const pageResponse = await fetch(secondResult.stdout);
t.is(pageResponse.status, 200);
t.regex(await pageResponse.text(), /I am a website/gm);
const pageResponse1 = await fetch(secondResult.stdout);
t.is(pageResponse1.status, 200);
t.regex(await pageResponse1.text(), /I am a website/gm);
// Ensures that the `now.json` file has been applied
const pageResponse2 = await fetch(`${secondResult.stdout}/i-do-exist`);
t.is(pageResponse2.status, 200);
t.regex(await pageResponse2.text(), /I am a website/gm);
await apiFetch(`/v2/projects/${projectId}`, {
method: 'DELETE',

View File

@@ -1,4 +1,4 @@
import { join } from 'path';
import { join, sep } from 'path';
import { send } from 'micro';
import test from 'ava';
import sinon from 'sinon';
@@ -26,7 +26,7 @@ import getUpdateCommand from '../src/util/get-update-command';
import { isCanary } from '../src/util/is-canary';
const output = createOutput({ debug: false });
const prefix = `${join(__dirname, 'fixtures', 'unit')}/`;
const prefix = `${join(__dirname, 'fixtures', 'unit')}${sep}`;
const base = path => path.replace(prefix, '');
const fixture = name => join(prefix, name);
@@ -37,7 +37,8 @@ const getNpmFiles = async dir => {
strict: false,
});
return getNpmFiles_(dir, pkg, nowConfig, { hasNowJson, output });
const files = await getNpmFiles_(dir, pkg, nowConfig, { hasNowJson, output });
return normalizeWindowsPaths(files);
};
const getDockerFiles = async dir => {
@@ -46,7 +47,8 @@ const getDockerFiles = async dir => {
strict: false,
});
return getDockerFiles_(dir, nowConfig, { hasNowJson, output });
const files = await getDockerFiles_(dir, nowConfig, { hasNowJson, output });
return normalizeWindowsPaths(files);
};
const getStaticFiles = async (dir, isBuilds = false) => {
@@ -56,7 +58,22 @@ const getStaticFiles = async (dir, isBuilds = false) => {
strict: false,
});
return getStaticFiles_(dir, nowConfig, { hasNowJson, output, isBuilds });
const files = await getStaticFiles_(dir, nowConfig, {
hasNowJson,
output,
isBuilds,
});
return normalizeWindowsPaths(files);
};
const normalizeWindowsPaths = files => {
if (process.platform === 'win32') {
const prefix = 'D:/a/now/now/packages/now-cli/test/fixtures/unit/';
return files.map(f =>
f.replace(/\\/g, '/').slice(prefix.length)
);
}
return files;
};
test('`files`', async t => {
@@ -273,6 +290,11 @@ test('extensionless main', async t => {
});
test('hashes', async t => {
if (process.platform === 'win32') {
console.log('Skipping "hashes" test on Windows');
t.is(true, true);
return;
}
const files = await getNpmFiles(fixture('hashes'));
const hashes = await hash(files);
t.is(hashes.size, 3);
@@ -542,6 +564,11 @@ test('support `package.json:now.type` to bypass multiple manifests error', async
});
test('friendly error for malformed JSON', async t => {
if (process.platform === 'win32') {
console.log('Skipping "friendly error for malformed JSON" test on Windows');
t.is(true, true);
return;
}
const err = await t.throwsAsync(() =>
readMetadata(fixture('json-syntax-error'), {
quiet: true,

View File

@@ -123,7 +123,7 @@ export async function createGo(
export async function downloadGo(
dir = GO_DIR,
version = '1.12',
version = '1.13.7',
platform = process.platform,
arch = process.arch
) {

View File

@@ -23,15 +23,6 @@ interface Analyzed {
functionName: string;
watch: string[];
}
interface BuildParamsMeta {
isDev: boolean | undefined;
}
interface BuildParamsType extends BuildOptions {
files: Files;
entrypoint: string;
workPath: string;
meta: BuildParamsMeta;
}
// Initialize private git repo for Go Modules
async function initPrivateGit(credentials: string) {
@@ -45,6 +36,26 @@ async function initPrivateGit(credentials: string) {
await writeFile(join(homedir(), '.git-credentials'), credentials);
}
/**
* Since `go build` does not support files that begin with a square bracket,
* we must rename to something temporary to support Path Segments.
* The output file is not renamed because v3 builders can't rename outputs
* which works great for this feature.
*/
async function getRenamedEntrypoint(entrypoint: string, files: Files) {
const filename = basename(entrypoint);
if (filename.startsWith('[')) {
const newEntrypoint = entrypoint.replace('/[', '/now-bracket[');
const file = files[entrypoint];
delete files[entrypoint];
files[newEntrypoint] = file;
debug(`Renamed entrypoint from ${entrypoint} to ${newEntrypoint}`);
entrypoint = newEntrypoint;
}
return entrypoint;
}
export const version = 3;
export async function build({
@@ -52,8 +63,8 @@ export async function build({
entrypoint,
config,
workPath,
meta = {} as BuildParamsMeta,
}: BuildParamsType) {
meta = {},
}: BuildOptions) {
if (process.env.GIT_CREDENTIALS && !meta.isDev) {
debug('Initialize Git credentials...');
await initPrivateGit(process.env.GIT_CREDENTIALS);
@@ -70,7 +81,7 @@ We highly recommend you leverage Go Modules in your project.
Learn more: https://github.com/golang/go/wiki/Modules
`);
}
entrypoint = await getRenamedEntrypoint(entrypoint, files);
const entrypointArr = entrypoint.split(sep);
// eslint-disable-next-line prefer-const
@@ -79,22 +90,19 @@ Learn more: https://github.com/golang/go/wiki/Modules
getWriteableDirectory(),
]);
const forceMove = Boolean(meta.isDev);
const srcPath = join(goPath, 'src', 'lambda');
let downloadedFiles;
if (meta.isDev) {
downloadedFiles = await download(files, workPath, meta);
} else {
downloadedFiles = await download(files, srcPath);
}
let downloadPath = meta.isDev ? workPath : srcPath;
let downloadedFiles = await download(files, downloadPath, meta);
debug(`Parsing AST for "${entrypoint}"`);
let analyzed: string;
try {
let goModAbsPathDir = '';
for (const file of Object.keys(downloadedFiles)) {
if (file === 'go.mod') {
goModAbsPathDir = dirname(downloadedFiles[file].fsPath);
}
const fileName = 'go.mod';
if (fileName in downloadedFiles) {
goModAbsPathDir = dirname(downloadedFiles[fileName].fsPath);
debug(`Found ${fileName} file in "${goModAbsPathDir}"`);
}
analyzed = await getAnalyzedEntrypoint(
downloadedFiles[entrypoint].fsPath,
@@ -139,6 +147,7 @@ Learn more: https://zeit.co/docs/v2/advanced/builders/#go
await download(downloadedFiles, destNow);
downloadedFiles = await glob('**', destNow);
downloadPath = destNow;
}
// find `go.mod` in downloadedFiles
@@ -147,15 +156,38 @@ Learn more: https://zeit.co/docs/v2/advanced/builders/#go
let goModPath = '';
let isGoModInRootDir = false;
for (const file of Object.keys(downloadedFiles)) {
const fileDirname = dirname(downloadedFiles[file].fsPath);
const { fsPath } = downloadedFiles[file];
const fileDirname = dirname(fsPath);
if (file === 'go.mod') {
isGoModExist = true;
isGoModInRootDir = true;
goModPath = fileDirname;
} else if (file.endsWith('go.mod') && !file.endsWith('vendor')) {
} else if (file.endsWith('go.mod')) {
if (entrypointDirname === fileDirname) {
isGoModExist = true;
goModPath = fileDirname;
debug(`Found file dirname equals entrypoint dirname: ${fileDirname}`);
break;
}
if (!isGoModInRootDir && config.zeroConfig && file === 'api/go.mod') {
// We didn't find `/go.mod` but we found `/api/go.mod` so move it to the root
isGoModExist = true;
isGoModInRootDir = true;
goModPath = join(fileDirname, '..');
const pathParts = fsPath.split(sep);
pathParts.pop(); // Remove go.mod
pathParts.pop(); // Remove api
pathParts.push('go.mod');
const newFsPath = pathParts.join(sep);
debug(`Moving api/go.mod to root: ${fsPath} to ${newFsPath}`);
await move(fsPath, newFsPath, { overwrite: forceMove });
const oldSumPath = join(dirname(fsPath), 'go.sum');
const newSumPath = join(dirname(newFsPath), 'go.sum');
if (await pathExists(oldSumPath)) {
debug(`Moving api/go.sum to root: ${oldSumPath} to ${newSumPath}`);
await move(oldSumPath, newSumPath, { overwrite: forceMove });
}
break;
}
}
@@ -165,10 +197,13 @@ Learn more: https://zeit.co/docs/v2/advanced/builders/#go
const includedFiles: Files = {};
if (config && config.includeFiles) {
for (const pattern of config.includeFiles) {
const files = await glob(pattern, input);
for (const assetName of Object.keys(files)) {
includedFiles[assetName] = files[assetName];
const patterns = Array.isArray(config.includeFiles)
? config.includeFiles
: [config.includeFiles];
for (const pattern of patterns) {
const fsFiles = await glob(pattern, input);
for (const [assetName, asset] of Object.entries(fsFiles)) {
includedFiles[assetName] = asset;
}
}
}
@@ -221,7 +256,6 @@ Learn more: https://zeit.co/docs/v2/advanced/builders/#go
if (isGoModExist) {
const goModContents = await readFile(join(goModPath, 'go.mod'), 'utf8');
const usrModName = goModContents.split('\n')[0].split(' ')[1];
if (entrypointArr.length > 1 && isGoModInRootDir) {
const cleanPackagePath = [...entrypointArr];
cleanPackagePath.pop();
@@ -235,18 +269,14 @@ Learn more: https://zeit.co/docs/v2/advanced/builders/#go
.replace('__NOW_HANDLER_PACKAGE_NAME', goPackageName)
.replace('__NOW_HANDLER_FUNC_NAME', goFuncName);
if (meta.isDev && isGoModExist && isGoModInRootDir) {
await writeFile(
join(dirname(downloadedFiles['go.mod'].fsPath), mainModGoFileName),
mainModGoContents
);
} else if (isGoModExist && isGoModInRootDir) {
await writeFile(join(srcPath, mainModGoFileName), mainModGoContents);
if (isGoModExist && isGoModInRootDir) {
debug('[mod-root] Write main file to ' + downloadPath);
await writeFile(join(downloadPath, mainModGoFileName), mainModGoContents);
} else if (isGoModExist && !isGoModInRootDir) {
// using `go.mod` path to write main__mod__.go
debug('[mod-other] Write main file to ' + goModPath);
await writeFile(join(goModPath, mainModGoFileName), mainModGoContents);
} else {
// using `entrypointDirname` to write main__mod__.go
debug('[entrypoint] Write main file to ' + entrypointDirname);
await writeFile(
join(entrypointDirname, mainModGoFileName),
mainModGoContents
@@ -257,11 +287,6 @@ Learn more: https://zeit.co/docs/v2/advanced/builders/#go
try {
// default path
let finalDestination = join(entrypointDirname, packageName, entrypoint);
let forceMove = false;
if (meta.isDev) {
forceMove = true;
}
// if `entrypoint` include folder, only use filename
if (entrypointArr.length > 1) {
@@ -286,10 +311,8 @@ Learn more: https://zeit.co/docs/v2/advanced/builders/#go
}
let baseGoModPath = '';
if (meta.isDev && isGoModExist && isGoModInRootDir) {
baseGoModPath = dirname(downloadedFiles['go.mod'].fsPath);
} else if (isGoModExist && isGoModInRootDir) {
baseGoModPath = srcPath;
if (isGoModExist && isGoModInRootDir) {
baseGoModPath = downloadPath;
} else if (isGoModExist && !isGoModInRootDir) {
baseGoModPath = goModPath;
} else {
@@ -302,12 +325,12 @@ Learn more: https://zeit.co/docs/v2/advanced/builders/#go
await move(
join(baseGoModPath, 'go.mod.bk'),
join(baseGoModPath, 'go.mod'),
{ overwrite: true }
{ overwrite: forceMove }
);
await move(
join(baseGoModPath, 'go.sum.bk'),
join(baseGoModPath, 'go.sum'),
{ overwrite: true }
{ overwrite: forceMove }
);
}
}
@@ -337,12 +360,12 @@ Learn more: https://zeit.co/docs/v2/advanced/builders/#go
await move(
join(baseGoModPath, 'go.mod'),
join(baseGoModPath, 'go.mod.bk'),
{ overwrite: true }
{ overwrite: forceMove }
);
await move(
join(baseGoModPath, 'go.sum'),
join(baseGoModPath, 'go.sum.bk'),
{ overwrite: true }
{ overwrite: forceMove }
);
}
} else {

View File

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

View File

@@ -0,0 +1,16 @@
package cowsay
import (
"fmt"
"io/ioutil"
"net/http"
)
// Handler function
func Handler(w http.ResponseWriter, r *http.Request) {
bts, err := ioutil.ReadFile("templates/another.txt")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
fmt.Fprintf(w, string(bts))
}

View File

@@ -7,12 +7,23 @@
"config": {
"includeFiles": ["templates/**"]
}
},
{
"src": "another.go",
"use": "@now/go",
"config": {
"includeFiles": "templates/**"
}
}
],
"probes": [
{
"path": "/",
"mustContain": "foobar from file"
},
{
"path": "/another.go",
"mustContain": "another text file"
}
]
}

View File

@@ -0,0 +1 @@
another text file

View File

@@ -0,0 +1,3 @@
package somepackage
const Foo = "Dependency:RANDOMNESS_PLACEHOLDER"

View File

@@ -0,0 +1,5 @@
module github.com/zeit/does-not-exist
go 1.13
require github.com/dhruvbird/go-cowsay v0.0.0-20131019225157-6fd7bd0281c0 // indirect

View File

@@ -0,0 +1,2 @@
github.com/dhruvbird/go-cowsay v0.0.0-20131019225157-6fd7bd0281c0 h1:t/CyPFyQ/Y5zHoxCd7hZ+15fOYIwKPsTxyqTmBlA2A8=
github.com/dhruvbird/go-cowsay v0.0.0-20131019225157-6fd7bd0281c0/go.mod h1:Pj0EWCBFS61kh+c6d7rUu6496j3XEAL/QnDQ3D0V8Js=

View File

@@ -0,0 +1,12 @@
package routes
import (
"fmt"
"net/http"
"github.com/zeit/does-not-exist/api/_pkg/somepackage"
)
func Handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello %v", somepackage.Foo)
}

View File

@@ -0,0 +1,16 @@
{
"version": 2,
"builds": [
{
"src": "api/v1/**/*.go",
"use": "@now/go",
"config": { "zeroConfig": true }
}
],
"probes": [
{
"path": "/api/v1/routes/someroute.go",
"mustContain": "Hello Dependency:RANDOMNESS_PLACEHOLDER"
}
]
}

View File

@@ -0,0 +1,10 @@
package handler
import (
"fmt"
"net/http"
)
func Handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello:RANDOMNESS_PLACEHOLDER")
}

View File

@@ -0,0 +1,10 @@
package handler
import (
"fmt"
"net/http"
)
func Handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hi:RANDOMNESS_PLACEHOLDER")
}

View File

@@ -0,0 +1,11 @@
{
"version": 2,
"builds": [{ "src": "api/**/*.go", "use": "@now/go" }],
"probes": [
{
"path": "/api/[hello].go",
"mustContain": "hello:RANDOMNESS_PLACEHOLDER"
},
{ "path": "/api/sub/[hi].go", "mustContain": "hi:RANDOMNESS_PLACEHOLDER" }
]
}

View File

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

View File

@@ -612,13 +612,12 @@ export const build = async ({
// Prerendered routes emit a `.html` file but should not be treated as a
// static page.
// Lazily prerendered routes do not have a `.html` file so we don't need
// to check/skip it here.
// Lazily prerendered routes have a fallback `.html` file on newer
// Next.js versions so we need to also not treat it as a static page here.
if (
Object.prototype.hasOwnProperty.call(
prerenderManifest.routes,
routeName
)
prerenderManifest.routes[routeName] ||
(prerenderManifest.lazyRoutes[routeName] &&
prerenderManifest.lazyRoutes[routeName].fallback)
) {
return;
}
@@ -848,12 +847,18 @@ export const build = async ({
const onPrerenderRoute = (routeKey: string, isLazy: boolean) => {
// Get the route file as it'd be mounted in the builder output
const routeFileNoExt = routeKey === '/' ? '/index' : routeKey;
const lazyHtmlFallback =
isLazy && prerenderManifest.lazyRoutes[routeKey].fallback;
const htmlFsRef = isLazy
? null
: new FileFsRef({
fsPath: path.join(pagesDir, `${routeFileNoExt}.html`),
});
const htmlFsRef =
isLazy && !lazyHtmlFallback
? null
: new FileFsRef({
fsPath: path.join(
pagesDir,
`${lazyHtmlFallback || routeFileNoExt + '.html'}`
),
});
const jsonFsRef = isLazy
? null
: new FileFsRef({

View File

@@ -623,6 +623,7 @@ export type NextPrerenderedRoutes = {
lazyRoutes: {
[route: string]: {
fallback?: string;
routeRegex: string;
dataRoute: string;
dataRouteRegex: string;
@@ -726,6 +727,7 @@ export async function getPrerenderManifest(
};
dynamicRoutes: {
[key: string]: {
fallback?: string;
routeRegex: string;
dataRoute: string;
dataRouteRegex: string;
@@ -759,11 +761,17 @@ export async function getPrerenderManifest(
lazyRoutes.forEach(lazyRoute => {
const {
routeRegex,
fallback,
dataRoute,
dataRouteRegex,
} = manifest.dynamicRoutes[lazyRoute];
ret.lazyRoutes[lazyRoute] = { routeRegex, dataRoute, dataRouteRegex };
ret.lazyRoutes[lazyRoute] = {
routeRegex,
fallback,
dataRoute,
dataRouteRegex,
};
});
return ret;

View File

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

View File

@@ -0,0 +1,170 @@
{
"version": 2,
"builds": [{ "src": "package.json", "use": "@now/next" }],
"probes": [
{
"path": "/lambda",
"status": 200,
"responseHeaders": {
"x-now-cache": "MISS"
}
},
{
"path": "/forever",
"status": 200,
"responseHeaders": {
"x-now-cache": "MISS"
}
},
{ "delay": 2000 },
{
"path": "/forever",
"status": 200,
"responseHeaders": {
"x-now-cache": "HIT"
}
},
{
"path": "/another",
"status": 200,
"responseHeaders": {
"x-now-cache": "PRERENDER"
}
},
{ "delay": 2000 },
{
"path": "/another",
"status": 200,
"responseHeaders": {
"x-now-cache": "HIT"
}
},
{
"path": "/blog/post-1",
"status": 200,
"responseHeaders": {
"x-now-cache": "PRERENDER"
}
},
{ "delay": 2000 },
{
"path": "/blog/post-1",
"status": 200,
"responseHeaders": {
"x-now-cache": "HIT"
}
},
{
"path": "/blog/post-2",
"status": 200,
"responseHeaders": {
"x-now-cache": "PRERENDER"
}
},
{ "delay": 2000 },
{
"path": "/blog/post-2",
"status": 200,
"responseHeaders": {
"x-now-cache": "HIT"
}
},
{
"path": "/blog/post-3",
"status": 200,
"mustContain": "loading..."
},
{ "delay": 2000 },
{
"path": "/blog/post-3",
"status": 200,
"responseHeaders": {
"x-now-cache": "/HIT|STALE/"
}
},
{
"path": "/blog/post-3",
"status": 200,
"mustContain": "post-3"
},
{
"path": "/blog/post-1/comment-1",
"status": 200,
"responseHeaders": {
"x-now-cache": "PRERENDER"
}
},
{
"path": "/blog/post-2/comment-2",
"status": 200,
"responseHeaders": {
"x-now-cache": "PRERENDER"
}
},
{
"path": "/blog/post-3/comment-3",
"status": 200,
"mustContain": "loading..."
},
{
"path": "/_next/data/testing-build-id/lambda.json",
"status": 404
},
{
"path": "/_next/data/testing-build-id/forever.json",
"status": 200,
"responseHeaders": {
"x-now-cache": "MISS"
}
},
{ "delay": 2000 },
{
"path": "/blog/post-3/comment-3",
"status": 200,
"mustContain": "comment-3"
},
{
"path": "/_next/data/testing-build-id/forever.json",
"status": 200,
"responseHeaders": {
"x-now-cache": "HIT"
}
},
{
"path": "/_next/data/testing-build-id/another.json",
"status": 200,
"responseHeaders": {
"x-now-cache": "/HIT|STALE/"
}
},
{
"path": "/_next/data/testing-build-id/another2.json",
"status": 200,
"responseHeaders": {
"x-now-cache": "PRERENDER"
}
},
{ "delay": 2000 },
{
"path": "/_next/data/testing-build-id/another2.json",
"status": 200,
"responseHeaders": {
"x-now-cache": "HIT"
}
},
{
"path": "/_next/data/testing-build-id/blog/post-1.json",
"status": 200,
"responseHeaders": {
"x-now-cache": "/HIT|STALE|PRERENDER/"
}
},
{
"path": "/_next/data/testing-build-id/blog/post-1337/comment-1337.json",
"status": 200,
"responseHeaders": {
"x-now-cache": "PRERENDER"
}
}
]
}

View File

@@ -0,0 +1,7 @@
{
"dependencies": {
"next": "9.2.2-canary.16",
"react": "^16.8.6",
"react-dom": "^16.8.6"
}
}

View File

@@ -0,0 +1,21 @@
import React from 'react';
// eslint-disable-next-line camelcase
export async function unstable_getStaticProps() {
return {
props: {
world: 'world',
time: new Date().getTime(),
},
revalidate: 5,
};
}
export default ({ world, time }) => {
return (
<>
<p>hello: {world}</p>
<span>time: {time}</span>
</>
);
};

View File

@@ -0,0 +1,21 @@
import React from 'react';
// eslint-disable-next-line camelcase
export async function unstable_getStaticProps() {
return {
props: {
world: 'world',
time: new Date().getTime(),
},
revalidate: 5,
};
}
export default ({ world, time }) => {
return (
<>
<p>hello: {world}</p>
<span>time: {time}</span>
</>
);
};

View File

@@ -0,0 +1,36 @@
import React from 'react';
// eslint-disable-next-line camelcase
export async function unstable_getStaticPaths () {
return {
paths: [
'/blog/post-1/comment-1',
{ params: { post: 'post-2', comment: 'comment-2' } },
'/blog/post-1337/comment-1337',
]
}
}
// eslint-disable-next-line camelcase
export async function unstable_getStaticProps ({ params }) {
return {
props: {
post: params.post,
comment: params.comment,
time: new Date().getTime(),
},
revalidate: 2,
};
}
export default ({ post, comment, time }) => {
if (!post) return <p>loading...</p>
return (
<>
<p>Post: {post}</p>
<p>Comment: {comment}</p>
<span>time: {time}</span>
</>
);
};

View File

@@ -0,0 +1,40 @@
import React from 'react'
// eslint-disable-next-line camelcase
export async function unstable_getStaticPaths () {
return {
paths: [
'/blog/post-1',
{ params: { post: 'post-2' } },
]
}
}
// eslint-disable-next-line camelcase
export async function unstable_getStaticProps ({ params }) {
if (params.post === 'post-10') {
await new Promise(resolve => {
setTimeout(() => resolve(), 1000)
})
}
return {
props: {
post: params.post,
time: (await import('perf_hooks')).performance.now()
},
revalidate: 10
}
}
export default ({ post, time }) => {
if (!post) return <p>loading...</p>
return (
<>
<p>Post: {post}</p>
<span>time: {time}</span>
</>
)
}

View File

@@ -0,0 +1,21 @@
import React from 'react';
// eslint-disable-next-line camelcase
export async function unstable_getStaticProps() {
return {
props: {
world: 'world',
time: new Date().getTime(),
},
revalidate: false,
};
}
export default ({ world, time }) => {
return (
<>
<p>hello: {world}</p>
<span>time: {time}</span>
</>
);
};

View File

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

View File

@@ -0,0 +1,5 @@
const Page = ({ data }) => <p>{data} world</p>;
Page.getInitialProps = () => ({ data: 'hello' });
export default Page;

View File

@@ -115,7 +115,7 @@ describe('build meta dev', () => {
{ src: '/nested/page', dest: 'http://localhost:5000/nested/page' },
{ src: '/api/test', dest: 'http://localhost:5000/api/test' },
{
src: '^/(nested\\/([^\\/]+?)(?:\\/)?)$',
src: '^/(nested\\/([^/]+?)(?:\\/)?)$',
dest: 'http://localhost:5000/$1',
},
{ src: '/data.txt', dest: 'http://localhost:5000/data.txt' },

View File

@@ -3,9 +3,13 @@ from http.server import BaseHTTPRequestHandler
import base64
import json
import inspect
from importlib import util
import __NOW_HANDLER_FILENAME
__now_variables = dir(__NOW_HANDLER_FILENAME)
# Import relative path https://stackoverflow.com/a/67692/266535
__now_spec = util.spec_from_file_location("__NOW_HANDLER_MODULE_NAME", "./__NOW_HANDLER_ENTRYPOINT")
__now_module = util.module_from_spec(__now_spec)
__now_spec.loader.exec_module(__now_module)
__now_variables = dir(__now_module)
def format_headers(headers, decode=False):
@@ -21,10 +25,10 @@ def format_headers(headers, decode=False):
if 'handler' in __now_variables or 'Handler' in __now_variables:
base = __NOW_HANDLER_FILENAME.handler if ('handler' in __now_variables) else __NOW_HANDLER_FILENAME.Handler
base = __now_module.handler if ('handler' in __now_variables) else __now_module.Handler
if not issubclass(base, BaseHTTPRequestHandler):
print('Handler must inherit from BaseHTTPRequestHandler')
print('See the docs https://zeit.co/docs/v2/deployments/official-builders/python-now-python')
print('See the docs https://zeit.co/docs/runtimes#advanced-usage/advanced-python-usage')
exit(1)
print('using HTTP Handler')
@@ -74,8 +78,8 @@ if 'handler' in __now_variables or 'Handler' in __now_variables:
elif 'app' in __now_variables:
if (
not inspect.iscoroutinefunction(__NOW_HANDLER_FILENAME.app) and
not inspect.iscoroutinefunction(__NOW_HANDLER_FILENAME.app.__call__)
not inspect.iscoroutinefunction(__now_module.app) and
not inspect.iscoroutinefunction(__now_module.app.__call__)
):
print('using Web Server Gateway Interface (WSGI)')
import sys
@@ -136,7 +140,7 @@ elif 'app' in __now_variables:
if key not in ('HTTP_CONTENT_TYPE', 'HTTP_CONTENT_LENGTH'):
environ[key] = value
response = Response.from_app(__NOW_HANDLER_FILENAME.app, environ)
response = Response.from_app(__now_module.app, environ)
return_dict = {
'statusCode': response.status_code,
@@ -271,10 +275,10 @@ elif 'app' in __now_variables:
}
asgi_cycle = ASGICycle(scope)
response = asgi_cycle(__NOW_HANDLER_FILENAME.app, body)
response = asgi_cycle(__now_module.app, body)
return response
else:
print('Missing variable `handler` or `app` in file __NOW_HANDLER_FILENAME.py')
print('See the docs https://zeit.co/docs/v2/deployments/official-builders/python-now-python')
print('Missing variable `handler` or `app` in file "__NOW_HANDLER_ENTRYPOINT".')
print('See the docs https://zeit.co/docs/runtimes#advanced-usage/advanced-python-usage')
exit(1)

View File

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

View File

@@ -155,13 +155,10 @@ export const build = async ({
// will be used on `from $here import handler`
// for example, `from api.users import handler`
debug('Entrypoint is', entrypoint);
const userHandlerFilePath = entrypoint
.replace(/\//g, '.')
.replace(/\.py$/, '');
const nowHandlerPyContents = originalNowHandlerPyContents.replace(
/__NOW_HANDLER_FILENAME/g,
userHandlerFilePath
);
const moduleName = entrypoint.replace(/\//g, '.').replace(/\.py$/, '');
const nowHandlerPyContents = originalNowHandlerPyContents
.replace(/__NOW_HANDLER_MODULE_NAME/g, moduleName)
.replace(/__NOW_HANDLER_ENTRYPOINT/g, entrypoint);
// in order to allow the user to have `server.py`, we need our `server.py` to be called
// somethig else

View File

@@ -0,0 +1,11 @@
from http.server import BaseHTTPRequestHandler
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
self.wfile.write('hello:RANDOMNESS_PLACEHOLDER'.encode())
return

View File

@@ -0,0 +1,7 @@
{
"version": 2,
"builds": [{ "src": "api/[hello].py", "use": "@now/python" }],
"probes": [
{ "path": "/api/[hello].py", "mustContain": "hello:RANDOMNESS_PLACEHOLDER" }
]
}

View File

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

View File

@@ -105,10 +105,15 @@ export function convertTrailingSlash(enable: boolean, status = 308): Route[] {
const routes: Route[] = [];
if (enable) {
routes.push({
src: '^/(.*[^\\/])$',
src: '^/((?:[^/]+/)*[^/\\.]+)$',
headers: { Location: '/$1/' },
status,
});
routes.push({
src: '^/((?:[^/]+/)*[^/]+\\.\\w+)/$',
headers: { Location: '/$1' },
status,
});
} else {
routes.push({
src: '^/(.*)\\/$',

View File

@@ -503,16 +503,41 @@ test('convertTrailingSlash enabled', () => {
const actual = convertTrailingSlash(true);
const expected = [
{
src: '^/(.*[^\\/])$',
src: '^/((?:[^/]+/)*[^/\\.]+)$',
headers: { Location: '/$1/' },
status: 308,
},
{
src: '^/((?:[^/]+/)*[^/]+\\.\\w+)/$',
headers: { Location: '/$1' },
status: 308,
},
];
deepEqual(actual, expected);
const mustMatch = [['/index.html', '/dir', '/dir/index.html', '/foo/bar']];
const mustMatch = [
['/dir', '/dir/foo', '/dir/foo/bar'],
['/foo.html/', '/dir/foo.html/', '/dir/foo/bar.css/', '/dir/about.map.js/'],
];
const mustNotMatch = [['/', '/dir/', '/dir/foo/', '/next.php?page=/']];
const mustNotMatch = [
[
'/',
'/index.html',
'/asset/style.css',
'/asset/about.map.js',
'/dir/',
'/dir/foo/',
'/next.php?page=/',
],
[
'/',
'/foo.html',
'/dir/foo.html',
'/dir/foo/bar.css',
'/dir/about.map.js',
],
];
assertRegexMatches(actual, mustMatch, mustNotMatch);
});

View File

@@ -1,31 +0,0 @@
{
"overrides": [
{
"files": ["**"],
"rules": {
"array-bracket-spacing": [
"error",
"always"
],
"arrow-body-style": "off",
"arrow-parens": [
"error",
"always"
],
"comma-dangle": "off",
"import/no-extraneous-dependencies": "off",
"no-await-in-loop": "off",
"no-param-reassign": "off",
"no-restricted-syntax": "off",
"no-return-await": "off",
"no-use-before-define": "off",
"prefer-destructuring": "off",
"space-before-function-paren": [
"error",
"always"
]
}
}
]
}

View File

@@ -53,6 +53,19 @@ async function testDeployment(
}
const nowJson = JSON.parse(bodies['now.json']);
if (process.env.NOW_BUILDER_DEBUG) {
if (!nowJson.build) {
nowJson.build = {};
}
if (!nowJson.build.env) {
nowJson.build.env = {};
}
nowJson.build.env = {
NOW_BUILDER_DEBUG: process.env.NOW_BUILDER_DEBUG,
};
}
for (const build of nowJson.builds) {
if (builderUrl) {
if (builderUrl === '@canary') {

View File

@@ -1,45 +1,56 @@
const { join } = require('path');
const { execSync } = require('child_process');
const fetch = require('node-fetch');
process.chdir(join(__dirname, '..'));
const commit = execSync('git log --pretty=format:"%s %H"')
.toString()
.trim()
.split('\n')
.find(line => line.startsWith('Publish Stable '))
.split(' ')
.pop();
async function main() {
const res = await fetch(
'https://api.github.com/repos/zeit/now/releases/latest'
);
const { tag_name } = await res.json();
if (!commit) {
throw new Error('Unable to find last publish commit');
// git log --pretty=format:"- %s [%an]" `git show-ref -s 'now@16.7.3'`...HEAD | grep -v '\- Publish '
if (!tag_name) {
throw new Error('Unable to find last GitHub Release tag.');
}
const log =
execSync(`git log --pretty=format:"- %s [%an]" ${tag_name}...HEAD`)
.toString()
.trim()
.split('\n')
.filter(line => !line.startsWith('- Publish '))
.join('\n') || 'NO CHANGES DETECTED';
console.log(`Changes since the last stable release (${tag_name}):`);
console.log(`\n${log}\n`);
const pkgs =
Array.from(
new Set(
execSync(`git diff --name-only ${tag_name}...HEAD`)
.toString()
.trim()
.split('\n')
.filter(line => line.startsWith('packages/'))
.map(line => line.split('/')[1])
.map(pkgName => {
try {
return require(`../packages/${pkgName}/package.json`).name;
} catch {
// Failed to read package.json (perhaps the pkg was deleted)
}
})
.filter(s => Boolean(s))
)
).join(',') || 'now';
console.log('To publish a stable release, execute the following:');
console.log(
`\nnpx lerna version --message 'Publish Stable' --exact --force-publish=${pkgs}\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`
);
main().catch(console.error);

View File

@@ -83,9 +83,11 @@ async function main() {
await runScript(pkgName, script);
}
execSync(
`rm -rf public && mkdir public && echo '<a href="https://zeit.co/new">https://zeit.co/new</a>' > public/index.html`
);
if (process.env.NOW_GITHUB_DEPLOYMENT) {
execSync(
`rm -rf public && mkdir public && echo '<a href="https://zeit.co/new">https://zeit.co/new</a>' > public/output.html`
);
}
}
function runScript(pkgName, script) {
@@ -99,7 +101,11 @@ function runScript(pkgName, script) {
}
if (pkgJson && pkgJson.scripts && pkgJson.scripts[script]) {
console.log(`\n[${pkgName}] Running yarn ${script}`);
const child = spawn('yarn', [script], { cwd, stdio: 'inherit' });
const child = spawn('yarn', [script], {
cwd,
stdio: 'inherit',
shell: true,
});
child.on('error', reject);
child.on('close', (code, signal) => {
if (code === 0) {