mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-11 12:57:46 +00:00
Compare commits
54 Commits
@now/build
...
@now/pytho
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
372a674625 | ||
|
|
fafeadb7ba | ||
|
|
966b1e763f | ||
|
|
bb60e1a5fe | ||
|
|
cac9f807cc | ||
|
|
a0b1254820 | ||
|
|
0faff4132b | ||
|
|
1793a1287d | ||
|
|
5b572239c1 | ||
|
|
f6a66d937e | ||
|
|
2cf9a2f489 | ||
|
|
454f4dcc61 | ||
|
|
6e1065fde2 | ||
|
|
80ce06b20c | ||
|
|
99f3ab8b64 | ||
|
|
ca4f6d2491 | ||
|
|
2ceb2a78aa | ||
|
|
d97da21afc | ||
|
|
03b5a0c0bf | ||
|
|
f76abe3372 | ||
|
|
cdd43b74ae | ||
|
|
fa633d0e02 | ||
|
|
9b46e60c09 | ||
|
|
58eef7f394 | ||
|
|
e97e0fbb64 | ||
|
|
b82876fd82 | ||
|
|
02ad32ec22 | ||
|
|
433fe35c93 | ||
|
|
90c59d6ae2 | ||
|
|
33672c3d78 | ||
|
|
59ae7a989a | ||
|
|
5767e9e8c2 | ||
|
|
e62b9e8ed9 | ||
|
|
59597ccd17 | ||
|
|
7be49c66ef | ||
|
|
1380e25ef3 | ||
|
|
e825ce746f | ||
|
|
4e58951808 | ||
|
|
fbd805aad7 | ||
|
|
2a2705c6e3 | ||
|
|
986c957183 | ||
|
|
c5d063e876 | ||
|
|
500c36f5d4 | ||
|
|
69dbbeac44 | ||
|
|
69486c3adb | ||
|
|
e6692bb79b | ||
|
|
94fba1d7af | ||
|
|
223d8f4774 | ||
|
|
42e7a7e4e3 | ||
|
|
6716fdd49b | ||
|
|
3b69092fd8 | ||
|
|
aa8eaedbc8 | ||
|
|
f519ed373f | ||
|
|
851dff4b03 |
@@ -115,15 +115,12 @@ jobs:
|
||||
|
||||
test-integration-macos-node-8:
|
||||
macos:
|
||||
xcode: '9.2.0'
|
||||
xcode: '9.0.1'
|
||||
working_directory: ~/repo
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
name: Update Node.js
|
||||
command: curl -sfLS install-node.now.sh/8.11 | sh -s -- --yes
|
||||
- run:
|
||||
name: Output version
|
||||
command: node --version
|
||||
@@ -208,15 +205,12 @@ jobs:
|
||||
|
||||
test-integration-macos-now-dev-node-8:
|
||||
macos:
|
||||
xcode: '9.2.0'
|
||||
xcode: '9.0.1'
|
||||
working_directory: ~/repo
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
name: Update Node.js
|
||||
command: curl -sfLS install-node.now.sh/8.11 | sh -s -- --yes
|
||||
- run:
|
||||
name: Output version
|
||||
command: node --version
|
||||
@@ -332,6 +326,24 @@ jobs:
|
||||
name: Running Integration Tests Once
|
||||
command: yarn test-integration-once --clean false
|
||||
|
||||
test-unit:
|
||||
docker:
|
||||
- image: circleci/node:10
|
||||
working_directory: ~/repo
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
name: Compiling `now dev` HTML error templates
|
||||
command: node packages/now-cli/scripts/compile-templates.js
|
||||
- run:
|
||||
name: Output version
|
||||
command: node --version
|
||||
- run:
|
||||
name: Running Unit Tests
|
||||
command: yarn test-unit --clean false
|
||||
|
||||
coverage:
|
||||
docker:
|
||||
- image: circleci/node:10
|
||||
@@ -466,6 +478,9 @@ workflows:
|
||||
- test-integration-once:
|
||||
requires:
|
||||
- build
|
||||
- test-unit:
|
||||
requires:
|
||||
- build
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
@@ -484,6 +499,7 @@ workflows:
|
||||
- test-integration-linux-now-dev-node-10
|
||||
- test-integration-linux-now-dev-node-12
|
||||
- test-integration-once
|
||||
- test-unit
|
||||
- test-lint
|
||||
filters:
|
||||
tags:
|
||||
|
||||
@@ -58,5 +58,8 @@
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"resolutions": {
|
||||
"signal-exit": "TooTallNate/signal-exit#update/sighub-to-sigint-on-windows"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@now/build-utils",
|
||||
"version": "0.10.1",
|
||||
"version": "0.11.1",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.js",
|
||||
@@ -12,7 +12,8 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "./build.sh",
|
||||
"test-integration-once": "jest --env node --verbose --runInBand",
|
||||
"test-unit": "jest --env node --verbose --runInBand test/unit.test.js",
|
||||
"test-integration-once": "jest --env node --verbose --runInBand test/integration.test.js",
|
||||
"prepublishOnly": "./build.sh"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -173,19 +173,17 @@ export async function detectBuilders(
|
||||
src: 'public/**/*',
|
||||
config,
|
||||
});
|
||||
} else if (builders.length > 0) {
|
||||
// We can't use pattern matching, since `!(api)` and `!(api)/**/*`
|
||||
// won't give the correct results
|
||||
builders.push(
|
||||
...files
|
||||
.filter(name => !name.startsWith('api/'))
|
||||
.filter(name => !(name === 'package.json'))
|
||||
.map(name => ({
|
||||
use: '@now/static',
|
||||
src: name,
|
||||
config,
|
||||
}))
|
||||
);
|
||||
} else if (
|
||||
builders.length > 0 &&
|
||||
files.some(f => !f.startsWith('api/') && f !== 'package.json')
|
||||
) {
|
||||
// Everything besides the api directory
|
||||
// and package.json can be served as static files
|
||||
builders.push({
|
||||
use: '@now/static',
|
||||
src: '!{api/**,package.json}',
|
||||
config,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ function partiallyMatches(pathA: string, pathB: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Counts how often a path occurres when all placeholders
|
||||
// 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 => {
|
||||
@@ -226,7 +226,7 @@ async function detectApiRoutes(files: string[]): Promise<RoutesResult> {
|
||||
error: {
|
||||
code: 'conflicting_path_segment',
|
||||
message:
|
||||
`The segment "${conflictingSegment}" occurres more than ` +
|
||||
`The segment "${conflictingSegment}" occurs more than ` +
|
||||
`one time in your path "${file}". Please make sure that ` +
|
||||
`every segment in a path is unique`
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@ import { File } from './types';
|
||||
|
||||
interface FileBlobOptions {
|
||||
mode?: number;
|
||||
contentType?: string;
|
||||
data: string | Buffer;
|
||||
}
|
||||
|
||||
interface FromStreamOptions {
|
||||
mode?: number;
|
||||
contentType?: string;
|
||||
stream: NodeJS.ReadableStream;
|
||||
}
|
||||
|
||||
@@ -16,16 +18,22 @@ export default class FileBlob implements File {
|
||||
public type: 'FileBlob';
|
||||
public mode: number;
|
||||
public data: string | Buffer;
|
||||
public contentType: string | undefined;
|
||||
|
||||
constructor({ mode = 0o100644, data }: FileBlobOptions) {
|
||||
constructor({ mode = 0o100644, contentType, data }: FileBlobOptions) {
|
||||
assert(typeof mode === 'number');
|
||||
assert(typeof data === 'string' || Buffer.isBuffer(data));
|
||||
this.type = 'FileBlob';
|
||||
this.mode = mode;
|
||||
this.contentType = contentType;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
static async fromStream({ mode = 0o100644, stream }: FromStreamOptions) {
|
||||
static async fromStream({
|
||||
mode = 0o100644,
|
||||
contentType,
|
||||
stream,
|
||||
}: FromStreamOptions) {
|
||||
assert(typeof mode === 'number');
|
||||
assert(typeof stream.pipe === 'function'); // is-stream
|
||||
const chunks: Buffer[] = [];
|
||||
@@ -37,7 +45,7 @@ export default class FileBlob implements File {
|
||||
});
|
||||
|
||||
const data = Buffer.concat(chunks);
|
||||
return new FileBlob({ mode, data });
|
||||
return new FileBlob({ mode, contentType, data });
|
||||
}
|
||||
|
||||
toStream(): NodeJS.ReadableStream {
|
||||
|
||||
@@ -9,11 +9,13 @@ const semaToPreventEMFILE = new Sema(20);
|
||||
|
||||
interface FileFsRefOptions {
|
||||
mode?: number;
|
||||
contentType?: string;
|
||||
fsPath: string;
|
||||
}
|
||||
|
||||
interface FromStreamOptions {
|
||||
mode: number;
|
||||
contentType?: string;
|
||||
stream: NodeJS.ReadableStream;
|
||||
fsPath: string;
|
||||
}
|
||||
@@ -22,17 +24,20 @@ class FileFsRef implements File {
|
||||
public type: 'FileFsRef';
|
||||
public mode: number;
|
||||
public fsPath: string;
|
||||
public contentType: string | undefined;
|
||||
|
||||
constructor({ mode = 0o100644, fsPath }: FileFsRefOptions) {
|
||||
constructor({ mode = 0o100644, contentType, fsPath }: FileFsRefOptions) {
|
||||
assert(typeof mode === 'number');
|
||||
assert(typeof fsPath === 'string');
|
||||
this.type = 'FileFsRef';
|
||||
this.mode = mode;
|
||||
this.contentType = contentType;
|
||||
this.fsPath = fsPath;
|
||||
}
|
||||
|
||||
static async fromFsPath({
|
||||
mode,
|
||||
contentType,
|
||||
fsPath,
|
||||
}: FileFsRefOptions): Promise<FileFsRef> {
|
||||
let m = mode;
|
||||
@@ -40,11 +45,12 @@ class FileFsRef implements File {
|
||||
const stat = await fs.lstat(fsPath);
|
||||
m = stat.mode;
|
||||
}
|
||||
return new FileFsRef({ mode: m, fsPath });
|
||||
return new FileFsRef({ mode: m, contentType, fsPath });
|
||||
}
|
||||
|
||||
static async fromStream({
|
||||
mode = 0o100644,
|
||||
contentType,
|
||||
stream,
|
||||
fsPath,
|
||||
}: FromStreamOptions): Promise<FileFsRef> {
|
||||
@@ -63,7 +69,7 @@ class FileFsRef implements File {
|
||||
dest.on('error', reject);
|
||||
});
|
||||
|
||||
return new FileFsRef({ mode, fsPath });
|
||||
return new FileFsRef({ mode, contentType, fsPath });
|
||||
}
|
||||
|
||||
async toStreamAsync(): Promise<NodeJS.ReadableStream> {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { File } from './types';
|
||||
interface FileRefOptions {
|
||||
mode?: number;
|
||||
digest: string;
|
||||
contentType?: string;
|
||||
mutable?: boolean;
|
||||
}
|
||||
|
||||
@@ -26,14 +27,21 @@ export default class FileRef implements File {
|
||||
public type: 'FileRef';
|
||||
public mode: number;
|
||||
public digest: string;
|
||||
public contentType: string | undefined;
|
||||
private mutable: boolean;
|
||||
|
||||
constructor({ mode = 0o100644, digest, mutable = false }: FileRefOptions) {
|
||||
constructor({
|
||||
mode = 0o100644,
|
||||
digest,
|
||||
contentType,
|
||||
mutable = false,
|
||||
}: FileRefOptions) {
|
||||
assert(typeof mode === 'number');
|
||||
assert(typeof digest === 'string');
|
||||
this.type = 'FileRef';
|
||||
this.mode = mode;
|
||||
this.digest = digest;
|
||||
this.contentType = contentType;
|
||||
this.mutable = mutable;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Lambda } from './lambda';
|
||||
interface PrerenderOptions {
|
||||
expiration: number;
|
||||
lambda: Lambda;
|
||||
fallback: FileBlob | FileFsRef | FileRef;
|
||||
fallback: FileBlob | FileFsRef | FileRef | null;
|
||||
group?: number;
|
||||
}
|
||||
|
||||
@@ -14,22 +14,29 @@ export class Prerender {
|
||||
public type: 'Prerender';
|
||||
public expiration: number;
|
||||
public lambda: Lambda;
|
||||
public fallback: FileBlob | FileFsRef | FileRef;
|
||||
public fallback: FileBlob | FileFsRef | FileRef | null;
|
||||
public group?: number;
|
||||
|
||||
constructor({ expiration, lambda, fallback, group }: PrerenderOptions) {
|
||||
this.type = 'Prerender';
|
||||
this.expiration = expiration;
|
||||
this.lambda = lambda;
|
||||
this.fallback = fallback;
|
||||
|
||||
|
||||
if (
|
||||
typeof group !== 'undefined' &&
|
||||
(group <= 0 || !Number.isInteger(group))
|
||||
) {
|
||||
throw new Error('The `group` argument for `Prerender` needs to be a natural number.');
|
||||
throw new Error(
|
||||
'The `group` argument for `Prerender` needs to be a natural number.'
|
||||
);
|
||||
}
|
||||
|
||||
this.group = group;
|
||||
|
||||
if (typeof fallback === 'undefined') {
|
||||
throw new Error(
|
||||
'The `fallback` argument for `Prerender` needs to be a `FileBlob`, `FileFsRef`, `FileRef`, or null.'
|
||||
);
|
||||
}
|
||||
this.fallback = fallback;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface Env {
|
||||
export interface File {
|
||||
type: string;
|
||||
mode: number;
|
||||
contentType?: string;
|
||||
toStream: () => NodeJS.ReadableStream;
|
||||
/**
|
||||
* The absolute path to the file in the filesystem
|
||||
@@ -190,23 +191,110 @@ export interface ShouldServeOptions {
|
||||
config: Config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Credit to Iain Reid, MIT license.
|
||||
* Source: https://gist.github.com/iainreid820/5c1cc527fe6b5b7dba41fec7fe54bf6e
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace PackageJson {
|
||||
/**
|
||||
* An author or contributor
|
||||
*/
|
||||
export interface Author {
|
||||
name: string;
|
||||
email?: string;
|
||||
homepage?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A map of exposed bin commands
|
||||
*/
|
||||
export interface BinMap {
|
||||
[commandName: string]: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A bugs link
|
||||
*/
|
||||
export interface Bugs {
|
||||
email: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
name?: string;
|
||||
config?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* A map of dependencies
|
||||
*/
|
||||
export interface DependencyMap {
|
||||
[dependencyName: string]: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* CommonJS package structure
|
||||
*/
|
||||
export interface Directories {
|
||||
lib?: string;
|
||||
bin?: string;
|
||||
man?: string;
|
||||
doc?: string;
|
||||
example?: string;
|
||||
}
|
||||
|
||||
export interface Engines {
|
||||
node?: string;
|
||||
npm?: string;
|
||||
}
|
||||
|
||||
export interface PublishConfig {
|
||||
registry?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A project repository
|
||||
*/
|
||||
export interface Repository {
|
||||
type: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface ScriptsMap {
|
||||
[scriptName: string]: string;
|
||||
}
|
||||
}
|
||||
|
||||
export interface PackageJson {
|
||||
name?: string;
|
||||
version?: string;
|
||||
engines?: {
|
||||
[key: string]: string;
|
||||
node: string;
|
||||
npm: string;
|
||||
};
|
||||
scripts?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
dependencies?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
devDependencies?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
readonly name?: string;
|
||||
readonly version?: string;
|
||||
readonly description?: string;
|
||||
readonly keywords?: string[];
|
||||
readonly homepage?: string;
|
||||
readonly bugs?: string | PackageJson.Bugs;
|
||||
readonly license?: string;
|
||||
readonly author?: string | PackageJson.Author;
|
||||
readonly contributors?: string[] | PackageJson.Author[];
|
||||
readonly files?: string[];
|
||||
readonly main?: string;
|
||||
readonly bin?: string | PackageJson.BinMap;
|
||||
readonly man?: string | string[];
|
||||
readonly directories?: PackageJson.Directories;
|
||||
readonly repository?: string | PackageJson.Repository;
|
||||
readonly scripts?: PackageJson.ScriptsMap;
|
||||
readonly config?: PackageJson.Config;
|
||||
readonly dependencies?: PackageJson.DependencyMap;
|
||||
readonly devDependencies?: PackageJson.DependencyMap;
|
||||
readonly peerDependencies?: PackageJson.DependencyMap;
|
||||
readonly optionalDependencies?: PackageJson.DependencyMap;
|
||||
readonly bundledDependencies?: string[];
|
||||
readonly engines?: PackageJson.Engines;
|
||||
readonly os?: string[];
|
||||
readonly cpu?: string[];
|
||||
readonly preferGlobal?: boolean;
|
||||
readonly private?: boolean;
|
||||
readonly publishConfig?: PackageJson.PublishConfig;
|
||||
}
|
||||
|
||||
export interface NodeVersion {
|
||||
|
||||
209
packages/now-build-utils/test/integration.test.js
vendored
Normal file
209
packages/now-build-utils/test/integration.test.js
vendored
Normal file
@@ -0,0 +1,209 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs-extra');
|
||||
const {
|
||||
packAndDeploy,
|
||||
testDeployment,
|
||||
} = require('../../../test/lib/deployment/test-deployment');
|
||||
const { glob, detectBuilders, detectRoutes } = require('../');
|
||||
|
||||
jest.setTimeout(4 * 60 * 1000);
|
||||
|
||||
const builderUrl = '@canary';
|
||||
let buildUtilsUrl;
|
||||
|
||||
beforeAll(async () => {
|
||||
const buildUtilsPath = path.resolve(__dirname, '..');
|
||||
buildUtilsUrl = await packAndDeploy(buildUtilsPath);
|
||||
console.log('buildUtilsUrl', buildUtilsUrl);
|
||||
});
|
||||
|
||||
const fixturesPath = path.resolve(__dirname, 'fixtures');
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const fixture of fs.readdirSync(fixturesPath)) {
|
||||
if (fixture.includes('zero-config')) {
|
||||
// Those have separate tests
|
||||
continue; // eslint-disable-line no-continue
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-loop-func
|
||||
it(`should build ${fixture}`, async () => {
|
||||
await expect(
|
||||
testDeployment(
|
||||
{ builderUrl, buildUtilsUrl },
|
||||
path.join(fixturesPath, fixture)
|
||||
)
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
}
|
||||
|
||||
// few foreign tests
|
||||
|
||||
const buildersToTestWith = ['now-next', 'now-node', 'now-static-build'];
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const builder of buildersToTestWith) {
|
||||
const fixturesPath2 = path.resolve(
|
||||
__dirname,
|
||||
`../../${builder}/test/fixtures`
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const fixture of fs.readdirSync(fixturesPath2)) {
|
||||
// don't run all foreign fixtures, just some
|
||||
if (['01-cowsay', '01-cache-headers', '03-env-vars'].includes(fixture)) {
|
||||
// eslint-disable-next-line no-loop-func
|
||||
it(`should build ${builder}/${fixture}`, async () => {
|
||||
await expect(
|
||||
testDeployment(
|
||||
{ builderUrl, buildUtilsUrl },
|
||||
path.join(fixturesPath2, fixture)
|
||||
)
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it('Test `detectBuilders` and `detectRoutes`', async () => {
|
||||
const fixture = path.join(__dirname, 'fixtures', '01-zero-config-api');
|
||||
const pkg = await fs.readJSON(path.join(fixture, 'package.json'));
|
||||
const fileList = await glob('**', fixture);
|
||||
const files = Object.keys(fileList);
|
||||
|
||||
const probes = [
|
||||
{
|
||||
path: '/api/my-endpoint',
|
||||
mustContain: 'my-endpoint',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/other-endpoint',
|
||||
mustContain: 'other-endpoint',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/team/zeit',
|
||||
mustContain: 'team/zeit',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/user/myself',
|
||||
mustContain: 'user/myself',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/not-okay/',
|
||||
status: 404,
|
||||
},
|
||||
{
|
||||
path: '/api',
|
||||
status: 404,
|
||||
},
|
||||
{
|
||||
path: '/api/',
|
||||
status: 404,
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
mustContain: 'hello from index.txt',
|
||||
},
|
||||
];
|
||||
|
||||
const { builders } = await detectBuilders(files, pkg);
|
||||
const { defaultRoutes } = await detectRoutes(files, builders);
|
||||
|
||||
const nowConfig = { builds: builders, routes: defaultRoutes, probes };
|
||||
await fs.writeFile(
|
||||
path.join(fixture, 'now.json'),
|
||||
JSON.stringify(nowConfig, null, 2)
|
||||
);
|
||||
|
||||
const deployment = await testDeployment(
|
||||
{ builderUrl, buildUtilsUrl },
|
||||
fixture
|
||||
);
|
||||
expect(deployment).toBeDefined();
|
||||
});
|
||||
|
||||
it('Test `detectBuilders` and `detectRoutes` 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);
|
||||
const files = Object.keys(fileList);
|
||||
|
||||
const probes = [
|
||||
{
|
||||
path: '/api/not-okay',
|
||||
status: 404,
|
||||
},
|
||||
{
|
||||
path: '/api',
|
||||
mustContain: 'hello from api/index.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/',
|
||||
mustContain: 'hello from api/index.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/index',
|
||||
mustContain: 'hello from api/index.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/index.js',
|
||||
mustContain: 'hello from api/index.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/date.js',
|
||||
mustContain: 'hello from api/date.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
// Someone might expect this to be `date.js`,
|
||||
// but I doubt that there is any case were both
|
||||
// `date/index.js` and `date.js` exists,
|
||||
// so it is not special cased
|
||||
path: '/api/date',
|
||||
mustContain: 'hello from api/date/index.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/date/',
|
||||
mustContain: 'hello from api/date/index.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/date/index',
|
||||
mustContain: 'hello from api/date/index.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/date/index.js',
|
||||
mustContain: 'hello from api/date/index.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
mustContain: 'hello from index.txt',
|
||||
},
|
||||
];
|
||||
|
||||
const { builders } = await detectBuilders(files, pkg);
|
||||
const { defaultRoutes } = await detectRoutes(files, builders);
|
||||
|
||||
const nowConfig = { builds: builders, routes: defaultRoutes, probes };
|
||||
await fs.writeFile(
|
||||
path.join(fixture, 'now.json'),
|
||||
JSON.stringify(nowConfig, null, 2)
|
||||
);
|
||||
|
||||
const deployment = await testDeployment(
|
||||
{ builderUrl, buildUtilsUrl },
|
||||
fixture
|
||||
);
|
||||
expect(deployment).toBeDefined();
|
||||
});
|
||||
@@ -8,23 +8,6 @@ const {
|
||||
getSupportedNodeVersion,
|
||||
defaultSelection,
|
||||
} = require('../dist/fs/node-version');
|
||||
const {
|
||||
packAndDeploy,
|
||||
testDeployment,
|
||||
} = require('../../../test/lib/deployment/test-deployment');
|
||||
|
||||
jest.setTimeout(4 * 60 * 1000);
|
||||
|
||||
const builderUrl = '@canary';
|
||||
let buildUtilsUrl;
|
||||
|
||||
beforeAll(async () => {
|
||||
const buildUtilsPath = path.resolve(__dirname, '..');
|
||||
buildUtilsUrl = await packAndDeploy(buildUtilsPath);
|
||||
console.log('buildUtilsUrl', buildUtilsUrl);
|
||||
});
|
||||
|
||||
// unit tests
|
||||
|
||||
it('should re-create symlinks properly', async () => {
|
||||
const files = await glob('**', path.join(__dirname, 'symlinks'));
|
||||
@@ -142,56 +125,6 @@ it('should support require by path for legacy builders', () => {
|
||||
expect(Lambda2).toBe(index.Lambda);
|
||||
});
|
||||
|
||||
// own fixtures
|
||||
|
||||
const fixturesPath = path.resolve(__dirname, 'fixtures');
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const fixture of fs.readdirSync(fixturesPath)) {
|
||||
if (fixture.includes('zero-config')) {
|
||||
// Those have separate tests
|
||||
continue; // eslint-disable-line no-continue
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-loop-func
|
||||
it(`should build ${fixture}`, async () => {
|
||||
await expect(
|
||||
testDeployment(
|
||||
{ builderUrl, buildUtilsUrl },
|
||||
path.join(fixturesPath, fixture)
|
||||
)
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
}
|
||||
|
||||
// few foreign tests
|
||||
|
||||
const buildersToTestWith = ['now-next', 'now-node', 'now-static-build'];
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const builder of buildersToTestWith) {
|
||||
const fixturesPath2 = path.resolve(
|
||||
__dirname,
|
||||
`../../${builder}/test/fixtures`
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const fixture of fs.readdirSync(fixturesPath2)) {
|
||||
// don't run all foreign fixtures, just some
|
||||
if (['01-cowsay', '01-cache-headers', '03-env-vars'].includes(fixture)) {
|
||||
// eslint-disable-next-line no-loop-func
|
||||
it(`should build ${builder}/${fixture}`, async () => {
|
||||
await expect(
|
||||
testDeployment(
|
||||
{ builderUrl, buildUtilsUrl },
|
||||
path.join(fixturesPath2, fixture)
|
||||
)
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it('Test `detectBuilders`', async () => {
|
||||
{
|
||||
// package.json + no build
|
||||
@@ -258,7 +191,7 @@ it('Test `detectBuilders`', async () => {
|
||||
expect(builders[0].use).toBe('@now/node');
|
||||
expect(builders[0].src).toBe('api/users.js');
|
||||
expect(builders[1].use).toBe('@now/static');
|
||||
expect(builders[1].src).toBe('index.html');
|
||||
expect(builders[1].src).toBe('!{api/**,package.json}');
|
||||
expect(builders.length).toBe(2);
|
||||
expect(errors).toBe(null);
|
||||
}
|
||||
@@ -270,10 +203,8 @@ it('Test `detectBuilders`', async () => {
|
||||
expect(builders[0].use).toBe('@now/node');
|
||||
expect(builders[0].src).toBe('api/[endpoint].js');
|
||||
expect(builders[1].use).toBe('@now/static');
|
||||
expect(builders[1].src).toBe('index.html');
|
||||
expect(builders[2].use).toBe('@now/static');
|
||||
expect(builders[2].src).toBe('static/image.png');
|
||||
expect(builders.length).toBe(3);
|
||||
expect(builders[1].src).toBe('!{api/**,package.json}');
|
||||
expect(builders.length).toBe(2);
|
||||
expect(errors).toBe(null);
|
||||
}
|
||||
|
||||
@@ -331,10 +262,8 @@ it('Test `detectBuilders`', async () => {
|
||||
expect(builders[0].use).toBe('@now/node');
|
||||
expect(builders[0].src).toBe('api/endpoint.js');
|
||||
expect(builders[1].use).toBe('@now/static');
|
||||
expect(builders[1].src).toBe('favicon.ico');
|
||||
expect(builders[2].use).toBe('@now/static');
|
||||
expect(builders[2].src).toBe('index.html');
|
||||
expect(builders.length).toBe(3);
|
||||
expect(builders[1].src).toBe('!{api/**,package.json}');
|
||||
expect(builders.length).toBe(2);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -502,6 +431,19 @@ it('Test `detectBuilders`', async () => {
|
||||
expect(builders[0].use).toBe('@now/node');
|
||||
expect(builders[1].use).toBe('@now/next');
|
||||
}
|
||||
|
||||
{
|
||||
// many static files + one api file
|
||||
const files = Array.from({ length: 5000 }).map((_, i) => `file${i}.html`);
|
||||
files.push('api/index.ts');
|
||||
const { builders } = await detectBuilders(files);
|
||||
|
||||
expect(builders.length).toBe(2);
|
||||
expect(builders[0].use).toBe('@now/node');
|
||||
expect(builders[0].src).toBe('api/index.ts');
|
||||
expect(builders[1].use).toBe('@now/static');
|
||||
expect(builders[1].src).toBe('!{api/**,package.json}');
|
||||
}
|
||||
});
|
||||
|
||||
it('Test `detectRoutes`', async () => {
|
||||
@@ -647,146 +589,3 @@ it('Test `detectRoutes`', async () => {
|
||||
expect(defaultRoutes.length).toBe(5);
|
||||
}
|
||||
});
|
||||
|
||||
it('Test `detectBuilders` and `detectRoutes`', async () => {
|
||||
const fixture = path.join(__dirname, 'fixtures', '01-zero-config-api');
|
||||
const pkg = await fs.readJSON(path.join(fixture, 'package.json'));
|
||||
const fileList = await glob('**', fixture);
|
||||
const files = Object.keys(fileList);
|
||||
|
||||
const probes = [
|
||||
{
|
||||
path: '/api/my-endpoint',
|
||||
mustContain: 'my-endpoint',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/other-endpoint',
|
||||
mustContain: 'other-endpoint',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/team/zeit',
|
||||
mustContain: 'team/zeit',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/user/myself',
|
||||
mustContain: 'user/myself',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/not-okay/',
|
||||
status: 404,
|
||||
},
|
||||
{
|
||||
path: '/api',
|
||||
status: 404,
|
||||
},
|
||||
{
|
||||
path: '/api/',
|
||||
status: 404,
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
mustContain: 'hello from index.txt',
|
||||
},
|
||||
];
|
||||
|
||||
const { builders } = await detectBuilders(files, pkg);
|
||||
const { defaultRoutes } = await detectRoutes(files, builders);
|
||||
|
||||
const nowConfig = { builds: builders, routes: defaultRoutes, probes };
|
||||
await fs.writeFile(
|
||||
path.join(fixture, 'now.json'),
|
||||
JSON.stringify(nowConfig, null, 2)
|
||||
);
|
||||
|
||||
const deployment = await testDeployment(
|
||||
{ builderUrl, buildUtilsUrl },
|
||||
fixture
|
||||
);
|
||||
expect(deployment).toBeDefined();
|
||||
});
|
||||
|
||||
it('Test `detectBuilders` and `detectRoutes` 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);
|
||||
const files = Object.keys(fileList);
|
||||
|
||||
const probes = [
|
||||
{
|
||||
path: '/api/not-okay',
|
||||
status: 404,
|
||||
},
|
||||
{
|
||||
path: '/api',
|
||||
mustContain: 'hello from api/index.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/',
|
||||
mustContain: 'hello from api/index.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/index',
|
||||
mustContain: 'hello from api/index.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/index.js',
|
||||
mustContain: 'hello from api/index.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/date.js',
|
||||
mustContain: 'hello from api/date.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
// Someone might expect this to be `date.js`,
|
||||
// but I doubt that there is any case were both
|
||||
// `date/index.js` and `date.js` exists,
|
||||
// so it is not special cased
|
||||
path: '/api/date',
|
||||
mustContain: 'hello from api/date/index.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/date/',
|
||||
mustContain: 'hello from api/date/index.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/date/index',
|
||||
mustContain: 'hello from api/date/index.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/api/date/index.js',
|
||||
mustContain: 'hello from api/date/index.js',
|
||||
status: 200,
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
mustContain: 'hello from index.txt',
|
||||
},
|
||||
];
|
||||
|
||||
const { builders } = await detectBuilders(files, pkg);
|
||||
const { defaultRoutes } = await detectRoutes(files, builders);
|
||||
|
||||
const nowConfig = { builds: builders, routes: defaultRoutes, probes };
|
||||
await fs.writeFile(
|
||||
path.join(fixture, 'now.json'),
|
||||
JSON.stringify(nowConfig, null, 2)
|
||||
);
|
||||
|
||||
const deployment = await testDeployment(
|
||||
{ builderUrl, buildUtilsUrl },
|
||||
fixture
|
||||
);
|
||||
expect(deployment).toBeDefined();
|
||||
});
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
[](https://spectrum.chat/zeit)
|
||||
|
||||
## Usage
|
||||
## Usages
|
||||
|
||||
To install the latest version of Now CLI, visit [zeit.co/download](https://zeit.co/download) or run this command:
|
||||
|
||||
```
|
||||
```bash
|
||||
npm i -g now
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "now",
|
||||
"version": "16.3.0",
|
||||
"version": "16.4.1",
|
||||
"preferGlobal": true,
|
||||
"license": "Apache-2.0",
|
||||
"description": "The command-line interface for Now",
|
||||
@@ -58,7 +58,7 @@
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.11"
|
||||
"node": ">= 8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sentry/node": "5.5.0",
|
||||
@@ -168,6 +168,7 @@
|
||||
"through2": "2.0.3",
|
||||
"title": "3.4.1",
|
||||
"tmp-promise": "1.0.3",
|
||||
"tree-kill": "1.2.1",
|
||||
"ts-node": "8.3.0",
|
||||
"typescript": "3.2.4",
|
||||
"universal-analytics": "0.4.20",
|
||||
|
||||
@@ -127,6 +127,7 @@ export const legacyArgsMri = {
|
||||
'session-affinity',
|
||||
'regions',
|
||||
'dotenv',
|
||||
'target',
|
||||
],
|
||||
boolean: [
|
||||
'help',
|
||||
@@ -144,6 +145,7 @@ export const legacyArgsMri = {
|
||||
'no-scale',
|
||||
'no-verify',
|
||||
'dotenv',
|
||||
'prod',
|
||||
],
|
||||
default: {
|
||||
C: false,
|
||||
|
||||
@@ -388,7 +388,7 @@ export default async function main(
|
||||
|
||||
const deploymentResponse = handleCertError(
|
||||
output,
|
||||
await getDeploymentByIdOrHost(now, contextName, deployment.id, 'v9')
|
||||
await getDeploymentByIdOrHost(now, contextName, deployment.id, 'v10')
|
||||
);
|
||||
|
||||
if (deploymentResponse === 1) {
|
||||
|
||||
@@ -304,6 +304,18 @@ export default async function main(
|
||||
`You are using an old version of the Now Platform. More: ${link(infoUrl)}`
|
||||
);
|
||||
|
||||
if (argv.prod || argv.target) {
|
||||
error(
|
||||
`The option ${cmd(
|
||||
argv.prod ? '--prod' : '--target'
|
||||
)} is not supported for Now 1.0 deployments. To manually alias a deployment, use ${cmd(
|
||||
'now alias'
|
||||
)} instead.`
|
||||
);
|
||||
await exit(1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const {
|
||||
authConfig: { token },
|
||||
config,
|
||||
|
||||
@@ -60,12 +60,6 @@ export default async function main(ctx: NowContext) {
|
||||
args = getSubcommand(argv._.slice(1), COMMAND_CONFIG).args;
|
||||
output = createOutput({ debug });
|
||||
|
||||
// Builders won't show debug logs by default
|
||||
// the `NOW_BUILDER_DEBUG` env variable will enable them
|
||||
if (debug) {
|
||||
process.env.NOW_BUILDER_DEBUG = '1';
|
||||
}
|
||||
|
||||
if ('--port' in argv) {
|
||||
output.warn('`--port` is deprecated, please use `--listen` instead');
|
||||
argv['--listen'] = String(argv['--port']);
|
||||
|
||||
@@ -10,7 +10,7 @@ import Client from '../util/client.ts';
|
||||
import logo from '../util/output/logo';
|
||||
import getScope from '../util/get-scope';
|
||||
|
||||
const e = encodeURIComponent
|
||||
const e = encodeURIComponent;
|
||||
|
||||
const help = () => {
|
||||
console.log(`
|
||||
@@ -48,8 +48,8 @@ const main = async ctx => {
|
||||
argv = mri(ctx.argv.slice(2), {
|
||||
boolean: ['help'],
|
||||
alias: {
|
||||
help: 'h'
|
||||
}
|
||||
help: 'h',
|
||||
},
|
||||
});
|
||||
|
||||
argv._ = argv._.slice(1);
|
||||
@@ -63,7 +63,10 @@ const main = async ctx => {
|
||||
await exit(0);
|
||||
}
|
||||
|
||||
const { authConfig: { token }, config: { currentTeam }} = ctx;
|
||||
const {
|
||||
authConfig: { token },
|
||||
config: { currentTeam },
|
||||
} = ctx;
|
||||
const client = new Client({ apiUrl, token, currentTeam, debug });
|
||||
|
||||
const { contextName } = await getScope(client);
|
||||
@@ -93,17 +96,21 @@ async function run({ client, contextName }) {
|
||||
if (args.length !== 0) {
|
||||
console.error(
|
||||
error(
|
||||
`Invalid number of arguments. Usage: ${chalk.cyan('`now projects ls`')}`
|
||||
`Invalid number of arguments. Usage: ${chalk.cyan(
|
||||
'`now projects ls`'
|
||||
)}`
|
||||
)
|
||||
);
|
||||
return exit(1);
|
||||
}
|
||||
|
||||
const list = await client.fetch('/projects/list', {method: 'GET'});
|
||||
const list = await client.fetch('/v2/projects/', { method: 'GET' });
|
||||
const elapsed = ms(new Date() - start);
|
||||
|
||||
console.log(
|
||||
`> ${plural('project', list.length, true)} found under ${chalk.bold(contextName)} ${chalk.gray(`[${elapsed}]`)}`
|
||||
`> ${plural('project', list.length, true)} found under ${chalk.bold(
|
||||
contextName
|
||||
)} ${chalk.gray(`[${elapsed}]`)}`
|
||||
);
|
||||
|
||||
if (list.length > 0) {
|
||||
@@ -114,19 +121,19 @@ async function run({ client, contextName }) {
|
||||
header.concat(
|
||||
list.map(secret => [
|
||||
'',
|
||||
chalk.bold(secret.name),
|
||||
chalk.gray(`${ms(cur - new Date(secret.updatedAt)) } ago`)
|
||||
])
|
||||
chalk.bold(secret.name),
|
||||
chalk.gray(`${ms(cur - new Date(secret.updatedAt))} ago`),
|
||||
])
|
||||
),
|
||||
{
|
||||
align: ['l', 'l', 'l'],
|
||||
hsep: ' '.repeat(2),
|
||||
stringLength: strlen
|
||||
stringLength: strlen,
|
||||
}
|
||||
);
|
||||
|
||||
if (out) {
|
||||
console.log(`\n${ out }\n`);
|
||||
console.log(`\n${out}\n`);
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -148,11 +155,11 @@ async function run({ client, contextName }) {
|
||||
|
||||
// Check the existence of the project
|
||||
try {
|
||||
await client.fetch(`/projects/info/${e(name)}`)
|
||||
} catch(err) {
|
||||
await client.fetch(`/projects/info/${e(name)}`);
|
||||
} catch (err) {
|
||||
if (err.status === 404) {
|
||||
console.error(error('No such project exists'))
|
||||
return exit(1)
|
||||
console.error(error('No such project exists'));
|
||||
return exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +169,9 @@ async function run({ client, contextName }) {
|
||||
return exit(0);
|
||||
}
|
||||
|
||||
await client.fetch('/projects/remove', {method: 'DELETE', body: {name}});
|
||||
await client.fetch(`/v2/projects/${name}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
const elapsed = ms(new Date() - start);
|
||||
console.log(
|
||||
`${chalk.cyan('> Success!')} Project ${chalk.bold(
|
||||
@@ -193,7 +202,10 @@ async function run({ client, contextName }) {
|
||||
}
|
||||
|
||||
const [name] = args;
|
||||
await client.fetch('/projects/ensure-project', {method: 'POST', body: {name}});
|
||||
await client.fetch('/projects/ensure-project', {
|
||||
method: 'POST',
|
||||
body: { name },
|
||||
});
|
||||
const elapsed = ms(new Date() - start);
|
||||
|
||||
console.log(
|
||||
@@ -204,9 +216,7 @@ async function run({ client, contextName }) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(
|
||||
error('Please specify a valid subcommand: ls | add | rm')
|
||||
);
|
||||
console.error(error('Please specify a valid subcommand: ls | add | rm'));
|
||||
help();
|
||||
exit(1);
|
||||
}
|
||||
@@ -220,7 +230,7 @@ function readConfirmation(projectName) {
|
||||
return new Promise(resolve => {
|
||||
process.stdout.write(
|
||||
`The project: ${chalk.bold(projectName)} will be removed permanently.\n` +
|
||||
`It will also delete everything under the project including deployments.\n`
|
||||
`It will also delete everything under the project including deployments.\n`
|
||||
);
|
||||
|
||||
process.stdout.write(
|
||||
|
||||
@@ -195,28 +195,31 @@ export type DNSRecord = {
|
||||
};
|
||||
|
||||
type SRVRecordData = {
|
||||
name: string,
|
||||
type: 'SRV',
|
||||
name: string;
|
||||
type: 'SRV';
|
||||
srv: {
|
||||
port: number,
|
||||
priority: number,
|
||||
target: string,
|
||||
weight: number,
|
||||
}
|
||||
}
|
||||
|
||||
type MXRecordData = {
|
||||
name: string,
|
||||
type: 'MX',
|
||||
value: string,
|
||||
mxPriority: number,
|
||||
port: number;
|
||||
priority: number;
|
||||
target: string;
|
||||
weight: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type DNSRecordData = {
|
||||
name: string,
|
||||
type: string,
|
||||
value: string,
|
||||
} | SRVRecordData | MXRecordData;
|
||||
type MXRecordData = {
|
||||
name: string;
|
||||
type: 'MX';
|
||||
value: string;
|
||||
mxPriority: number;
|
||||
};
|
||||
|
||||
export type DNSRecordData =
|
||||
| {
|
||||
name: string;
|
||||
type: string;
|
||||
value: string;
|
||||
}
|
||||
| SRVRecordData
|
||||
| MXRecordData;
|
||||
|
||||
export interface Project {
|
||||
id: string;
|
||||
|
||||
@@ -4,11 +4,11 @@ import { Deployment } from '../../types';
|
||||
import {
|
||||
DeploymentNotFound,
|
||||
DeploymentPermissionDenied,
|
||||
InvalidDeploymentId
|
||||
InvalidDeploymentId,
|
||||
} from '../errors-ts';
|
||||
import mapCertError from '../certs/map-cert-error';
|
||||
|
||||
type APIVersion = 'v5' | 'v9';
|
||||
type APIVersion = 'v5' | 'v10';
|
||||
|
||||
export default async function getDeploymentByIdOrHost(
|
||||
client: Client,
|
||||
|
||||
@@ -5,13 +5,17 @@ import pluralize from 'pluralize';
|
||||
import {
|
||||
createDeployment,
|
||||
createLegacyDeployment,
|
||||
DeploymentOptions,
|
||||
} from '../../../../now-client';
|
||||
import wait from '../output/wait';
|
||||
import createOutput from '../output';
|
||||
import { Output } from '../output';
|
||||
// @ts-ignore
|
||||
import Now from '../../util';
|
||||
import { NowConfig } from '../dev/types';
|
||||
|
||||
export default async function processDeployment({
|
||||
now,
|
||||
debug,
|
||||
output,
|
||||
hashes,
|
||||
paths,
|
||||
requestBody,
|
||||
@@ -20,18 +24,35 @@ export default async function processDeployment({
|
||||
legacy,
|
||||
env,
|
||||
quiet,
|
||||
}: any) {
|
||||
const { warn, log } = createOutput({ debug });
|
||||
nowConfig,
|
||||
}: {
|
||||
now: Now;
|
||||
output: Output;
|
||||
hashes: { [key: string]: any };
|
||||
paths: string[];
|
||||
requestBody: DeploymentOptions;
|
||||
uploadStamp: () => number;
|
||||
deployStamp: () => number;
|
||||
legacy: boolean;
|
||||
env: any;
|
||||
quiet: boolean;
|
||||
nowConfig?: NowConfig;
|
||||
}) {
|
||||
const { warn, log, debug, note } = output;
|
||||
let bar: Progress | null = null;
|
||||
|
||||
const path0 = paths[0];
|
||||
const opts: DeploymentOptions = {
|
||||
...requestBody,
|
||||
debug: now._debug,
|
||||
apiUrl: now._apiUrl,
|
||||
};
|
||||
|
||||
if (!legacy) {
|
||||
let buildSpinner = null;
|
||||
let deploySpinner = null;
|
||||
|
||||
for await (const event of createDeployment(paths[0], {
|
||||
...requestBody,
|
||||
debug: now._debug,
|
||||
})) {
|
||||
for await (const event of createDeployment(path0, opts, nowConfig)) {
|
||||
if (event.type === 'hashes-calculated') {
|
||||
hashes = event.payload;
|
||||
}
|
||||
@@ -40,6 +61,10 @@ export default async function processDeployment({
|
||||
warn(event.payload);
|
||||
}
|
||||
|
||||
if (event.type === 'notice') {
|
||||
note(event.payload);
|
||||
}
|
||||
|
||||
if (event.type === 'file_count') {
|
||||
debug(
|
||||
`Total files ${event.payload.total.size}, ${event.payload.missing.length} changed`
|
||||
@@ -119,7 +144,7 @@ export default async function processDeployment({
|
||||
}
|
||||
|
||||
// Handle ready event
|
||||
if (event.type === 'ready') {
|
||||
if (event.type === 'alias-assigned') {
|
||||
if (deploySpinner) {
|
||||
deploySpinner();
|
||||
}
|
||||
@@ -128,10 +153,7 @@ export default async function processDeployment({
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for await (const event of createLegacyDeployment(paths[0], {
|
||||
...requestBody,
|
||||
debug: now._debug,
|
||||
})) {
|
||||
for await (const event of createLegacyDeployment(path0, opts, nowConfig)) {
|
||||
if (event.type === 'hashes-calculated') {
|
||||
hashes = event.payload;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import ms from 'ms';
|
||||
import bytes from 'bytes';
|
||||
import { promisify } from 'util';
|
||||
import { delimiter, dirname, join } from 'path';
|
||||
import { fork, ChildProcess } from 'child_process';
|
||||
import { createFunction } from '@zeit/fun';
|
||||
@@ -10,8 +11,8 @@ import stripAnsi from 'strip-ansi';
|
||||
import chalk from 'chalk';
|
||||
import which from 'which';
|
||||
import plural from 'pluralize';
|
||||
import ora, { Ora } from 'ora';
|
||||
import minimatch from 'minimatch';
|
||||
import _treeKill from 'tree-kill';
|
||||
|
||||
import { Output } from '../output';
|
||||
import highlight from '../output/highlight';
|
||||
@@ -40,7 +41,7 @@ interface BuildMessageResult extends BuildMessage {
|
||||
error?: object;
|
||||
}
|
||||
|
||||
const isLogging = new WeakSet<ChildProcess>();
|
||||
const treeKill = promisify(_treeKill);
|
||||
|
||||
let nodeBinPromise: Promise<string>;
|
||||
|
||||
@@ -48,20 +49,13 @@ async function getNodeBin(): Promise<string> {
|
||||
return which.sync('node', { nothrow: true }) || process.execPath;
|
||||
}
|
||||
|
||||
function pipeChildLogging(child: ChildProcess): void {
|
||||
if (!isLogging.has(child)) {
|
||||
child.stdout!.pipe(process.stdout);
|
||||
child.stderr!.pipe(process.stderr);
|
||||
isLogging.add(child);
|
||||
}
|
||||
}
|
||||
|
||||
async function createBuildProcess(
|
||||
match: BuildMatch,
|
||||
buildEnv: EnvConfig,
|
||||
workPath: string,
|
||||
output: Output,
|
||||
yarnPath?: string
|
||||
yarnPath?: string,
|
||||
debugEnabled: boolean = false
|
||||
): Promise<ChildProcess> {
|
||||
if (!nodeBinPromise) {
|
||||
nodeBinPromise = getNodeBin();
|
||||
@@ -70,21 +64,33 @@ async function createBuildProcess(
|
||||
nodeBinPromise,
|
||||
builderModulePathPromise,
|
||||
]);
|
||||
|
||||
// Ensure that `node` is in the builder's `PATH`
|
||||
let PATH = `${dirname(execPath)}${delimiter}${process.env.PATH}`;
|
||||
|
||||
// Ensure that `yarn` is in the builder's `PATH`
|
||||
if (yarnPath) {
|
||||
PATH = `${yarnPath}${delimiter}${PATH}`;
|
||||
}
|
||||
|
||||
const env: EnvConfig = {
|
||||
...process.env,
|
||||
PATH,
|
||||
...buildEnv,
|
||||
NOW_REGION: 'dev1',
|
||||
};
|
||||
|
||||
// Builders won't show debug logs by default.
|
||||
// The `NOW_BUILDER_DEBUG` env variable enables them.
|
||||
if (debugEnabled) {
|
||||
env.NOW_BUILDER_DEBUG = '1';
|
||||
}
|
||||
|
||||
const buildProcess = fork(modulePath, [], {
|
||||
cwd: workPath,
|
||||
env: {
|
||||
...process.env,
|
||||
PATH,
|
||||
...buildEnv,
|
||||
NOW_REGION: 'dev1',
|
||||
},
|
||||
env,
|
||||
execPath,
|
||||
execArgv: [],
|
||||
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
||||
});
|
||||
match.buildProcess = buildProcess;
|
||||
|
||||
@@ -95,9 +101,6 @@ async function createBuildProcess(
|
||||
match.buildProcess = undefined;
|
||||
});
|
||||
|
||||
buildProcess.stdout!.setEncoding('utf8');
|
||||
buildProcess.stderr!.setEncoding('utf8');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// The first message that the builder process sends is the `ready` event
|
||||
buildProcess.once('message', ({ type }) => {
|
||||
@@ -149,7 +152,8 @@ export async function executeBuild(
|
||||
buildEnv,
|
||||
workPath,
|
||||
devServer.output,
|
||||
yarnPath
|
||||
yarnPath,
|
||||
debug
|
||||
);
|
||||
}
|
||||
|
||||
@@ -170,85 +174,39 @@ export async function executeBuild(
|
||||
|
||||
let buildResultOrOutputs: BuilderOutputs | BuildResult;
|
||||
if (buildProcess) {
|
||||
let spinLogger;
|
||||
let spinner: Ora | undefined;
|
||||
const fullLogs: string[] = [];
|
||||
buildProcess.send({
|
||||
type: 'build',
|
||||
builderName: pkg.name,
|
||||
buildParams,
|
||||
});
|
||||
|
||||
if (isInitialBuild && !debug && process.stdout.isTTY) {
|
||||
const logTitle = `${chalk.bold(
|
||||
`Preparing ${chalk.underline(entrypoint)} for build`
|
||||
)}:`;
|
||||
spinner = ora(logTitle).start();
|
||||
|
||||
spinLogger = (data: Buffer) => {
|
||||
const rawLog = stripAnsi(data.toString());
|
||||
fullLogs.push(rawLog);
|
||||
|
||||
const lines = rawLog.replace(/\s+$/, '').split('\n');
|
||||
const spinText = `${logTitle} ${lines[lines.length - 1]}`;
|
||||
const maxCols = process.stdout.columns || 80;
|
||||
const overflow = stripAnsi(spinText).length + 2 - maxCols;
|
||||
spinner!.text =
|
||||
overflow > 0 ? `${spinText.slice(0, -overflow - 3)}...` : spinText;
|
||||
};
|
||||
|
||||
buildProcess!.stdout!.on('data', spinLogger);
|
||||
buildProcess!.stderr!.on('data', spinLogger);
|
||||
} else {
|
||||
pipeChildLogging(buildProcess!);
|
||||
}
|
||||
|
||||
try {
|
||||
buildProcess.send({
|
||||
type: 'build',
|
||||
builderName: pkg.name,
|
||||
buildParams,
|
||||
});
|
||||
|
||||
buildResultOrOutputs = await new Promise((resolve, reject) => {
|
||||
function onMessage({ type, result, error }: BuildMessageResult) {
|
||||
cleanup();
|
||||
if (type === 'buildResult') {
|
||||
if (result) {
|
||||
resolve(result);
|
||||
} else if (error) {
|
||||
reject(Object.assign(new Error(), error));
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`Got unexpected message type: ${type}`));
|
||||
buildResultOrOutputs = await new Promise((resolve, reject) => {
|
||||
function onMessage({ type, result, error }: BuildMessageResult) {
|
||||
cleanup();
|
||||
if (type === 'buildResult') {
|
||||
if (result) {
|
||||
resolve(result);
|
||||
} else if (error) {
|
||||
reject(Object.assign(new Error(), error));
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`Got unexpected message type: ${type}`));
|
||||
}
|
||||
function onExit(code: number | null, signal: string | null) {
|
||||
cleanup();
|
||||
const err = new Error(
|
||||
`Builder exited with ${signal || code} before sending build result`
|
||||
);
|
||||
reject(err);
|
||||
}
|
||||
function cleanup() {
|
||||
buildProcess!.removeListener('exit', onExit);
|
||||
buildProcess!.removeListener('message', onMessage);
|
||||
}
|
||||
buildProcess!.on('exit', onExit);
|
||||
buildProcess!.on('message', onMessage);
|
||||
});
|
||||
} catch (err) {
|
||||
if (spinner) {
|
||||
spinner.stop();
|
||||
spinner = undefined;
|
||||
console.log(fullLogs.join(''));
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
if (spinLogger) {
|
||||
buildProcess.stdout!.removeListener('data', spinLogger);
|
||||
buildProcess.stderr!.removeListener('data', spinLogger);
|
||||
function onExit(code: number | null, signal: string | null) {
|
||||
cleanup();
|
||||
const err = new Error(
|
||||
`Builder exited with ${signal || code} before sending build result`
|
||||
);
|
||||
reject(err);
|
||||
}
|
||||
if (spinner) {
|
||||
spinner.stop();
|
||||
function cleanup() {
|
||||
buildProcess!.removeListener('exit', onExit);
|
||||
buildProcess!.removeListener('message', onMessage);
|
||||
}
|
||||
pipeChildLogging(buildProcess!);
|
||||
}
|
||||
buildProcess!.on('exit', onExit);
|
||||
buildProcess!.on('message', onMessage);
|
||||
});
|
||||
} else {
|
||||
buildResultOrOutputs = await builder.build(buildParams);
|
||||
}
|
||||
@@ -445,3 +403,35 @@ export async function getBuildMatches(
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
export async function shutdownBuilder(
|
||||
match: BuildMatch,
|
||||
{ debug }: Output
|
||||
): Promise<void> {
|
||||
const ops: Promise<void>[] = [];
|
||||
|
||||
if (match.buildProcess) {
|
||||
const { pid } = match.buildProcess;
|
||||
debug(`Killing builder sub-process with PID ${pid}`);
|
||||
const killPromise = treeKill(pid)
|
||||
.then(() => {
|
||||
debug(`Killed builder with PID ${pid}`);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
debug(`Failed to kill builder with PID ${pid}: ${err}`);
|
||||
});
|
||||
ops.push(killPromise);
|
||||
delete match.buildProcess;
|
||||
}
|
||||
|
||||
if (match.buildOutput) {
|
||||
for (const asset of Object.values(match.buildOutput)) {
|
||||
if (asset.type === 'Lambda' && asset.fn) {
|
||||
debug(`Shutting down Lambda function`);
|
||||
ops.push(asset.fn.destroy());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(ops);
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ import isURL from './is-url';
|
||||
import devRouter from './router';
|
||||
import getMimeType from './mime-type';
|
||||
import { getYarnPath } from './yarn-installer';
|
||||
import { executeBuild, getBuildMatches } from './builder';
|
||||
import { executeBuild, getBuildMatches, shutdownBuilder } from './builder';
|
||||
import { generateErrorMessage, generateHttpStatusDescription } from './errors';
|
||||
import {
|
||||
builderDirPromise,
|
||||
@@ -347,13 +347,18 @@ export default class DevServer {
|
||||
}
|
||||
|
||||
// Delete build matches that no longer exists
|
||||
const ops: Promise<void>[] = [];
|
||||
for (const src of this.buildMatches.keys()) {
|
||||
if (!sources.includes(src)) {
|
||||
this.output.debug(`Removing build match for "${src}"`);
|
||||
// TODO: shutdown lambda functions
|
||||
const match = this.buildMatches.get(src);
|
||||
if (match) {
|
||||
ops.push(shutdownBuilder(match, this.output));
|
||||
}
|
||||
this.buildMatches.delete(src);
|
||||
}
|
||||
}
|
||||
await Promise.all(ops);
|
||||
|
||||
// Add the new matches to the `buildMatches` map
|
||||
const blockingBuilds: Promise<void>[] = [];
|
||||
@@ -429,6 +434,7 @@ export default class DevServer {
|
||||
} = buildMatch;
|
||||
if (pkg.name === '@now/static') continue;
|
||||
if (pkg.name && updatedBuilders.includes(pkg.name)) {
|
||||
shutdownBuilder(buildMatch, this.output);
|
||||
this.buildMatches.delete(src);
|
||||
this.output.debug(`Invalidated build match for "${src}"`);
|
||||
}
|
||||
@@ -729,10 +735,12 @@ export default class DevServer {
|
||||
this.yarnPath,
|
||||
this.output
|
||||
)
|
||||
.then(updatedBuilders =>
|
||||
this.invalidateBuildMatches(nowConfig, updatedBuilders)
|
||||
)
|
||||
.then(updatedBuilders => {
|
||||
this.updateBuildersPromise = null;
|
||||
this.invalidateBuildMatches(nowConfig, updatedBuilders);
|
||||
})
|
||||
.catch(err => {
|
||||
this.updateBuildersPromise = null;
|
||||
this.output.error(`Failed to update builders: ${err.message}`);
|
||||
this.output.debug(err.stack);
|
||||
});
|
||||
@@ -831,22 +839,18 @@ export default class DevServer {
|
||||
const ops: Promise<void>[] = [];
|
||||
|
||||
for (const match of this.buildMatches.values()) {
|
||||
if (!match.buildOutput) continue;
|
||||
|
||||
for (const asset of Object.values(match.buildOutput)) {
|
||||
if (asset.type === 'Lambda' && asset.fn) {
|
||||
ops.push(asset.fn.destroy());
|
||||
}
|
||||
}
|
||||
ops.push(shutdownBuilder(match, this.output));
|
||||
}
|
||||
|
||||
ops.push(close(this.server));
|
||||
|
||||
if (this.watcher) {
|
||||
this.output.debug(`Closing file watcher`);
|
||||
this.watcher.close();
|
||||
}
|
||||
|
||||
if (this.updateBuildersPromise) {
|
||||
this.output.debug(`Waiting for builders update to complete`);
|
||||
ops.push(this.updateBuildersPromise);
|
||||
}
|
||||
|
||||
@@ -1102,7 +1106,7 @@ export default class DevServer {
|
||||
}
|
||||
|
||||
const method = req.method || 'GET';
|
||||
this.output.log(`${chalk.bold(method)} ${req.url}`);
|
||||
this.output.debug(`${chalk.bold(method)} ${req.url}`);
|
||||
|
||||
try {
|
||||
const nowConfig = await this.getNowConfig();
|
||||
@@ -1452,25 +1456,6 @@ export default class DevServer {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve project directory as a static deployment.
|
||||
*/
|
||||
serveProjectAsStatic = async (
|
||||
req: http.IncomingMessage,
|
||||
res: http.ServerResponse,
|
||||
nowRequestId: string
|
||||
) => {
|
||||
const filePath = req.url ? req.url.replace(/^\//, '') : '';
|
||||
|
||||
if (filePath && typeof this.files[filePath] === 'undefined') {
|
||||
await this.send404(req, res, nowRequestId);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setResponseHeaders(res, nowRequestId);
|
||||
return serveStaticFile(req, res, this.cwd, { cleanUrls: true });
|
||||
};
|
||||
|
||||
async hasFilesystem(dest: string): Promise<boolean> {
|
||||
const requestPath = dest.replace(/^\//, '');
|
||||
if (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Ajv from 'ajv';
|
||||
import { schema as routesSchema } from '@now/routing-utils';
|
||||
import { routesSchema } from '@now/routing-utils';
|
||||
import { NowConfig } from './types';
|
||||
|
||||
const ajv = new Ajv();
|
||||
|
||||
@@ -784,6 +784,19 @@ export class CantFindConfig extends NowError<
|
||||
}
|
||||
}
|
||||
|
||||
export class WorkingDirectoryDoesNotExist extends NowError<
|
||||
'CWD_DOES_NOT_EXIST',
|
||||
{}
|
||||
> {
|
||||
constructor() {
|
||||
super({
|
||||
code: 'CWD_DOES_NOT_EXIST',
|
||||
meta: {},
|
||||
message: 'The current working directory does not exist.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class FileNotFound extends NowError<'FILE_NOT_FOUND', { file: string }> {
|
||||
constructor(file: string) {
|
||||
super({
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import path from 'path';
|
||||
import { CantParseJSONFile, CantFindConfig } from './errors-ts';
|
||||
import {
|
||||
CantParseJSONFile,
|
||||
CantFindConfig,
|
||||
WorkingDirectoryDoesNotExist,
|
||||
} from './errors-ts';
|
||||
import humanizePath from './humanize-path';
|
||||
import readJSONFile from './read-json-file';
|
||||
import readPackage from './read-package';
|
||||
@@ -8,14 +12,25 @@ import { Output } from './output';
|
||||
|
||||
let config: Config;
|
||||
|
||||
export default async function getConfig(output: Output, configFile?: string) {
|
||||
const localPath = process.cwd();
|
||||
|
||||
export default async function getConfig(
|
||||
output: Output,
|
||||
configFile?: string
|
||||
): Promise<Config | Error> {
|
||||
// If config was already read, just return it
|
||||
if (config) {
|
||||
return config;
|
||||
}
|
||||
|
||||
let localPath: string;
|
||||
try {
|
||||
localPath = process.cwd();
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
return new WorkingDirectoryDoesNotExist();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
// First try with the config supplied by the user via --local-config
|
||||
if (configFile) {
|
||||
const localFilePath = path.resolve(localPath, configFile);
|
||||
@@ -27,8 +42,7 @@ export default async function getConfig(output: Output, configFile?: string) {
|
||||
return localConfig;
|
||||
}
|
||||
if (localConfig !== null) {
|
||||
const castedConfig = localConfig;
|
||||
config = castedConfig;
|
||||
config = localConfig;
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,13 +142,14 @@ export default class Now extends EventEmitter {
|
||||
if (isBuilds) {
|
||||
deployment = await processDeployment({
|
||||
now: this,
|
||||
debug,
|
||||
output: this._output,
|
||||
hashes,
|
||||
paths,
|
||||
requestBody,
|
||||
uploadStamp,
|
||||
deployStamp,
|
||||
quiet,
|
||||
nowConfig,
|
||||
});
|
||||
} else {
|
||||
// Read `registry.npmjs.org` authToken from .npmrc
|
||||
@@ -183,7 +184,7 @@ export default class Now extends EventEmitter {
|
||||
deployment = await processDeployment({
|
||||
legacy: true,
|
||||
now: this,
|
||||
debug,
|
||||
output: this._output,
|
||||
hashes,
|
||||
paths,
|
||||
requestBody,
|
||||
@@ -191,6 +192,7 @@ export default class Now extends EventEmitter {
|
||||
deployStamp,
|
||||
quiet,
|
||||
env,
|
||||
nowConfig,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -377,7 +379,7 @@ export default class Now extends EventEmitter {
|
||||
if (!app && !Object.keys(meta).length) {
|
||||
// Get the 35 latest projects and their latest deployment
|
||||
const query = new URLSearchParams({ limit: 35 });
|
||||
const projects = await fetchRetry(`/projects/list?${query}`);
|
||||
const projects = await fetchRetry(`/v2/projects/?${query}`);
|
||||
|
||||
const deployments = await Promise.all(
|
||||
projects.map(async ({ id: projectId }) => {
|
||||
@@ -477,7 +479,7 @@ export default class Now extends EventEmitter {
|
||||
}
|
||||
|
||||
const url = `/${
|
||||
isBuilds ? 'v9' : 'v5'
|
||||
isBuilds ? 'v10' : 'v5'
|
||||
}/now/deployments/${encodeURIComponent(id)}`;
|
||||
|
||||
return this.retry(
|
||||
|
||||
14
packages/now-cli/test/dev-server.unit.js
vendored
14
packages/now-cli/test/dev-server.unit.js
vendored
@@ -31,7 +31,7 @@ function testFixture(name, fn) {
|
||||
readyResolve = resolve;
|
||||
});
|
||||
|
||||
const debug = false;
|
||||
const debug = true;
|
||||
const output = createOutput({ debug });
|
||||
const origReady = output.ready;
|
||||
|
||||
@@ -329,8 +329,8 @@ test(
|
||||
// HTML response
|
||||
const res = await fetch(`${server.address}/does-not-exist`, {
|
||||
headers: {
|
||||
Accept: 'text/html'
|
||||
}
|
||||
Accept: 'text/html',
|
||||
},
|
||||
});
|
||||
t.is(res.status, 404);
|
||||
t.is(res.headers.get('content-type'), 'text/html; charset=utf-8');
|
||||
@@ -342,8 +342,8 @@ test(
|
||||
// JSON response
|
||||
const res = await fetch(`${server.address}/does-not-exist`, {
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
}
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
t.is(res.status, 404);
|
||||
t.is(res.headers.get('content-type'), 'application/json');
|
||||
@@ -401,10 +401,10 @@ test('[DevServer] parseListen()', t => {
|
||||
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'
|
||||
'/home/user/server.sock',
|
||||
]);
|
||||
t.deepEqual(parseListen('pipe:\\\\.\\pipe\\PipeName'), [
|
||||
'\\\\.\\pipe\\PipeName'
|
||||
'\\\\.\\pipe\\PipeName',
|
||||
]);
|
||||
|
||||
let err;
|
||||
|
||||
2
packages/now-cli/test/dev/fixtures/25-nextjs-src-dir/.gitignore
vendored
Normal file
2
packages/now-cli/test/dev/fixtures/25-nextjs-src-dir/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
.next
|
||||
@@ -0,0 +1,2 @@
|
||||
README.md
|
||||
yarn.lock
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "nextjs",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "next",
|
||||
"build": "next build"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^9.1.1",
|
||||
"react": "^16.7.0",
|
||||
"react-dom": "^16.7.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
|
||||
function Index() {
|
||||
const [date, setDate] = useState(null);
|
||||
useEffect(() => {
|
||||
async function getDate() {
|
||||
const res = await fetch('/api/date');
|
||||
const newDate = await res.text();
|
||||
setDate(newDate);
|
||||
}
|
||||
getDate();
|
||||
}, []);
|
||||
return (
|
||||
<main>
|
||||
<Head>
|
||||
<title>Next.js + Node API</title>
|
||||
</Head>
|
||||
<h1>Next.js + Node.js API</h1>
|
||||
<h2>
|
||||
Deployed with{' '}
|
||||
<a
|
||||
href="https://zeit.co/docs"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
ZEIT Now
|
||||
</a>
|
||||
!
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
href="https://github.com/zeit/now-examples/blob/master/nextjs-node"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
This project
|
||||
</a>{' '}
|
||||
is a <a href="https://nextjs.org/">Next.js</a> app with two directories,{' '}
|
||||
<code>/pages</code> for static content and <code>/api</code> which
|
||||
contains a serverless <a href="https://nodejs.org/en/">Node.js</a>{' '}
|
||||
function. See{' '}
|
||||
<a href="/api/date">
|
||||
<code>api/date</code> for the Date API with Node.js
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<br />
|
||||
<h2>The date according to Node.js is:</h2>
|
||||
<p>{date ? date : 'Loading date...'}</p>
|
||||
<style jsx>{`
|
||||
main {
|
||||
align-content: center;
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
font-family: 'SF Pro Text', 'SF Pro Icons', 'Helvetica Neue',
|
||||
'Helvetica', 'Arial', sans-serif;
|
||||
hyphens: auto;
|
||||
line-height: 1.65;
|
||||
margin: 0 auto;
|
||||
max-width: 680px;
|
||||
min-height: 100vh;
|
||||
padding: 72px 0;
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
font-size: 45px;
|
||||
}
|
||||
h2 {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
p {
|
||||
font-size: 16px;
|
||||
}
|
||||
a {
|
||||
border-bottom: 1px solid white;
|
||||
color: #0076ff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
a:hover {
|
||||
border-bottom: 1px solid #0076ff;
|
||||
}
|
||||
code,
|
||||
pre {
|
||||
color: #d400ff;
|
||||
font-family: Menlo, Monaco, Lucida Console, Liberation Mono,
|
||||
DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace,
|
||||
serif;
|
||||
font-size: 0.92em;
|
||||
}
|
||||
code:before,
|
||||
code:after {
|
||||
content: '\`';
|
||||
}
|
||||
`}</style>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default Index;
|
||||
5467
packages/now-cli/test/dev/fixtures/25-nextjs-src-dir/yarn.lock
Normal file
5467
packages/now-cli/test/dev/fixtures/25-nextjs-src-dir/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -263,18 +263,25 @@ if (satisfies(process.version, '10.x')) {
|
||||
console.log('Skipping `02-angular-node` test since it requires Node >= 10.9');
|
||||
}
|
||||
|
||||
test(
|
||||
'[now dev] 03-aurelia',
|
||||
testFixtureStdio('03-aurelia', async (t, port) => {
|
||||
const result = fetch(`http://localhost:${port}`);
|
||||
const response = await result;
|
||||
// eslint has `engines: { node: ">^6.14.0 || ^8.10.0 || >=9.10.0" }` in its `package.json`
|
||||
if (satisfies(process.version, '>^6.14.0 || ^8.10.0 || >=9.10.0')) {
|
||||
test(
|
||||
'[now dev] 03-aurelia',
|
||||
testFixtureStdio('03-aurelia', async (t, port) => {
|
||||
const result = fetch(`http://localhost:${port}`);
|
||||
const response = await result;
|
||||
|
||||
validateResponseHeaders(t, response);
|
||||
validateResponseHeaders(t, response);
|
||||
|
||||
const body = await response.text();
|
||||
t.regex(body, /Aurelia Navigation Skeleton/gm);
|
||||
})
|
||||
);
|
||||
const body = await response.text();
|
||||
t.regex(body, /Aurelia Navigation Skeleton/gm);
|
||||
})
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
'Skipping `03-aurelia` test since it requires Node >= ^6.14.0 || ^8.10.0 || >=9.10.0'
|
||||
);
|
||||
}
|
||||
|
||||
// test(
|
||||
// '[now dev] 04-create-react-app-node',
|
||||
@@ -289,31 +296,45 @@ test(
|
||||
// })
|
||||
// );
|
||||
|
||||
test(
|
||||
'[now dev] 05-gatsby',
|
||||
testFixtureStdio('05-gatsby', async (t, port) => {
|
||||
const result = fetch(`http://localhost:${port}`);
|
||||
const response = await result;
|
||||
// eslint has `engines: { node: ">^6.14.0 || ^8.10.0 || >=9.10.0" }` in its `package.json`
|
||||
if (satisfies(process.version, '>^6.14.0 || ^8.10.0 || >=9.10.0')) {
|
||||
test(
|
||||
'[now dev] 05-gatsby',
|
||||
testFixtureStdio('05-gatsby', async (t, port) => {
|
||||
const result = fetch(`http://localhost:${port}`);
|
||||
const response = await result;
|
||||
|
||||
validateResponseHeaders(t, response);
|
||||
validateResponseHeaders(t, response);
|
||||
|
||||
const body = await response.text();
|
||||
t.regex(body, /Gatsby Default Starter/gm);
|
||||
})
|
||||
);
|
||||
const body = await response.text();
|
||||
t.regex(body, /Gatsby Default Starter/gm);
|
||||
})
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
'Skipping `05-gatsby` test since it requires Node >= ^6.14.0 || ^8.10.0 || >=9.10.0'
|
||||
);
|
||||
}
|
||||
|
||||
test(
|
||||
'[now dev] 06-gridsome',
|
||||
testFixtureStdio('06-gridsome', async (t, port) => {
|
||||
const result = fetch(`http://localhost:${port}`);
|
||||
const response = await result;
|
||||
// mini-css-extract-plugin has `engines: { node: ">= 6.9.0 <7.0.0 || >= 8.9.0" }` in its `package.json`
|
||||
if (satisfies(process.version, '>= 6.9.0 <7.0.0 || >= 8.9.0')) {
|
||||
test(
|
||||
'[now dev] 06-gridsome',
|
||||
testFixtureStdio('06-gridsome', async (t, port) => {
|
||||
const result = fetch(`http://localhost:${port}`);
|
||||
const response = await result;
|
||||
|
||||
validateResponseHeaders(t, response);
|
||||
validateResponseHeaders(t, response);
|
||||
|
||||
const body = await response.text();
|
||||
t.regex(body, /Hello, world!/gm);
|
||||
})
|
||||
);
|
||||
const body = await response.text();
|
||||
t.regex(body, /Hello, world!/gm);
|
||||
})
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
'Skipping `06-gridsome` test since it requires Node >= 6.9.0 <7.0.0 || >= 8.9.0'
|
||||
);
|
||||
}
|
||||
|
||||
test(
|
||||
'[now dev] 07-hexo-node',
|
||||
@@ -562,18 +583,25 @@ test('[now dev] double slashes redirect', async t => {
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
'[now dev] 18-marko',
|
||||
testFixtureStdio('18-marko', async (t, port) => {
|
||||
const result = fetch(`http://localhost:${port}`);
|
||||
const response = await result;
|
||||
// eslint has `engines: { node: ">^6.14.0 || ^8.10.0 || >=9.10.0" }` in its `package.json`
|
||||
if (satisfies(process.version, '>^6.14.0 || ^8.10.0 || >=9.10.0')) {
|
||||
test(
|
||||
'[now dev] 18-marko',
|
||||
testFixtureStdio('18-marko', async (t, port) => {
|
||||
const result = fetch(`http://localhost:${port}`);
|
||||
const response = await result;
|
||||
|
||||
validateResponseHeaders(t, response);
|
||||
validateResponseHeaders(t, response);
|
||||
|
||||
const body = await response.text();
|
||||
t.regex(body, /Marko Starter/gm);
|
||||
})
|
||||
);
|
||||
const body = await response.text();
|
||||
t.regex(body, /Marko Starter/gm);
|
||||
})
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
'Skipping `18-marko` test since it requires Node >= ^6.14.0 || ^8.10.0 || >=9.10.0'
|
||||
);
|
||||
}
|
||||
|
||||
test(
|
||||
'[now dev] 19-mithril',
|
||||
@@ -601,18 +629,23 @@ test(
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'[now dev] 21-charge',
|
||||
testFixtureStdio('21-charge', async (t, port) => {
|
||||
const result = fetch(`http://localhost:${port}`);
|
||||
const response = await result;
|
||||
// @static/charge has `engines: { node: ">= 8.10.0" }` in its `package.json`
|
||||
if (satisfies(process.version, '>= 8.10.0')) {
|
||||
test(
|
||||
'[now dev] 21-charge',
|
||||
testFixtureStdio('21-charge', async (t, port) => {
|
||||
const result = fetch(`http://localhost:${port}`);
|
||||
const response = await result;
|
||||
|
||||
validateResponseHeaders(t, response);
|
||||
validateResponseHeaders(t, response);
|
||||
|
||||
const body = await response.text();
|
||||
t.regex(body, /Welcome to my new Charge site/gm);
|
||||
})
|
||||
);
|
||||
const body = await response.text();
|
||||
t.regex(body, /Welcome to my new Charge site/gm);
|
||||
})
|
||||
);
|
||||
} else {
|
||||
console.log('Skipping `21-charge` test since it requires Node >= 8.10.0');
|
||||
}
|
||||
|
||||
test(
|
||||
'[now dev] 22-brunch',
|
||||
@@ -627,31 +660,43 @@ test(
|
||||
})
|
||||
);
|
||||
|
||||
test(
|
||||
'[now dev] 23-docusaurus',
|
||||
testFixtureStdio('23-docusaurus', async (t, port) => {
|
||||
const result = fetch(`http://localhost:${port}`);
|
||||
const response = await result;
|
||||
// react-dev-utils has `engines: { node: ">= 8.10" }` in its `package.json`
|
||||
if (satisfies(process.version, '>= 8.10')) {
|
||||
test(
|
||||
'[now dev] 23-docusaurus',
|
||||
testFixtureStdio('23-docusaurus', async (t, port) => {
|
||||
const result = fetch(`http://localhost:${port}`);
|
||||
const response = await result;
|
||||
|
||||
validateResponseHeaders(t, response);
|
||||
validateResponseHeaders(t, response);
|
||||
|
||||
const body = await response.text();
|
||||
t.regex(body, /Test Site · A website for testing/gm);
|
||||
})
|
||||
);
|
||||
const body = await response.text();
|
||||
t.regex(body, /Test Site · A website for testing/gm);
|
||||
})
|
||||
);
|
||||
} else {
|
||||
console.log('Skipping `23-docusaurus` test since it requires Node >= 8.10');
|
||||
}
|
||||
|
||||
test(
|
||||
'[now dev] 24-ember',
|
||||
testFixtureStdio('24-ember', async (t, port) => {
|
||||
const result = fetch(`http://localhost:${port}`);
|
||||
const response = await result;
|
||||
// eslint has `engines: { node: ">^6.14.0 || ^8.10.0 || >=9.10.0" }` in its `package.json`
|
||||
if (satisfies(process.version, '>^6.14.0 || ^8.10.0 || >=9.10.0')) {
|
||||
test(
|
||||
'[now dev] 24-ember',
|
||||
testFixtureStdio('24-ember', async (t, port) => {
|
||||
const result = fetch(`http://localhost:${port}`);
|
||||
const response = await result;
|
||||
|
||||
validateResponseHeaders(t, response);
|
||||
validateResponseHeaders(t, response);
|
||||
|
||||
const body = await response.text();
|
||||
t.regex(body, /HelloWorld/gm);
|
||||
})
|
||||
);
|
||||
const body = await response.text();
|
||||
t.regex(body, /HelloWorld/gm);
|
||||
})
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
'Skipping `24-ember` test since it requires Node >= ^6.14.0 || ^8.10.0 || >=9.10.0'
|
||||
);
|
||||
}
|
||||
|
||||
test('[now dev] temporary directory listing', async t => {
|
||||
const directory = fixture('temporary-directory-listing');
|
||||
@@ -869,3 +914,29 @@ test('[now dev] do not rebuild for changes in the output directory', async t =>
|
||||
dev.kill('SIGTERM');
|
||||
}
|
||||
});
|
||||
|
||||
if (satisfies(process.version, '>= 8.9.0')) {
|
||||
test('[now dev] 25-nextjs-src-dir', async t => {
|
||||
const directory = fixture('25-nextjs-src-dir');
|
||||
const { dev, port } = await testFixture(directory);
|
||||
|
||||
try {
|
||||
// start `now dev` detached in child_process
|
||||
dev.unref();
|
||||
|
||||
const result = await fetchWithRetry(`http://localhost:${port}`, 80);
|
||||
const response = await result;
|
||||
|
||||
validateResponseHeaders(t, response);
|
||||
|
||||
const body = await response.text();
|
||||
t.regex(body, /Next.js \+ Node.js API/gm);
|
||||
} finally {
|
||||
dev.kill('SIGTERM');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
'Skipping `25-nextjs-src-dir` test since it requires Node >= 8.9.0'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{
|
||||
"version": 2,
|
||||
"name": "now-dev-next",
|
||||
"builds": [
|
||||
{ "src": "package.json", "use": "@now/next" }
|
||||
],
|
||||
"builds": [{ "src": "package.json", "use": "@now/next@canary" }],
|
||||
"routes": [
|
||||
{
|
||||
"src": "/(.*)",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"builds": [
|
||||
{
|
||||
"src": "package.json",
|
||||
"use": "@now/static-build",
|
||||
"use": "@now/static-build@canary",
|
||||
"config": {
|
||||
"distDir": "public"
|
||||
}
|
||||
@@ -13,7 +13,5 @@
|
||||
"use": "@now/node"
|
||||
}
|
||||
],
|
||||
"routes": [
|
||||
{ "src": "^/api/date$", "dest": "api/date.js" }
|
||||
]
|
||||
"routes": [{ "src": "^/api/date$", "dest": "api/date.js" }]
|
||||
}
|
||||
|
||||
@@ -267,6 +267,69 @@ module.exports = (req, res) => {
|
||||
Object.assign(JSON.parse(getConfigFile(true)), { alias: 'zeit.co' })
|
||||
),
|
||||
},
|
||||
'local-config-cloud-v1': {
|
||||
'.gitignore': '*.html',
|
||||
'index.js': `
|
||||
const { createServer } = require('http');
|
||||
const { readFileSync } = require('fs');
|
||||
const svr = createServer((req, res) => {
|
||||
const { url = '/' } = req;
|
||||
const file = '.' + url;
|
||||
console.log('reading file ' + file);
|
||||
try {
|
||||
let contents = readFileSync(file, 'utf8');
|
||||
res.end(contents || '');
|
||||
} catch (e) {
|
||||
res.statusCode = 404;
|
||||
res.end('Not found');
|
||||
}
|
||||
});
|
||||
svr.listen(3000);`,
|
||||
'main.html': '<h1>hello main</h1>',
|
||||
'test.html': '<h1>hello test</h1>',
|
||||
'folder/file1.txt': 'file1',
|
||||
'folder/sub/file2.txt': 'file2',
|
||||
Dockerfile: `FROM mhart/alpine-node:latest
|
||||
LABEL name "now-cli-dockerfile-${session}"
|
||||
|
||||
RUN mkdir /app
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
RUN yarn
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["node", "index.js"]`,
|
||||
'now.json': JSON.stringify({
|
||||
version: 1,
|
||||
type: 'docker',
|
||||
features: {
|
||||
cloud: 'v1',
|
||||
},
|
||||
files: ['.gitignore', 'folder', 'index.js', 'main.html'],
|
||||
}),
|
||||
'now-test.json': JSON.stringify({
|
||||
version: 1,
|
||||
type: 'docker',
|
||||
features: {
|
||||
cloud: 'v1',
|
||||
},
|
||||
files: ['.gitignore', 'folder', 'index.js', 'test.html'],
|
||||
}),
|
||||
},
|
||||
'local-config-v2': {
|
||||
[`main-${session}.html`]: '<h1>hello main</h1>',
|
||||
[`test-${session}.html`]: '<h1>hello test</h1>',
|
||||
'now.json': JSON.stringify({
|
||||
version: 2,
|
||||
builds: [{ src: `main-${session}.html`, use: '@now/static' }],
|
||||
routes: [{ src: '/another-main', dest: `/main-${session}.html` }],
|
||||
}),
|
||||
'now-test.json': JSON.stringify({
|
||||
version: 2,
|
||||
builds: [{ src: `test-${session}.html`, use: '@now/static' }],
|
||||
routes: [{ src: '/another-test', dest: `/test-${session}.html` }],
|
||||
}),
|
||||
},
|
||||
'alias-rules': {
|
||||
'rules.json': JSON.stringify({
|
||||
rules: [
|
||||
|
||||
246
packages/now-cli/test/integration.js
vendored
246
packages/now-cli/test/integration.js
vendored
@@ -32,16 +32,25 @@ const pickUrl = stdout => {
|
||||
|
||||
const createFile = dest => fs.closeSync(fs.openSync(dest, 'w'));
|
||||
const createDirectory = dest => fs.mkdirSync(dest);
|
||||
const testv1 = async (...args) => {
|
||||
if (!process.version.startsWith('v12.')) {
|
||||
// Only run v1 tests on Node 12
|
||||
return;
|
||||
}
|
||||
await test(...args);
|
||||
};
|
||||
|
||||
const waitForDeployment = async href => {
|
||||
console.log(`waiting for ${href} to become ready...`);
|
||||
const start = Date.now();
|
||||
const max = ms('4m');
|
||||
const inspectorText = '<title>Deployment Overview';
|
||||
|
||||
// eslint-disable-next-line
|
||||
while (true) {
|
||||
const response = await fetch(href, { redirect: 'manual' });
|
||||
|
||||
if (response.status === 200) {
|
||||
const text = await response.text();
|
||||
if (response.status === 200 && !text.includes(inspectorText)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -50,7 +59,7 @@ const waitForDeployment = async href => {
|
||||
if (current - start > max || response.status >= 500) {
|
||||
throw new Error(
|
||||
`Waiting for "${href}" failed since it took longer than 4 minutes.\n` +
|
||||
`Received status ${response.status}:\n"${await response.text()}"`
|
||||
`Received status ${response.status}:\n"${text}"`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -201,6 +210,80 @@ test('login', async t => {
|
||||
t.is(typeof token, 'string');
|
||||
});
|
||||
|
||||
test('deploy using --local-config flag v2', async t => {
|
||||
const target = fixture('local-config-v2');
|
||||
|
||||
const { stdout, stderr, code } = await execa(
|
||||
binaryPath,
|
||||
['deploy', '--local-config', 'now-test.json', ...defaultArgs],
|
||||
{
|
||||
cwd: target,
|
||||
reject: false,
|
||||
}
|
||||
);
|
||||
|
||||
console.log(stderr);
|
||||
console.log(stdout);
|
||||
console.log(code);
|
||||
|
||||
t.is(code, 0);
|
||||
|
||||
const { host } = new URL(stdout);
|
||||
|
||||
const testRes = await fetch(`https://${host}/test-${contextName}.html`);
|
||||
const testText = await testRes.text();
|
||||
t.is(testText, '<h1>hello test</h1>');
|
||||
|
||||
const anotherTestRes = await fetch(`https://${host}/another-test`);
|
||||
const anotherTestText = await anotherTestRes.text();
|
||||
t.is(anotherTestText, testText);
|
||||
|
||||
const mainRes = await fetch(`https://${host}/main-${contextName}.html`);
|
||||
t.is(mainRes.status, 404, 'Should not deploy/build main now.json');
|
||||
|
||||
const anotherMainRes = await fetch(`https://${host}/another-main`);
|
||||
t.is(anotherMainRes.status, 404, 'Should not deploy/build main now.json');
|
||||
});
|
||||
|
||||
testv1('deploy using --local-config flag type cloud v1', async t => {
|
||||
const target = fixture('local-config-cloud-v1');
|
||||
|
||||
const { stdout, stderr, code } = await execa(
|
||||
binaryPath,
|
||||
['deploy', '--public', '--local-config', 'now-test.json', ...defaultArgs],
|
||||
{
|
||||
cwd: target,
|
||||
reject: false,
|
||||
}
|
||||
);
|
||||
|
||||
console.log(stderr);
|
||||
console.log(stdout);
|
||||
console.log(code);
|
||||
|
||||
t.is(code, 0);
|
||||
|
||||
const { host } = new URL(stdout);
|
||||
await waitForDeployment(`https://${host}/test.html`);
|
||||
await waitForDeployment(`https://${host}/folder/file1.txt`);
|
||||
await waitForDeployment(`https://${host}/folder/sub/file2.txt`);
|
||||
|
||||
const testRes = await fetch(`https://${host}/test.html`);
|
||||
const testText = await testRes.text();
|
||||
t.is(testText, '<h1>hello test</h1>');
|
||||
|
||||
const file1Res = await fetch(`https://${host}/folder/file1.txt`);
|
||||
const file1Text = await file1Res.text();
|
||||
t.is(file1Text, 'file1');
|
||||
|
||||
const file2Res = await fetch(`https://${host}/folder/sub/file2.txt`);
|
||||
const file2Text = await file2Res.text();
|
||||
t.is(file2Text, 'file2');
|
||||
|
||||
const mainRes = await fetch(`https://${host}/main.html`);
|
||||
t.is(mainRes.status, 404, 'Should not deploy/build main now.json');
|
||||
});
|
||||
|
||||
test('print the deploy help message', async t => {
|
||||
const { stderr, stdout, code } = await execa(
|
||||
binaryPath,
|
||||
@@ -291,7 +374,7 @@ test('detect update command', async t => {
|
||||
}
|
||||
});
|
||||
|
||||
test('login with unregisterd user', async t => {
|
||||
test('login with unregistered user', async t => {
|
||||
const { stdout, stderr, code } = await execa(
|
||||
binaryPath,
|
||||
['login', `${session}@${session}.com`, ...defaultArgs],
|
||||
@@ -312,13 +395,14 @@ test('login with unregisterd user', async t => {
|
||||
t.is(last, goal);
|
||||
});
|
||||
|
||||
test('deploy a node microservice', async t => {
|
||||
testv1('deploy a v1 node microservice', async t => {
|
||||
const target = fixture('node');
|
||||
|
||||
let { stdout, stderr, code } = await execa(
|
||||
binaryPath,
|
||||
[target, '--public', '--name', session, ...defaultArgs],
|
||||
['--public', '--name', session, ...defaultArgs],
|
||||
{
|
||||
cwd: target,
|
||||
reject: false,
|
||||
}
|
||||
);
|
||||
@@ -334,6 +418,8 @@ test('deploy a node microservice', async t => {
|
||||
const { href, host } = new URL(stdout);
|
||||
t.is(host.split('-')[0], session, formatOutput({ stdout, stderr }));
|
||||
|
||||
await waitForDeployment(href);
|
||||
|
||||
// Send a test request to the deployment
|
||||
let response = await fetch(href);
|
||||
t.is(response.status, 200);
|
||||
@@ -365,30 +451,33 @@ test('deploy a node microservice', async t => {
|
||||
t.is(response.status, 404);
|
||||
});
|
||||
|
||||
test('deploy a node microservice and infer name from `package.json`', async t => {
|
||||
const target = fixture('node');
|
||||
testv1(
|
||||
'deploy a v1 node microservice and infer name from `package.json`',
|
||||
async t => {
|
||||
const target = fixture('node');
|
||||
|
||||
const { stdout, stderr, code } = await execa(
|
||||
binaryPath,
|
||||
[target, '--public', ...defaultArgs],
|
||||
{
|
||||
reject: false,
|
||||
}
|
||||
);
|
||||
const { stdout, stderr, code } = await execa(
|
||||
binaryPath,
|
||||
[target, '--public', ...defaultArgs],
|
||||
{
|
||||
reject: false,
|
||||
}
|
||||
);
|
||||
|
||||
console.log(stderr);
|
||||
console.log(stdout);
|
||||
console.log(code);
|
||||
console.log(stderr);
|
||||
console.log(stdout);
|
||||
console.log(code);
|
||||
|
||||
// Ensure the exit code is right
|
||||
t.is(code, 0);
|
||||
// Ensure the exit code is right
|
||||
t.is(code, 0);
|
||||
|
||||
// Test if the output is really a URL
|
||||
const { host } = new URL(stdout);
|
||||
t.true(host.startsWith(`node-test-${contextName}`));
|
||||
});
|
||||
// Test if the output is really a URL
|
||||
const { host } = new URL(stdout);
|
||||
t.true(host.startsWith(`node-test-${contextName}`));
|
||||
}
|
||||
);
|
||||
|
||||
test('deploy a dockerfile project', async t => {
|
||||
testv1('deploy a v1 dockerfile project', async t => {
|
||||
const target = fixture('dockerfile');
|
||||
|
||||
// Add the "name" field to the `now.json` file
|
||||
@@ -420,11 +509,7 @@ test('deploy a dockerfile project', async t => {
|
||||
await waitForDeployment(href);
|
||||
|
||||
// Send a test request to the deployment
|
||||
const response = await fetch(href, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
const response = await fetch(href);
|
||||
t.is(response.status, 200);
|
||||
const contentType = response.headers.get('content-type');
|
||||
const textContent = await response.text();
|
||||
@@ -474,7 +559,7 @@ test('test invalid type for alias rules', async t => {
|
||||
t.regex(output.stderr, /Path Alias validation error/, formatOutput(output));
|
||||
});
|
||||
|
||||
test('apply alias rules', async t => {
|
||||
testv1('apply alias rules', async t => {
|
||||
const fixturePath = fixture('alias-rules');
|
||||
|
||||
// Create the rules file
|
||||
@@ -497,7 +582,7 @@ test('apply alias rules', async t => {
|
||||
t.is(output.code, 0, formatOutput(output));
|
||||
});
|
||||
|
||||
test('find deployment in list', async t => {
|
||||
testv1('find deployment in list', async t => {
|
||||
const output = await execa(binaryPath, ['--debug', 'ls', ...defaultArgs], {
|
||||
reject: false,
|
||||
});
|
||||
@@ -519,7 +604,7 @@ test('find deployment in list', async t => {
|
||||
t.is(target, context.deployment, formatOutput(output));
|
||||
});
|
||||
|
||||
test('find deployment in list with mixed args', async t => {
|
||||
testv1('find deployment in list with mixed args', async t => {
|
||||
const { stdout, stderr, code } = await execa(
|
||||
binaryPath,
|
||||
['--debug', 'ls', ...defaultArgs],
|
||||
@@ -545,7 +630,7 @@ test('find deployment in list with mixed args', async t => {
|
||||
t.is(target, context.deployment, formatOutput({ stdout, stderr }));
|
||||
});
|
||||
|
||||
test('create an explicit alias for deployment', async t => {
|
||||
testv1('create an explicit alias for deployment', async t => {
|
||||
const hosts = {
|
||||
deployment: context.deployment,
|
||||
alias: `${session}.now.sh`,
|
||||
@@ -579,7 +664,7 @@ test('create an explicit alias for deployment', async t => {
|
||||
context.alias = hosts.alias;
|
||||
});
|
||||
|
||||
test('list the aliases', async t => {
|
||||
testv1('list the aliases', async t => {
|
||||
const { stdout, stderr, code } = await execa(
|
||||
binaryPath,
|
||||
['alias', 'ls', ...defaultArgs],
|
||||
@@ -598,7 +683,7 @@ test('list the aliases', async t => {
|
||||
t.true(results.includes(context.deployment));
|
||||
});
|
||||
|
||||
test('scale the alias', async t => {
|
||||
testv1('scale the v1 alias', async t => {
|
||||
const { stdout, stderr, code } = await execa(
|
||||
binaryPath,
|
||||
['scale', context.alias, 'bru', '1', ...defaultArgs],
|
||||
@@ -615,7 +700,7 @@ test('scale the alias', async t => {
|
||||
t.true(stdout.includes(`(min: 1, max: 1)`));
|
||||
});
|
||||
|
||||
test('remove the explicit alias', async t => {
|
||||
testv1('remove the explicit alias', async t => {
|
||||
const goal = `> Success! Alias ${context.alias} removed`;
|
||||
|
||||
const { stdout, stderr, code } = await execa(
|
||||
@@ -634,7 +719,7 @@ test('remove the explicit alias', async t => {
|
||||
t.true(stdout.startsWith(goal));
|
||||
});
|
||||
|
||||
test('create an alias from "now.json" `alias` for deployment', async t => {
|
||||
testv1('create an v1 alias from "now.json" `alias` for deployment', async t => {
|
||||
const target = fixture('dockerfile');
|
||||
|
||||
// Add the `alias` field to the "now.json" file
|
||||
@@ -672,7 +757,7 @@ test('create an alias from "now.json" `alias` for deployment', async t => {
|
||||
context.alias = json.alias;
|
||||
});
|
||||
|
||||
test('remove the alias from "now.json" `alias`', async t => {
|
||||
testv1('remove the alias from "now.json" `alias`', async t => {
|
||||
const goal = `> Success! Alias ${context.alias} removed`;
|
||||
|
||||
const { stdout, stderr, code } = await execa(
|
||||
@@ -695,7 +780,10 @@ test('ignore files specified in .nowignore', async t => {
|
||||
const directory = fixture('nowignore');
|
||||
|
||||
const args = ['--debug', '--public', '--name', session, ...defaultArgs];
|
||||
const targetCall = await execa(binaryPath, args, { cwd: directory, reject: false });
|
||||
const targetCall = await execa(binaryPath, args, {
|
||||
cwd: directory,
|
||||
reject: false,
|
||||
});
|
||||
|
||||
console.log(targetCall.stderr);
|
||||
console.log(targetCall.stdout);
|
||||
@@ -713,7 +801,10 @@ test('ignore files specified in .nowignore via allowlist', async t => {
|
||||
const directory = fixture('nowignore-allowlist');
|
||||
|
||||
const args = ['--debug', '--public', '--name', session, ...defaultArgs];
|
||||
const targetCall = await execa(binaryPath, args, { cwd: directory, reject: false });
|
||||
const targetCall = await execa(binaryPath, args, {
|
||||
cwd: directory,
|
||||
reject: false,
|
||||
});
|
||||
|
||||
console.log(targetCall.stderr);
|
||||
console.log(targetCall.stdout);
|
||||
@@ -727,7 +818,7 @@ test('ignore files specified in .nowignore via allowlist', async t => {
|
||||
t.is(presentFile.status, 200);
|
||||
});
|
||||
|
||||
test('scale down the deployment directly', async t => {
|
||||
testv1('scale down the deployment directly', async t => {
|
||||
const { stdout, stderr, code } = await execa(
|
||||
binaryPath,
|
||||
['scale', context.deployment, 'bru', '0', ...defaultArgs],
|
||||
@@ -1946,6 +2037,40 @@ test('try to deploy with non-existing team', async t => {
|
||||
t.true(stderr.includes(goal));
|
||||
});
|
||||
|
||||
testv1('try to deploy v1 deployment with --prod', async t => {
|
||||
const target = fixture('node');
|
||||
const goal = `is not supported for Now 1.0 deployments`;
|
||||
|
||||
const { stderr, stdout, code } = await execa(binaryPath, [target, '--prod'], {
|
||||
reject: false,
|
||||
});
|
||||
|
||||
console.log(stderr);
|
||||
console.log(stdout);
|
||||
console.log(code);
|
||||
|
||||
t.is(code, 1);
|
||||
t.true(stderr.includes(goal));
|
||||
});
|
||||
|
||||
testv1('try to deploy v1 deployment with --target production', async t => {
|
||||
const target = fixture('node');
|
||||
const goal = `is not supported for Now 1.0 deployments`;
|
||||
|
||||
const { stderr, stdout, code } = await execa(
|
||||
binaryPath,
|
||||
[target, '--target', 'production'],
|
||||
{ reject: false }
|
||||
);
|
||||
|
||||
console.log(stderr);
|
||||
console.log(stdout);
|
||||
console.log(code);
|
||||
|
||||
t.is(code, 1);
|
||||
t.true(stderr.includes(goal));
|
||||
});
|
||||
|
||||
const verifyExampleAngular = (cwd, dir) =>
|
||||
fs.existsSync(path.join(cwd, dir, 'package.json')) &&
|
||||
fs.existsSync(path.join(cwd, dir, 'tsconfig.json')) &&
|
||||
@@ -2380,6 +2505,43 @@ test('now secret rm', async t => {
|
||||
t.is(output.code, 0, formatOutput(output));
|
||||
});
|
||||
|
||||
test('deploy with a custom API URL', async t => {
|
||||
const directory = fixture('static-single-file');
|
||||
|
||||
const { stdout, stderr, code } = await execa(
|
||||
binaryPath,
|
||||
[
|
||||
directory,
|
||||
'--public',
|
||||
'--name',
|
||||
session,
|
||||
'--api',
|
||||
'https://zeit.co/api',
|
||||
...defaultArgs,
|
||||
],
|
||||
{
|
||||
reject: false,
|
||||
}
|
||||
);
|
||||
|
||||
console.log(stderr);
|
||||
console.log(stdout);
|
||||
console.log(code);
|
||||
|
||||
// Ensure the exit code is right
|
||||
t.is(code, 0);
|
||||
|
||||
// Test if the output is really a URL
|
||||
const { href, host } = new URL(stdout);
|
||||
t.is(host.split('-')[0], session);
|
||||
|
||||
// Send a test request to the deployment
|
||||
const response = await fetch(href);
|
||||
const contentType = response.headers.get('content-type');
|
||||
|
||||
t.is(contentType, 'text/html; charset=utf-8');
|
||||
});
|
||||
|
||||
test.after.always(async () => {
|
||||
// Make sure the token gets revoked
|
||||
await execa(binaryPath, ['logout', ...defaultArgs]);
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
src
|
||||
types
|
||||
.git
|
||||
@@ -1,9 +1,18 @@
|
||||
{
|
||||
"name": "now-client",
|
||||
"version": "5.1.4",
|
||||
"version": "5.2.2",
|
||||
"main": "dist/src/index.js",
|
||||
"typings": "dist/src/index.d.ts",
|
||||
"homepage": "https://zeit.co",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/zeit/now.git",
|
||||
"directory": "packages/now-client"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"prepare": "npm run build",
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { readdir as readRootFolder, lstatSync } from 'fs-extra';
|
||||
|
||||
import readdir from 'recursive-readdir';
|
||||
import { relative, join } from 'path';
|
||||
import { relative, join, isAbsolute } from 'path';
|
||||
import hashes, { mapToObject } from './utils/hashes';
|
||||
import uploadAndDeploy from './upload';
|
||||
import { getNowIgnore, createDebug } from './utils';
|
||||
import { getNowIgnore, createDebug, parseNowJSON } from './utils';
|
||||
import { DeploymentError } from './errors';
|
||||
import { CreateDeploymentFunction, DeploymentOptions } from './types';
|
||||
import {
|
||||
CreateDeploymentFunction,
|
||||
DeploymentOptions,
|
||||
NowJsonOptions,
|
||||
} from './types';
|
||||
|
||||
export { EVENTS } from './utils';
|
||||
|
||||
@@ -15,9 +19,11 @@ export default function buildCreateDeployment(
|
||||
): CreateDeploymentFunction {
|
||||
return async function* createDeployment(
|
||||
path: string | string[],
|
||||
options: DeploymentOptions = {}
|
||||
options: DeploymentOptions = {},
|
||||
nowConfig?: NowJsonOptions
|
||||
): AsyncIterableIterator<any> {
|
||||
const debug = createDebug(options.debug);
|
||||
const cwd = process.cwd();
|
||||
|
||||
debug('Creating deployment...');
|
||||
|
||||
@@ -47,6 +53,22 @@ export default function buildCreateDeployment(
|
||||
|
||||
let rootFiles: string[];
|
||||
|
||||
if (Array.isArray(path)) {
|
||||
for (const filePath of path) {
|
||||
if (!isAbsolute(filePath)) {
|
||||
throw new DeploymentError({
|
||||
code: 'invalid_path',
|
||||
message: `Provided path ${filePath} is not absolute`,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (!isAbsolute(path)) {
|
||||
throw new DeploymentError({
|
||||
code: 'invalid_path',
|
||||
message: `Provided path ${path} is not absolute`,
|
||||
});
|
||||
}
|
||||
|
||||
if (isDirectory && !Array.isArray(path)) {
|
||||
debug(`Provided 'path' is a directory. Reading subpaths... `);
|
||||
rootFiles = await readRootFolder(path);
|
||||
@@ -60,7 +82,7 @@ export default function buildCreateDeployment(
|
||||
}
|
||||
|
||||
// Get .nowignore
|
||||
let ig = await getNowIgnore(path);
|
||||
let { ig, ignores } = await getNowIgnore(path);
|
||||
|
||||
debug(`Found ${ig.ignores.length} rules in .nowignore`);
|
||||
|
||||
@@ -70,7 +92,7 @@ export default function buildCreateDeployment(
|
||||
|
||||
if (isDirectory && !Array.isArray(path)) {
|
||||
// Directory path
|
||||
const dirContents = await readdir(path);
|
||||
const dirContents = await readdir(path, ignores);
|
||||
const relativeFileList = dirContents.map(filePath =>
|
||||
relative(process.cwd(), filePath)
|
||||
);
|
||||
@@ -89,6 +111,47 @@ export default function buildCreateDeployment(
|
||||
debug(`Deploying the provided path as single file`);
|
||||
}
|
||||
|
||||
if (!nowConfig) {
|
||||
// If the user did not provide a nowConfig,
|
||||
// then use the now.json file in the root.
|
||||
const fileName = 'now.json';
|
||||
const absolutePath = fileList.find(f => relative(cwd, f) === fileName);
|
||||
debug(absolutePath ? `Found ${fileName}` : `Missing ${fileName}`);
|
||||
nowConfig = await parseNowJSON(absolutePath);
|
||||
}
|
||||
|
||||
if (
|
||||
version === 1 &&
|
||||
nowConfig &&
|
||||
Array.isArray(nowConfig.files) &&
|
||||
nowConfig.files.length > 0
|
||||
) {
|
||||
// See the docs: https://zeit.co/docs/v1/features/configuration/#files-(array)
|
||||
debug('Filtering file list based on `files` key in now.json');
|
||||
const allowedFiles = new Set<string>(['Dockerfile']);
|
||||
const allowedDirs = new Set<string>();
|
||||
nowConfig.files.forEach(relPath => {
|
||||
if (lstatSync(relPath).isDirectory()) {
|
||||
allowedDirs.add(relPath);
|
||||
} else {
|
||||
allowedFiles.add(relPath);
|
||||
}
|
||||
});
|
||||
fileList = fileList.filter(absPath => {
|
||||
const relPath = relative(cwd, absPath);
|
||||
if (allowedFiles.has(relPath)) {
|
||||
return true;
|
||||
}
|
||||
for (let dir of allowedDirs) {
|
||||
if (relPath.startsWith(dir + '/')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
debug(`Found ${fileList.length} files: ${JSON.stringify(fileList)}`);
|
||||
}
|
||||
|
||||
// This is a useful warning because it prevents people
|
||||
// from getting confused about a deployment that renders 404.
|
||||
if (
|
||||
@@ -124,15 +187,21 @@ export default function buildCreateDeployment(
|
||||
force,
|
||||
defaultName,
|
||||
debug: debug_,
|
||||
apiUrl,
|
||||
...metadata
|
||||
} = options;
|
||||
|
||||
if (apiUrl) {
|
||||
debug(`Using provided API URL: ${apiUrl}`);
|
||||
}
|
||||
|
||||
debug(`Setting platform version to ${version}`);
|
||||
metadata.version = version;
|
||||
|
||||
const deploymentOpts = {
|
||||
debug: debug_,
|
||||
totalFiles: files.size,
|
||||
nowConfig,
|
||||
token,
|
||||
isDirectory,
|
||||
path,
|
||||
@@ -140,6 +209,7 @@ export default function buildCreateDeployment(
|
||||
force,
|
||||
defaultName,
|
||||
metadata,
|
||||
apiUrl,
|
||||
};
|
||||
|
||||
debug(`Creating the deployment and starting upload...`);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { DeploymentFile } from './utils/hashes';
|
||||
import {
|
||||
parseNowJSON,
|
||||
fetch,
|
||||
API_DEPLOYMENTS,
|
||||
prepareFiles,
|
||||
@@ -10,6 +9,7 @@ import {
|
||||
import checkDeploymentStatus from './deployment-status';
|
||||
import { generateQueryString } from './utils/query-string';
|
||||
import { Deployment, DeploymentOptions, NowJsonOptions } from './types';
|
||||
import { isReady, isAliasAssigned } from './utils/ready-state';
|
||||
|
||||
export interface Options {
|
||||
metadata: DeploymentOptions;
|
||||
@@ -22,6 +22,8 @@ export interface Options {
|
||||
defaultName?: string;
|
||||
preflight?: boolean;
|
||||
debug?: boolean;
|
||||
nowConfig?: NowJsonOptions;
|
||||
apiUrl?: string;
|
||||
}
|
||||
|
||||
async function* createDeployment(
|
||||
@@ -50,6 +52,7 @@ async function* createDeployment(
|
||||
...metadata,
|
||||
files: preparedFiles,
|
||||
}),
|
||||
apiUrl: options.apiUrl,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -73,6 +76,10 @@ async function* createDeployment(
|
||||
debug('Deployment created with a warning:', value);
|
||||
yield { type: 'warning', payload: value };
|
||||
}
|
||||
if (name.startsWith('x-now-notice-')) {
|
||||
debug('Deployment created with a notice:', value);
|
||||
yield { type: 'notice', payload: value };
|
||||
}
|
||||
}
|
||||
|
||||
yield { type: 'created', payload: json };
|
||||
@@ -108,32 +115,12 @@ export default async function* deploy(
|
||||
options: Options
|
||||
): AsyncIterableIterator<{ type: string; payload: any }> {
|
||||
const debug = createDebug(options.debug);
|
||||
delete options.debug;
|
||||
|
||||
debug(`Trying to read 'now.json'`);
|
||||
const nowJson: DeploymentFile | undefined = Array.from(files.values()).find(
|
||||
(file: DeploymentFile): boolean => {
|
||||
return Boolean(
|
||||
file.names.find((name: string): boolean => name.includes('now.json'))
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
debug(`'now.json' ${nowJson ? 'found' : "doesn't exist"}`);
|
||||
|
||||
const nowJsonMetadata: NowJsonOptions = parseNowJSON(nowJson);
|
||||
|
||||
const nowJsonMetadata = options.nowConfig || {};
|
||||
delete nowJsonMetadata.github;
|
||||
delete nowJsonMetadata.scope;
|
||||
|
||||
const meta = options.metadata || {};
|
||||
const metadata = { ...nowJsonMetadata, ...meta };
|
||||
if (nowJson) {
|
||||
debug(
|
||||
`Merged 'now.json' metadata and locally provided metadata:`,
|
||||
JSON.stringify(metadata)
|
||||
);
|
||||
}
|
||||
|
||||
// Check if we should default to a static deployment
|
||||
if (!metadata.version && !metadata.name) {
|
||||
@@ -207,9 +194,12 @@ export default async function* deploy(
|
||||
}
|
||||
|
||||
if (deployment) {
|
||||
if (deployment.readyState === 'READY') {
|
||||
debug('Deployment is READY. Not performing additional polling');
|
||||
return yield { type: 'ready', payload: deployment };
|
||||
if (isReady(deployment) && isAliasAssigned(deployment)) {
|
||||
debug('Deployment state changed to READY 3');
|
||||
yield { type: 'ready', payload: deployment };
|
||||
|
||||
debug('Deployment alias assigned');
|
||||
return yield { type: 'alias-assigned', payload: deployment };
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -219,7 +209,8 @@ export default async function* deploy(
|
||||
options.token,
|
||||
metadata.version,
|
||||
options.teamId,
|
||||
debug
|
||||
debug,
|
||||
options.apiUrl
|
||||
)) {
|
||||
yield event;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import sleep from 'sleep-promise';
|
||||
import ms from 'ms';
|
||||
import { fetch, API_DEPLOYMENTS, API_DEPLOYMENTS_LEGACY } from './utils';
|
||||
import { isDone, isReady, isFailed } from './utils/ready-state';
|
||||
import {
|
||||
isDone,
|
||||
isReady,
|
||||
isFailed,
|
||||
isAliasAssigned,
|
||||
isAliasError,
|
||||
} from './utils/ready-state';
|
||||
import { Deployment, DeploymentBuild } from './types';
|
||||
|
||||
interface DeploymentStatus {
|
||||
@@ -15,7 +21,8 @@ export default async function* checkDeploymentStatus(
|
||||
token: string,
|
||||
version: number | undefined,
|
||||
teamId: string | undefined,
|
||||
debug: Function
|
||||
debug: Function,
|
||||
apiUrl?: string
|
||||
): AsyncIterableIterator<DeploymentStatus> {
|
||||
let deploymentState = deployment;
|
||||
let allBuildsCompleted = false;
|
||||
@@ -25,20 +32,24 @@ export default async function* checkDeploymentStatus(
|
||||
debug(`Using ${version ? `${version}.0` : '2.0'} API for status checks`);
|
||||
|
||||
// If the deployment is ready, we don't want any of this to run
|
||||
if (isDone(deploymentState)) {
|
||||
debug(`Deployment is already READY. Not running status checks`);
|
||||
if (isDone(deploymentState) && isAliasAssigned(deploymentState)) {
|
||||
debug(
|
||||
`Deployment is already READY and aliases are assigned. Not running status checks`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build polling
|
||||
debug('Waiting for builds and the deployment to complete...');
|
||||
let readyEventFired = false;
|
||||
while (true) {
|
||||
if (!allBuildsCompleted) {
|
||||
const buildsData = await fetch(
|
||||
`${apiDeployments}/${deployment.id}/builds${
|
||||
teamId ? `?teamId=${teamId}` : ''
|
||||
}`,
|
||||
token
|
||||
token,
|
||||
{ apiUrl }
|
||||
);
|
||||
|
||||
const data = await buildsData.json();
|
||||
@@ -84,16 +95,30 @@ export default async function* checkDeploymentStatus(
|
||||
return yield { type: 'error', payload: deploymentUpdate.error };
|
||||
}
|
||||
|
||||
if (isReady(deploymentUpdate)) {
|
||||
debug('Deployment state changed to READY');
|
||||
return yield { type: 'ready', payload: deploymentUpdate };
|
||||
if (isReady(deploymentUpdate) && !readyEventFired) {
|
||||
debug('Deployment state changed to READY 2');
|
||||
readyEventFired = true;
|
||||
yield { type: 'ready', payload: deploymentUpdate };
|
||||
}
|
||||
|
||||
if (isFailed(deploymentUpdate)) {
|
||||
debug('Deployment has failed');
|
||||
if (isAliasAssigned(deploymentUpdate)) {
|
||||
debug('Deployment alias assigned');
|
||||
return yield { type: 'alias-assigned', payload: deploymentUpdate };
|
||||
}
|
||||
|
||||
const aliasError = isAliasError(deploymentUpdate);
|
||||
|
||||
if (isFailed(deploymentUpdate) || aliasError) {
|
||||
debug(
|
||||
aliasError
|
||||
? 'Alias assignment error has occurred'
|
||||
: 'Deployment has failed'
|
||||
);
|
||||
return yield {
|
||||
type: 'error',
|
||||
payload: deploymentUpdate.error || deploymentUpdate,
|
||||
payload: aliasError
|
||||
? deploymentUpdate.aliasError
|
||||
: deploymentUpdate.error || deploymentUpdate,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,8 @@ export interface Deployment {
|
||||
};
|
||||
target: string;
|
||||
alias: string[];
|
||||
aliasAssigned: boolean;
|
||||
aliasError: string | null;
|
||||
}
|
||||
|
||||
export interface DeploymentBuild {
|
||||
@@ -117,6 +119,7 @@ export interface DeploymentOptions {
|
||||
sessionAffinity?: 'ip' | 'random';
|
||||
config?: { [key: string]: any };
|
||||
debug?: boolean;
|
||||
apiUrl?: string;
|
||||
}
|
||||
|
||||
export interface NowJsonOptions {
|
||||
@@ -124,9 +127,11 @@ export interface NowJsonOptions {
|
||||
scope?: string;
|
||||
type?: 'NPM' | 'STATIC' | 'DOCKER';
|
||||
version?: number;
|
||||
files?: string[];
|
||||
}
|
||||
|
||||
export type CreateDeploymentFunction = (
|
||||
path: string | string[],
|
||||
options?: DeploymentOptions
|
||||
options?: DeploymentOptions,
|
||||
nowConfig?: NowJsonOptions
|
||||
) => AsyncIterableIterator<any>;
|
||||
|
||||
@@ -28,7 +28,7 @@ export default async function* upload(
|
||||
files: Map<string, DeploymentFile>,
|
||||
options: Options
|
||||
): AsyncIterableIterator<any> {
|
||||
const { token, teamId, debug: isDebug } = options;
|
||||
const { token, teamId, debug: isDebug, apiUrl } = options;
|
||||
const debug = createDebug(isDebug);
|
||||
|
||||
if (!files && !token && !teamId) {
|
||||
@@ -51,7 +51,7 @@ export default async function* upload(
|
||||
}
|
||||
} else {
|
||||
// If the deployment has succeeded here, don't continue
|
||||
if (event.type === 'ready') {
|
||||
if (event.type === 'alias-assigned') {
|
||||
debug('Deployment succeeded on file check');
|
||||
|
||||
return yield event;
|
||||
@@ -103,6 +103,7 @@ export default async function* upload(
|
||||
},
|
||||
body: stream,
|
||||
teamId,
|
||||
apiUrl,
|
||||
},
|
||||
isDebug
|
||||
);
|
||||
@@ -184,7 +185,7 @@ export default async function* upload(
|
||||
try {
|
||||
debug('Starting deployment creation');
|
||||
for await (const event of deploy(files, options)) {
|
||||
if (event.type === 'ready') {
|
||||
if (event.type === 'alias-assigned') {
|
||||
debug('Deployment is ready');
|
||||
return yield event;
|
||||
}
|
||||
|
||||
@@ -11,11 +11,10 @@ import { Sema } from 'async-sema';
|
||||
import { readFile } from 'fs-extra';
|
||||
const semaphore = new Sema(10);
|
||||
|
||||
export const API_FILES = 'https://api.zeit.co/v2/now/files';
|
||||
export const API_DEPLOYMENTS = 'https://api.zeit.co/v9/now/deployments';
|
||||
export const API_DEPLOYMENTS_LEGACY = 'https://api.zeit.co/v3/now/deployments';
|
||||
export const API_DELETE_DEPLOYMENTS_LEGACY =
|
||||
'https://api.zeit.co/v2/now/deployments';
|
||||
export const API_FILES = '/v2/now/files';
|
||||
export const API_DEPLOYMENTS = '/v10/now/deployments';
|
||||
export const API_DEPLOYMENTS_LEGACY = '/v3/now/deployments';
|
||||
export const API_DELETE_DEPLOYMENTS_LEGACY = '/v2/now/deployments';
|
||||
|
||||
export const EVENTS = new Set([
|
||||
// File events
|
||||
@@ -26,19 +25,20 @@ export const EVENTS = new Set([
|
||||
// Deployment events
|
||||
'created',
|
||||
'ready',
|
||||
'alias-assigned',
|
||||
'warning',
|
||||
'error',
|
||||
// Build events
|
||||
'build-state-changed',
|
||||
]);
|
||||
|
||||
export function parseNowJSON(file?: DeploymentFile): NowJsonOptions {
|
||||
if (!file) {
|
||||
export async function parseNowJSON(filePath?: string): Promise<NowJsonOptions> {
|
||||
if (!filePath) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
const jsonString = file.data.toString();
|
||||
const jsonString = await readFile(filePath, 'utf8');
|
||||
|
||||
return JSON.parse(jsonString);
|
||||
} catch (e) {
|
||||
@@ -96,7 +96,7 @@ export async function getNowIgnore(path: string | string[]): Promise<any> {
|
||||
|
||||
const ig = ignore().add(`${ignores.join('\n')}\n${nowIgnore}`);
|
||||
|
||||
return ig;
|
||||
return { ig, ignores };
|
||||
}
|
||||
|
||||
export const fetch = async (
|
||||
@@ -109,6 +109,9 @@ export const fetch = async (
|
||||
const debug = createDebug(debugEnabled);
|
||||
let time: number;
|
||||
|
||||
url = `${opts.apiUrl || 'https://api.zeit.co'}${url}`;
|
||||
delete opts.apiUrl;
|
||||
|
||||
if (opts.teamId) {
|
||||
const parsedUrl = parseUrl(url, true);
|
||||
const query = parsedUrl.query;
|
||||
|
||||
@@ -14,3 +14,7 @@ export const isFailed = ({
|
||||
export const isDone = (
|
||||
buildOrDeployment: Deployment | DeploymentBuild
|
||||
): boolean => isReady(buildOrDeployment) || isFailed(buildOrDeployment);
|
||||
export const isAliasAssigned = (deployment: Deployment): boolean =>
|
||||
Boolean(deployment.aliasAssigned);
|
||||
export const isAliasError = (deployment: Deployment): boolean =>
|
||||
Boolean(deployment.aliasError);
|
||||
|
||||
30
packages/now-client/tests/common.ts
Normal file
30
packages/now-client/tests/common.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import fetch from 'node-fetch';
|
||||
const str = 'aHR0cHM6Ly9hcGktdG9rZW4tZmFjdG9yeS56ZWl0LnNo';
|
||||
|
||||
async function fetchTokenWithRetry(url: string, retries = 3): Promise<string> {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
return data.token;
|
||||
} catch (error) {
|
||||
console.log(`Failed to fetch token. Retries remaining: ${retries}`);
|
||||
if (retries === 0) {
|
||||
throw error;
|
||||
}
|
||||
await sleep(500);
|
||||
return fetchTokenWithRetry(url, retries - 1);
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateNewToken(): Promise<string> {
|
||||
const token = await fetchTokenWithRetry(
|
||||
Buffer.from(str, 'base64').toString()
|
||||
);
|
||||
return token;
|
||||
}
|
||||
|
||||
export function sleep(ms: number) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
// zeit-support user
|
||||
export const TOKEN = 'HRp5EAN0TZBnSUBIleD3ZrMW'
|
||||
@@ -1,19 +1,24 @@
|
||||
import path from 'path';
|
||||
import { TOKEN } from './constants';
|
||||
import { generateNewToken } from './common';
|
||||
import { fetch, API_DEPLOYMENTS } from '../src/utils';
|
||||
import { Deployment } from './types';
|
||||
import { createDeployment } from '../src/index';
|
||||
|
||||
describe('create v2 deployment', () => {
|
||||
let deployment: Deployment;
|
||||
let token = '';
|
||||
|
||||
beforeEach(async () => {
|
||||
token = await generateNewToken();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (deployment) {
|
||||
const response = await fetch(
|
||||
`${API_DEPLOYMENTS}/${deployment.id}`,
|
||||
TOKEN,
|
||||
token,
|
||||
{
|
||||
method: 'DELETE'
|
||||
method: 'DELETE',
|
||||
}
|
||||
);
|
||||
expect(response.status).toEqual(200);
|
||||
@@ -24,8 +29,8 @@ describe('create v2 deployment', () => {
|
||||
for await (const event of createDeployment(
|
||||
path.resolve(__dirname, 'fixtures', 'v2'),
|
||||
{
|
||||
token: TOKEN,
|
||||
name: 'now-client-tests-v2'
|
||||
token,
|
||||
name: 'now-client-tests-v2',
|
||||
}
|
||||
)) {
|
||||
if (event.type === 'warning') {
|
||||
@@ -43,8 +48,8 @@ describe('create v2 deployment', () => {
|
||||
for await (const event of createDeployment(
|
||||
path.resolve(__dirname, 'fixtures', 'v2'),
|
||||
{
|
||||
token: TOKEN,
|
||||
name: 'now-client-tests-v2'
|
||||
token,
|
||||
name: 'now-client-tests-v2',
|
||||
}
|
||||
)) {
|
||||
if (event.type === 'file_count') {
|
||||
@@ -62,8 +67,8 @@ describe('create v2 deployment', () => {
|
||||
for await (const event of createDeployment(
|
||||
path.resolve(__dirname, 'fixtures', 'v2'),
|
||||
{
|
||||
token: TOKEN,
|
||||
name: 'now-client-tests-v2'
|
||||
token,
|
||||
name: 'now-client-tests-v2',
|
||||
}
|
||||
)) {
|
||||
if (event.type === 'ready') {
|
||||
|
||||
@@ -1,77 +1,83 @@
|
||||
import path from 'path'
|
||||
import { TOKEN } from './constants'
|
||||
import { fetch, API_DELETE_DEPLOYMENTS_LEGACY } from '../src/utils'
|
||||
import { Deployment } from './types'
|
||||
import { createLegacyDeployment } from '../src/index'
|
||||
import path from 'path';
|
||||
import { generateNewToken } from './common';
|
||||
import { fetch, API_DELETE_DEPLOYMENTS_LEGACY } from '../src/utils';
|
||||
import { Deployment } from './types';
|
||||
import { createLegacyDeployment } from '../src/index';
|
||||
|
||||
describe('create v1 deployment', () => {
|
||||
let deployment: Deployment | undefined
|
||||
let deployment: Deployment | undefined;
|
||||
let token = '';
|
||||
|
||||
beforeEach(async () => {
|
||||
token = await generateNewToken();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (deployment) {
|
||||
const response = await fetch(
|
||||
`${API_DELETE_DEPLOYMENTS_LEGACY}/${deployment.deploymentId || deployment.uid}`,
|
||||
TOKEN,
|
||||
`${API_DELETE_DEPLOYMENTS_LEGACY}/${deployment.deploymentId ||
|
||||
deployment.uid}`,
|
||||
token,
|
||||
{
|
||||
method: 'DELETE'
|
||||
method: 'DELETE',
|
||||
}
|
||||
)
|
||||
expect(response.status).toEqual(200)
|
||||
deployment = undefined
|
||||
);
|
||||
expect(response.status).toEqual(200);
|
||||
deployment = undefined;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
it('will create a v1 static deployment', async () => {
|
||||
for await (const event of createLegacyDeployment(
|
||||
path.resolve(__dirname, 'fixtures', 'v1', 'static'),
|
||||
{
|
||||
token: TOKEN,
|
||||
name: 'now-client-tests-v1-static'
|
||||
token,
|
||||
name: 'now-client-tests-v1-static',
|
||||
}
|
||||
)) {
|
||||
if (event.type === 'ready') {
|
||||
deployment = event.payload
|
||||
deployment = event.payload;
|
||||
if (deployment) {
|
||||
expect(deployment.readyState || deployment.state).toEqual('READY')
|
||||
break
|
||||
expect(deployment.readyState || deployment.state).toEqual('READY');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
it('will create a v1 npm deployment', async () => {
|
||||
for await (const event of createLegacyDeployment(
|
||||
path.resolve(__dirname, 'fixtures', 'v1', 'npm'),
|
||||
{
|
||||
token: TOKEN,
|
||||
name: 'now-client-tests-v1-npm'
|
||||
token,
|
||||
name: 'now-client-tests-v1-npm',
|
||||
}
|
||||
)) {
|
||||
if (event.type === 'ready') {
|
||||
deployment = event.payload
|
||||
deployment = event.payload;
|
||||
if (deployment) {
|
||||
expect(deployment.readyState || deployment.state).toEqual('READY')
|
||||
break
|
||||
expect(deployment.readyState || deployment.state).toEqual('READY');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
it('will create a v1 Docker deployment', async () => {
|
||||
for await (const event of createLegacyDeployment(
|
||||
path.resolve(__dirname, 'fixtures', 'v1', 'docker'),
|
||||
{
|
||||
token: TOKEN,
|
||||
name: 'now-client-tests-v1-docker'
|
||||
token,
|
||||
name: 'now-client-tests-v1-docker',
|
||||
}
|
||||
)) {
|
||||
if (event.type === 'ready') {
|
||||
deployment = event.payload
|
||||
deployment = event.payload;
|
||||
if (deployment) {
|
||||
expect(deployment.readyState || deployment.state).toEqual('READY')
|
||||
break
|
||||
expect(deployment.readyState || deployment.state).toEqual('READY');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
32
packages/now-client/tests/paths.test.ts
Normal file
32
packages/now-client/tests/paths.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { generateNewToken } from './common';
|
||||
import { createDeployment } from '../src/index';
|
||||
|
||||
describe('path handling', () => {
|
||||
let token = '';
|
||||
|
||||
beforeEach(async () => {
|
||||
token = await generateNewToken();
|
||||
});
|
||||
|
||||
it('will fali with a relative path', async () => {
|
||||
try {
|
||||
await createDeployment('./fixtures/v2/now.json', {
|
||||
token,
|
||||
name: 'now-client-tests-v2',
|
||||
});
|
||||
} catch (e) {
|
||||
expect(e.code).toEqual('invalid_path');
|
||||
}
|
||||
});
|
||||
|
||||
it('will fali with an array of relative paths', async () => {
|
||||
try {
|
||||
await createDeployment(['./fixtures/v2/now.json'], {
|
||||
token,
|
||||
name: 'now-client-tests-v2',
|
||||
});
|
||||
} catch (e) {
|
||||
expect(e.code).toEqual('invalid_path');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1 +1 @@
|
||||
jest.setTimeout(120000)
|
||||
jest.setTimeout(120000);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@now/next",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.5",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://zeit.co/docs/v2/deployments/official-builders/next-js-now-next",
|
||||
|
||||
@@ -37,12 +37,10 @@ import {
|
||||
EnvConfig,
|
||||
excludeFiles,
|
||||
ExperimentalTraceVersion,
|
||||
filesFromDirectory,
|
||||
getDynamicRoutes,
|
||||
getNextConfig,
|
||||
getPathsInside,
|
||||
getRoutes,
|
||||
includeOnlyEntryDirectory,
|
||||
isDynamicRoute,
|
||||
normalizePackageJson,
|
||||
normalizePage,
|
||||
@@ -69,6 +67,20 @@ interface BuildParamsType extends BuildOptions {
|
||||
|
||||
export const version = 2;
|
||||
|
||||
const nowDevChildProcesses = new Set<ChildProcess>();
|
||||
|
||||
['SIGINT', 'SIGTERM'].forEach(signal => {
|
||||
process.once(signal as NodeJS.Signals, () => {
|
||||
for (const child of nowDevChildProcesses) {
|
||||
debug(
|
||||
`Got ${signal}, killing dev server child process (pid=${child.pid})`
|
||||
);
|
||||
process.kill(child.pid, signal);
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Read package.json from files
|
||||
*/
|
||||
@@ -214,6 +226,7 @@ export const build = async ({
|
||||
const { forked, getUrl } = startDevServer(entryPath, runtimeEnv);
|
||||
urls[entrypoint] = await getUrl();
|
||||
childProcess = forked;
|
||||
nowDevChildProcesses.add(forked);
|
||||
debug(
|
||||
`${name} Development server for ${entrypoint} running at ${urls[entrypoint]}`
|
||||
);
|
||||
@@ -337,7 +350,7 @@ export const build = async ({
|
||||
if (isLegacy) {
|
||||
const filesAfterBuild = await glob('**', entryPath);
|
||||
|
||||
debug('Preparing lambda files...');
|
||||
debug('Preparing serverless function files...');
|
||||
let buildId: string;
|
||||
try {
|
||||
buildId = await readFile(
|
||||
@@ -405,7 +418,7 @@ export const build = async ({
|
||||
],
|
||||
};
|
||||
|
||||
debug(`Creating lambda for page: "${page}"...`);
|
||||
debug(`Creating serverless function for page: "${page}"...`);
|
||||
lambdas[path.join(entryDirectory, pathname)] = await createLambda({
|
||||
files: {
|
||||
...nextFiles,
|
||||
@@ -415,11 +428,11 @@ export const build = async ({
|
||||
handler: 'now__launcher.launcher',
|
||||
runtime: nodeVersion.runtime,
|
||||
});
|
||||
debug(`Created lambda for page: "${page}"`);
|
||||
debug(`Created serverless function for page: "${page}"`);
|
||||
})
|
||||
);
|
||||
} else {
|
||||
debug('Preparing lambda files...');
|
||||
debug('Preparing serverless function files...');
|
||||
const pagesDir = path.join(entryPath, '.next', 'serverless', 'pages');
|
||||
|
||||
const pages = await glob('**/*.js', pagesDir);
|
||||
@@ -496,7 +509,8 @@ export const build = async ({
|
||||
} = {};
|
||||
|
||||
if (requiresTracing) {
|
||||
const tracingLabel = 'Tracing Next.js lambdas for external files ...';
|
||||
const tracingLabel =
|
||||
'Tracing Next.js serverless functions for external files ...';
|
||||
console.time(tracingLabel);
|
||||
|
||||
const apiPages: string[] = [];
|
||||
@@ -542,7 +556,7 @@ export const build = async ({
|
||||
apiFileList.forEach(collectTracedFiles(apiReasons, apiTracedFiles));
|
||||
console.timeEnd(tracingLabel);
|
||||
|
||||
const zippingLabel = 'Compressing shared lambda files';
|
||||
const zippingLabel = 'Compressing shared serverless function files';
|
||||
console.time(zippingLabel);
|
||||
|
||||
pseudoLayers.push(await createPseudoLayer(tracedFiles));
|
||||
@@ -560,7 +574,9 @@ export const build = async ({
|
||||
|
||||
const assetKeys = Object.keys(assets);
|
||||
if (assetKeys.length > 0) {
|
||||
debug('detected (legacy) assets to be bundled with lambda:');
|
||||
debug(
|
||||
'detected (legacy) assets to be bundled with serverless function:'
|
||||
);
|
||||
assetKeys.forEach(assetFile => debug(`\t${assetFile}`));
|
||||
debug(
|
||||
'\nPlease upgrade to Next.js 9.1 to leverage modern asset handling.'
|
||||
@@ -570,7 +586,7 @@ export const build = async ({
|
||||
|
||||
const launcherPath = path.join(__dirname, 'templated-launcher.js');
|
||||
const launcherData = await readFile(launcherPath, 'utf8');
|
||||
const allLambdasLabel = `All lambdas created`;
|
||||
const allLambdasLabel = `All serverless functions created (in parallel)`;
|
||||
console.time(allLambdasLabel);
|
||||
|
||||
await Promise.all(
|
||||
@@ -586,7 +602,7 @@ export const build = async ({
|
||||
dynamicPages.push(normalizePage(pathname));
|
||||
}
|
||||
|
||||
const label = `Creating lambda for page: "${page}"...`;
|
||||
const label = `Created serverless function for "${page}" in`;
|
||||
console.time(label);
|
||||
|
||||
const pageFileName = path.normalize(
|
||||
@@ -637,6 +653,9 @@ export const build = async ({
|
||||
'**',
|
||||
path.join(entryPath, '.next', 'static')
|
||||
);
|
||||
const staticFolderFiles = await glob('**', path.join(entryPath, 'static'));
|
||||
const publicFolderFiles = await glob('**', path.join(entryPath, 'public'));
|
||||
|
||||
const staticFiles = Object.keys(nextStaticFiles).reduce(
|
||||
(mappedFiles, file) => ({
|
||||
...mappedFiles,
|
||||
@@ -646,23 +665,24 @@ export const build = async ({
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
const entryDirectoryFiles = includeOnlyEntryDirectory(files, entryDirectory);
|
||||
const staticDirectoryFiles = filesFromDirectory(
|
||||
entryDirectoryFiles,
|
||||
path.join(entryDirectory, 'static')
|
||||
);
|
||||
const publicDirectoryFiles = filesFromDirectory(
|
||||
entryDirectoryFiles,
|
||||
path.join(entryDirectory, 'public')
|
||||
);
|
||||
const publicFiles = Object.keys(publicDirectoryFiles).reduce(
|
||||
const staticDirectoryFiles = Object.keys(staticFolderFiles).reduce(
|
||||
(mappedFiles, file) => ({
|
||||
...mappedFiles,
|
||||
[file.replace(/public[/\\]+/, '')]: publicDirectoryFiles[file],
|
||||
[path.join(entryDirectory, 'static', file)]: staticFolderFiles[file],
|
||||
}),
|
||||
{}
|
||||
);
|
||||
const publicDirectoryFiles = Object.keys(publicFolderFiles).reduce(
|
||||
(mappedFiles, file) => ({
|
||||
...mappedFiles,
|
||||
[path.join(
|
||||
entryDirectory,
|
||||
file.replace(/public[/\\]+/, '')
|
||||
)]: publicFolderFiles[file],
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
let dynamicPrefix = path.join('/', entryDirectory);
|
||||
dynamicPrefix = dynamicPrefix === '/' ? '' : dynamicPrefix;
|
||||
|
||||
@@ -682,7 +702,7 @@ export const build = async ({
|
||||
|
||||
return {
|
||||
output: {
|
||||
...publicFiles,
|
||||
...publicDirectoryFiles,
|
||||
...lambdas,
|
||||
...staticPages,
|
||||
...staticFiles,
|
||||
@@ -695,7 +715,7 @@ export const build = async ({
|
||||
{
|
||||
// This ensures we only match known emitted-by-Next.js files and not
|
||||
// user-emitted files which may be missing a hash in their filename.
|
||||
src: '/_next/static/(?:[^/]+/pages|chunks|runtime)/.+',
|
||||
src: '/_next/static/(?:[^/]+/pages|chunks|runtime|css|media)/.+',
|
||||
// Next.js assets contain a hash or entropy in their filenames, so they
|
||||
// are guaranteed to be unique and cacheable indefinitely.
|
||||
headers: { 'cache-control': 'public,max-age=31536000,immutable' },
|
||||
|
||||
@@ -60,24 +60,6 @@ function excludeFiles(
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Files object holding only the entrypoint files
|
||||
*/
|
||||
function includeOnlyEntryDirectory(
|
||||
files: Files,
|
||||
entryDirectory: string
|
||||
): Files {
|
||||
if (entryDirectory === '.') {
|
||||
return files;
|
||||
}
|
||||
|
||||
function matcher(filePath: string) {
|
||||
return !filePath.startsWith(entryDirectory);
|
||||
}
|
||||
|
||||
return excludeFiles(files, matcher);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exclude package manager lockfiles from files
|
||||
*/
|
||||
@@ -92,17 +74,6 @@ function excludeLockFiles(files: Files): Files {
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Include only the files from a selected directory
|
||||
*/
|
||||
function filesFromDirectory(files: Files, dir: string): Files {
|
||||
function matcher(filePath: string) {
|
||||
return !filePath.startsWith(dir.replace(/\\/g, '/'));
|
||||
}
|
||||
|
||||
return excludeFiles(files, matcher);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce specific package.json configuration for smallest possible lambda
|
||||
*/
|
||||
@@ -205,17 +176,35 @@ function getRoutes(
|
||||
files: Files,
|
||||
url: string
|
||||
): Route[] {
|
||||
let pagesDir = '';
|
||||
const filesInside: Files = {};
|
||||
const prefix = entryDirectory === `.` ? `/` : `/${entryDirectory}/`;
|
||||
const fileKeys = Object.keys(files);
|
||||
|
||||
for (const file of Object.keys(files)) {
|
||||
for (const file of fileKeys) {
|
||||
if (!pathsInside.includes(file)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!pagesDir) {
|
||||
if (file.startsWith(path.join(entryDirectory, 'pages'))) {
|
||||
pagesDir = 'pages';
|
||||
}
|
||||
}
|
||||
|
||||
filesInside[file] = files[file];
|
||||
}
|
||||
|
||||
// If default pages dir isn't found check for `src/pages`
|
||||
if (
|
||||
!pagesDir &&
|
||||
fileKeys.some(file =>
|
||||
file.startsWith(path.join(entryDirectory, 'src/pages'))
|
||||
)
|
||||
) {
|
||||
pagesDir = 'src/pages';
|
||||
}
|
||||
|
||||
const routes: Route[] = [
|
||||
{
|
||||
src: `${prefix}_next/(.*)`,
|
||||
@@ -231,13 +220,13 @@ function getRoutes(
|
||||
|
||||
for (const file of filePaths) {
|
||||
const relativePath = path.relative(entryDirectory, file);
|
||||
const isPage = pathIsInside('pages', relativePath);
|
||||
const isPage = pathIsInside(pagesDir, relativePath);
|
||||
|
||||
if (!isPage) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const relativeToPages = path.relative('pages', relativePath);
|
||||
const relativeToPages = path.relative(pagesDir, relativePath);
|
||||
const extension = path.extname(relativeToPages);
|
||||
const pageName = relativeToPages.replace(extension, '').replace(/\\/g, '/');
|
||||
|
||||
@@ -484,10 +473,8 @@ export async function createLambdaFromPseudoLayers({
|
||||
export {
|
||||
excludeFiles,
|
||||
validateEntrypoint,
|
||||
includeOnlyEntryDirectory,
|
||||
excludeLockFiles,
|
||||
normalizePackageJson,
|
||||
filesFromDirectory,
|
||||
getNextConfig,
|
||||
getPathsInside,
|
||||
getRoutes,
|
||||
|
||||
@@ -122,6 +122,7 @@ it(
|
||||
buildResult: { output },
|
||||
} = await runBuildLambda(path.join(__dirname, 'public-files'));
|
||||
expect(output['robots.txt']).toBeDefined();
|
||||
expect(output['generated.txt']).toBeDefined();
|
||||
},
|
||||
FOUR_MINUTES
|
||||
);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
const fs = require('fs');
|
||||
|
||||
// Adds a new file to the public folder at build time
|
||||
fs.writeFileSync('public/generated.txt', 'Generated');
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"scripts": {
|
||||
"now-build": "next build"
|
||||
"now-build": "node create-public-file.js && next build"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "8",
|
||||
"next": "9",
|
||||
"react": "16",
|
||||
"react-dom": "16"
|
||||
}
|
||||
|
||||
@@ -2,9 +2,8 @@ const path = require('path');
|
||||
const {
|
||||
excludeFiles,
|
||||
validateEntrypoint,
|
||||
includeOnlyEntryDirectory,
|
||||
normalizePackageJson,
|
||||
getNextConfig
|
||||
getNextConfig,
|
||||
} = require('@now/next/dist/utils');
|
||||
const { FileRef } = require('@now/build-utils');
|
||||
|
||||
@@ -33,7 +32,7 @@ describe('excludeFiles', () => {
|
||||
const files = {
|
||||
'pages/index.js': new FileRef({ digest: 'index' }),
|
||||
'package.json': new FileRef({ digest: 'package' }),
|
||||
'package-lock.json': new FileRef({ digest: 'package-lock' })
|
||||
'package-lock.json': new FileRef({ digest: 'package-lock' }),
|
||||
};
|
||||
const result = excludeFiles(
|
||||
files,
|
||||
@@ -63,21 +62,6 @@ describe('validateEntrypoint', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('includeOnlyEntryDirectory', () => {
|
||||
it('should include files outside entry directory', () => {
|
||||
const entryDirectory = 'frontend';
|
||||
const files = {
|
||||
'frontend/pages/index.js': new FileRef({ digest: 'index' }),
|
||||
'package.json': new FileRef({ digest: 'package' }),
|
||||
'package-lock.json': new FileRef({ digest: 'package-lock' })
|
||||
};
|
||||
const result = includeOnlyEntryDirectory(files, entryDirectory);
|
||||
expect(result['frontend/pages/index.js']).toBeDefined();
|
||||
expect(result['package.json']).toBeUndefined();
|
||||
expect(result['package-lock.json']).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizePackageJson', () => {
|
||||
it('should work without a package.json being supplied', () => {
|
||||
const result = normalizePackageJson();
|
||||
@@ -85,15 +69,15 @@ describe('normalizePackageJson', () => {
|
||||
dependencies: {
|
||||
'next-server': 'v7.0.2-canary.49',
|
||||
react: 'latest',
|
||||
'react-dom': 'latest'
|
||||
'react-dom': 'latest',
|
||||
},
|
||||
devDependencies: {
|
||||
next: 'v7.0.2-canary.49'
|
||||
next: 'v7.0.2-canary.49',
|
||||
},
|
||||
scripts: {
|
||||
'now-build':
|
||||
'NODE_OPTIONS=--max_old_space_size=3000 next build --lambdas'
|
||||
}
|
||||
'NODE_OPTIONS=--max_old_space_size=3000 next build --lambdas',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -102,29 +86,29 @@ describe('normalizePackageJson', () => {
|
||||
dependencies: {
|
||||
'next-server': 'v7.0.2-canary.49',
|
||||
react: 'latest',
|
||||
'react-dom': 'latest'
|
||||
'react-dom': 'latest',
|
||||
},
|
||||
devDependencies: {
|
||||
next: 'v7.0.2-canary.49'
|
||||
next: 'v7.0.2-canary.49',
|
||||
},
|
||||
scripts: {
|
||||
'now-build': 'next build'
|
||||
}
|
||||
'now-build': 'next build',
|
||||
},
|
||||
};
|
||||
const result = normalizePackageJson(defaultPackage);
|
||||
expect(result).toEqual({
|
||||
dependencies: {
|
||||
'next-server': 'v7.0.2-canary.49',
|
||||
react: 'latest',
|
||||
'react-dom': 'latest'
|
||||
'react-dom': 'latest',
|
||||
},
|
||||
devDependencies: {
|
||||
next: 'v7.0.2-canary.49'
|
||||
next: 'v7.0.2-canary.49',
|
||||
},
|
||||
scripts: {
|
||||
'now-build':
|
||||
'NODE_OPTIONS=--max_old_space_size=3000 next build --lambdas'
|
||||
}
|
||||
'NODE_OPTIONS=--max_old_space_size=3000 next build --lambdas',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -133,23 +117,23 @@ describe('normalizePackageJson', () => {
|
||||
dependencies: {
|
||||
react: 'latest',
|
||||
'react-dom': 'latest',
|
||||
next: 'latest'
|
||||
}
|
||||
next: 'latest',
|
||||
},
|
||||
};
|
||||
const result = normalizePackageJson(defaultPackage);
|
||||
expect(result).toEqual({
|
||||
dependencies: {
|
||||
'next-server': 'v7.0.2-canary.49',
|
||||
react: 'latest',
|
||||
'react-dom': 'latest'
|
||||
'react-dom': 'latest',
|
||||
},
|
||||
devDependencies: {
|
||||
next: 'v7.0.2-canary.49'
|
||||
next: 'v7.0.2-canary.49',
|
||||
},
|
||||
scripts: {
|
||||
'now-build':
|
||||
'NODE_OPTIONS=--max_old_space_size=3000 next build --lambdas'
|
||||
}
|
||||
'NODE_OPTIONS=--max_old_space_size=3000 next build --lambdas',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -158,23 +142,23 @@ describe('normalizePackageJson', () => {
|
||||
dependencies: {
|
||||
react: 'latest',
|
||||
'react-dom': 'latest',
|
||||
next: 'latest'
|
||||
}
|
||||
next: 'latest',
|
||||
},
|
||||
};
|
||||
const result = normalizePackageJson(defaultPackage);
|
||||
expect(result).toEqual({
|
||||
dependencies: {
|
||||
'next-server': 'v7.0.2-canary.49',
|
||||
react: 'latest',
|
||||
'react-dom': 'latest'
|
||||
'react-dom': 'latest',
|
||||
},
|
||||
devDependencies: {
|
||||
next: 'v7.0.2-canary.49'
|
||||
next: 'v7.0.2-canary.49',
|
||||
},
|
||||
scripts: {
|
||||
'now-build':
|
||||
'NODE_OPTIONS=--max_old_space_size=3000 next build --lambdas'
|
||||
}
|
||||
'NODE_OPTIONS=--max_old_space_size=3000 next build --lambdas',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -183,23 +167,23 @@ describe('normalizePackageJson', () => {
|
||||
dependencies: {
|
||||
react: 'latest',
|
||||
'react-dom': 'latest',
|
||||
next: 'latest'
|
||||
}
|
||||
next: 'latest',
|
||||
},
|
||||
};
|
||||
const result = normalizePackageJson(defaultPackage);
|
||||
expect(result).toEqual({
|
||||
dependencies: {
|
||||
'next-server': 'v7.0.2-canary.49',
|
||||
react: 'latest',
|
||||
'react-dom': 'latest'
|
||||
'react-dom': 'latest',
|
||||
},
|
||||
devDependencies: {
|
||||
next: 'v7.0.2-canary.49'
|
||||
next: 'v7.0.2-canary.49',
|
||||
},
|
||||
scripts: {
|
||||
'now-build':
|
||||
'NODE_OPTIONS=--max_old_space_size=3000 next build --lambdas'
|
||||
}
|
||||
'NODE_OPTIONS=--max_old_space_size=3000 next build --lambdas',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -211,7 +195,7 @@ describe('normalizePackageJson', () => {
|
||||
dev: 'next',
|
||||
build: 'next build',
|
||||
start: 'next start',
|
||||
test: "xo && stylelint './pages/**/*.js' && jest"
|
||||
test: "xo && stylelint './pages/**/*.js' && jest",
|
||||
},
|
||||
main: 'pages/index.js',
|
||||
license: 'MIT',
|
||||
@@ -226,7 +210,7 @@ describe('normalizePackageJson', () => {
|
||||
'stylelint-config-recommended': '^2.1.0',
|
||||
'stylelint-config-styled-components': '^0.1.1',
|
||||
'stylelint-processor-styled-components': '^1.5.1',
|
||||
xo: '^0.23.0'
|
||||
xo: '^0.23.0',
|
||||
},
|
||||
dependencies: {
|
||||
consola: '^2.2.6',
|
||||
@@ -234,7 +218,7 @@ describe('normalizePackageJson', () => {
|
||||
next: '^7.0.2',
|
||||
react: '^16.6.3',
|
||||
'react-dom': '^16.6.3',
|
||||
'styled-components': '^4.1.1'
|
||||
'styled-components': '^4.1.1',
|
||||
},
|
||||
xo: {
|
||||
extends: 'xo-react',
|
||||
@@ -244,15 +228,15 @@ describe('normalizePackageJson', () => {
|
||||
'test',
|
||||
'pages/_document.js',
|
||||
'pages/index.js',
|
||||
'pages/home.js'
|
||||
'pages/home.js',
|
||||
],
|
||||
rules: {
|
||||
'react/no-unescaped-entities': null
|
||||
}
|
||||
'react/no-unescaped-entities': null,
|
||||
},
|
||||
},
|
||||
jest: {
|
||||
testEnvironment: 'node'
|
||||
}
|
||||
testEnvironment: 'node',
|
||||
},
|
||||
};
|
||||
const result = normalizePackageJson(defaultPackage);
|
||||
expect(result).toEqual({
|
||||
@@ -263,7 +247,7 @@ describe('normalizePackageJson', () => {
|
||||
'now-build':
|
||||
'NODE_OPTIONS=--max_old_space_size=3000 next build --lambdas',
|
||||
start: 'next start',
|
||||
test: "xo && stylelint './pages/**/*.js' && jest"
|
||||
test: "xo && stylelint './pages/**/*.js' && jest",
|
||||
},
|
||||
main: 'pages/index.js',
|
||||
license: 'MIT',
|
||||
@@ -283,12 +267,12 @@ describe('normalizePackageJson', () => {
|
||||
xo: '^0.23.0',
|
||||
consola: '^2.2.6',
|
||||
fontfaceobserver: '^2.0.13',
|
||||
'styled-components': '^4.1.1'
|
||||
'styled-components': '^4.1.1',
|
||||
},
|
||||
dependencies: {
|
||||
'next-server': 'v7.0.2-canary.49',
|
||||
react: '^16.6.3',
|
||||
'react-dom': '^16.6.3'
|
||||
'react-dom': '^16.6.3',
|
||||
},
|
||||
xo: {
|
||||
extends: 'xo-react',
|
||||
@@ -298,15 +282,15 @@ describe('normalizePackageJson', () => {
|
||||
'test',
|
||||
'pages/_document.js',
|
||||
'pages/index.js',
|
||||
'pages/home.js'
|
||||
'pages/home.js',
|
||||
],
|
||||
rules: {
|
||||
'react/no-unescaped-entities': null
|
||||
}
|
||||
'react/no-unescaped-entities': null,
|
||||
},
|
||||
},
|
||||
jest: {
|
||||
testEnvironment: 'node'
|
||||
}
|
||||
testEnvironment: 'node',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@now/node",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://zeit.co/docs/v2/deployments/official-builders/node-js-now-node",
|
||||
@@ -11,7 +11,8 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "./build.sh",
|
||||
"test-integration-once": "jest --env node --verbose --runInBand",
|
||||
"test-unit": "jest --env node --verbose --runInBand test/helpers.test.js",
|
||||
"test-integration-once": "jest --env node --verbose --runInBand test/integration.test.js",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -13,13 +13,14 @@ function getBodyParser(req: NowRequest, body: Buffer) {
|
||||
if (!req.headers['content-type']) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { parse: parseContentType } = require('content-type');
|
||||
const { type } = parseContentType(req.headers['content-type']);
|
||||
|
||||
if (type === 'application/json') {
|
||||
try {
|
||||
return JSON.parse(body.toString());
|
||||
const str = body.toString();
|
||||
return str ? JSON.parse(str) : {};
|
||||
} catch (error) {
|
||||
throw new ApiError(400, 'Invalid JSON');
|
||||
}
|
||||
@@ -30,6 +31,7 @@ function getBodyParser(req: NowRequest, body: Buffer) {
|
||||
}
|
||||
|
||||
if (type === 'application/x-www-form-urlencoded') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { parse: parseQS } = require('querystring');
|
||||
// note: querystring.parse does not produce an iterable object
|
||||
// https://nodejs.org/api/querystring.html#querystring_querystring_parse_str_sep_eq_options
|
||||
@@ -46,6 +48,7 @@ function getBodyParser(req: NowRequest, body: Buffer) {
|
||||
|
||||
function getQueryParser({ url = '/' }: NowRequest) {
|
||||
return function parseQuery(): NowRequestQuery {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { URL } = require('url');
|
||||
// we provide a placeholder base url because we only want searchParams
|
||||
const params = new URL(url, 'https://n').searchParams;
|
||||
@@ -67,6 +70,7 @@ function getCookieParser(req: NowRequest) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { parse } = require('cookie');
|
||||
return parse(Array.isArray(header) ? header.join(';') : header);
|
||||
};
|
||||
@@ -78,6 +82,7 @@ function status(res: NowResponse, statusCode: number): NowResponse {
|
||||
}
|
||||
|
||||
function setCharset(type: string, charset: string) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { parse, format } = require('content-type');
|
||||
const parsed = parse(type);
|
||||
parsed.parameters.charset = charset;
|
||||
@@ -85,6 +90,7 @@ function setCharset(type: string, charset: string) {
|
||||
}
|
||||
|
||||
function createETag(body: any, encoding: 'utf8' | undefined) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const etag = require('etag');
|
||||
const buf = !Buffer.isBuffer(body) ? Buffer.from(body, encoding) : body;
|
||||
return etag(buf, { weak: true });
|
||||
|
||||
@@ -25,6 +25,12 @@
|
||||
"body": { "who": "john" },
|
||||
"mustContain": "hello john:RANDOMNESS_PLACEHOLDER"
|
||||
},
|
||||
{
|
||||
"path": "/",
|
||||
"method": "POST",
|
||||
"headers": { "Content-Type": "application/json" },
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"path": "/",
|
||||
"headers": { "cookie": "who=chris" },
|
||||
|
||||
35
packages/now-node/test/helpers.test.js
vendored
35
packages/now-node/test/helpers.test.js
vendored
@@ -21,7 +21,7 @@ async function fetchWithProxyReq(_url, opts = {}) {
|
||||
|
||||
return fetch(_url, {
|
||||
...opts,
|
||||
headers: { ...opts.headers, 'x-now-bridge-request-id': '2' }
|
||||
headers: { ...opts.headers, 'x-now-bridge-request-id': '2' },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ describe('all helpers', () => {
|
||||
['body', 0],
|
||||
['status', 1],
|
||||
['send', 1],
|
||||
['json', 1]
|
||||
['json', 1],
|
||||
];
|
||||
|
||||
test('should not recalculate req properties twice', async () => {
|
||||
@@ -83,7 +83,7 @@ describe('all helpers', () => {
|
||||
await fetchWithProxyReq(`${url}/?who=bill`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ who: 'mike' }),
|
||||
headers: { 'content-type': 'application/json', cookie: 'who=jim' }
|
||||
headers: { 'content-type': 'application/json', cookie: 'who=jim' },
|
||||
});
|
||||
|
||||
// here we test that bodySpy is called twice with exactly the same arguments
|
||||
@@ -137,7 +137,7 @@ describe('req.query', () => {
|
||||
|
||||
expect(mockListener.mock.calls[0][0].query).toMatchObject({
|
||||
who: 'bill',
|
||||
where: 'us'
|
||||
where: 'us',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -152,13 +152,13 @@ describe('req.cookies', () => {
|
||||
test('req.cookies should reflect req.cookie header', async () => {
|
||||
await fetchWithProxyReq(url, {
|
||||
headers: {
|
||||
cookie: 'who=bill; where=us'
|
||||
}
|
||||
cookie: 'who=bill; where=us',
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockListener.mock.calls[0][0].cookies).toMatchObject({
|
||||
who: 'bill',
|
||||
where: 'us'
|
||||
where: 'us',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -172,7 +172,7 @@ describe('req.body', () => {
|
||||
test('req.body should be undefined if content-type is not defined', async () => {
|
||||
await fetchWithProxyReq(url, {
|
||||
method: 'POST',
|
||||
body: 'hello'
|
||||
body: 'hello',
|
||||
});
|
||||
expect(mockListener.mock.calls[0][0].body).toBe(undefined);
|
||||
});
|
||||
@@ -181,7 +181,7 @@ describe('req.body', () => {
|
||||
await fetchWithProxyReq(url, {
|
||||
method: 'POST',
|
||||
body: 'hello',
|
||||
headers: { 'content-type': 'text/plain' }
|
||||
headers: { 'content-type': 'text/plain' },
|
||||
});
|
||||
|
||||
expect(mockListener.mock.calls[0][0].body).toBe('hello');
|
||||
@@ -191,7 +191,7 @@ describe('req.body', () => {
|
||||
await fetchWithProxyReq(url, {
|
||||
method: 'POST',
|
||||
body: 'hello',
|
||||
headers: { 'content-type': 'application/octet-stream' }
|
||||
headers: { 'content-type': 'application/octet-stream' },
|
||||
});
|
||||
|
||||
const [{ body }] = mockListener.mock.calls[0];
|
||||
@@ -208,7 +208,7 @@ describe('req.body', () => {
|
||||
await fetchWithProxyReq(url, {
|
||||
method: 'POST',
|
||||
body: qs.encode(obj),
|
||||
headers: { 'content-type': 'application/x-www-form-urlencoded' }
|
||||
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
||||
});
|
||||
|
||||
expect(mockListener.mock.calls[0][0].body).toMatchObject(obj);
|
||||
@@ -217,19 +217,19 @@ describe('req.body', () => {
|
||||
test('req.body should be an object when content-type is `application/json`', async () => {
|
||||
const json = {
|
||||
who: 'bill',
|
||||
where: 'us'
|
||||
where: 'us',
|
||||
};
|
||||
|
||||
await fetchWithProxyReq(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(json),
|
||||
headers: { 'content-type': 'application/json' }
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
|
||||
expect(mockListener.mock.calls[0][0].body).toMatchObject(json);
|
||||
});
|
||||
|
||||
test('should throw error when body is empty and content-type is `application/json`', async () => {
|
||||
test('should work when body is empty and content-type is `application/json`', async () => {
|
||||
mockListener.mockImplementation((req, res) => {
|
||||
console.log(req.body);
|
||||
res.end();
|
||||
@@ -238,10 +238,11 @@ describe('req.body', () => {
|
||||
const res = await fetchWithProxyReq(url, {
|
||||
method: 'POST',
|
||||
body: '',
|
||||
headers: { 'content-type': 'application/json' }
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toMatchObject({});
|
||||
});
|
||||
|
||||
test('should be able to try/catch parse errors', async () => {
|
||||
@@ -260,7 +261,7 @@ describe('req.body', () => {
|
||||
await fetchWithProxyReq(url, {
|
||||
method: 'POST',
|
||||
body: '{"wrong":"json"',
|
||||
headers: { 'content-type': 'application/json' }
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
|
||||
expect(bodySpy).toHaveBeenCalled();
|
||||
|
||||
@@ -3,7 +3,7 @@ const path = require('path');
|
||||
|
||||
const {
|
||||
packAndDeploy,
|
||||
testDeployment
|
||||
testDeployment,
|
||||
} = require('../../../test/lib/deployment/test-deployment.js');
|
||||
|
||||
jest.setTimeout(4 * 60 * 1000);
|
||||
@@ -52,16 +52,26 @@ if 'handler' in __now_variables or 'Handler' in __now_variables:
|
||||
):
|
||||
body = base64.b64decode(body)
|
||||
|
||||
request_body = body.encode('utf-8') if isinstance(body, str) else body
|
||||
conn = http.client.HTTPConnection('0.0.0.0', port)
|
||||
conn.request(method, path, headers=headers, body=body)
|
||||
conn.request(method, path, headers=headers, body=request_body)
|
||||
res = conn.getresponse()
|
||||
data = res.read().decode('utf-8')
|
||||
|
||||
return {
|
||||
return_dict = {
|
||||
'statusCode': res.status,
|
||||
'headers': format_headers(res.headers),
|
||||
'body': data,
|
||||
}
|
||||
|
||||
data = res.read()
|
||||
|
||||
try:
|
||||
return_dict['body'] = data.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
return_dict['body'] = base64.b64encode(data).decode('utf-8')
|
||||
return_dict['encoding'] = 'base64'
|
||||
|
||||
return return_dict
|
||||
|
||||
elif 'app' in __now_variables:
|
||||
if (
|
||||
not inspect.iscoroutinefunction(__NOW_HANDLER_FILENAME.app) and
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@now/python",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.3",
|
||||
"main": "./dist/index.js",
|
||||
"license": "MIT",
|
||||
"homepage": "https://zeit.co/docs/v2/deployments/official-builders/python-now-python",
|
||||
|
||||
@@ -16,6 +16,14 @@ import {
|
||||
|
||||
async function pipInstall(pipPath: string, workDir: string, ...args: string[]) {
|
||||
const target = '.';
|
||||
// See: https://github.com/pypa/pip/issues/4222#issuecomment-417646535
|
||||
//
|
||||
// Disable installing to the Python user install directory, which is
|
||||
// the default behavior on Debian systems and causes error:
|
||||
//
|
||||
// distutils.errors.DistutilsOptionError: can't combine user with
|
||||
// prefix, exec_prefix/home, or install_(plat)base
|
||||
process.env.PIP_USER = '0';
|
||||
debug(
|
||||
`Running "pip install --disable-pip-version-check --target ${target} --upgrade ${args.join(
|
||||
' '
|
||||
|
||||
20
packages/now-python/test/fixtures/14-unicode-handler/index.py
vendored
Normal file
20
packages/now-python/test/fixtures/14-unicode-handler/index.py
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
from http.server import BaseHTTPRequestHandler
|
||||
import json
|
||||
|
||||
class handler(BaseHTTPRequestHandler):
|
||||
|
||||
def do_POST(self):
|
||||
post_body = json.loads(self.rfile.read(int(self.headers['content-length'])).decode('utf-8'))
|
||||
name = post_body.get('name', 'someone')
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
response_data = {'greeting': f'hello, {name}'}
|
||||
self.wfile.write(json.dumps(response_data).encode('utf-8'))
|
||||
return
|
||||
|
||||
def do_GET(self):
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write('ok'.encode('utf-8'))
|
||||
return
|
||||
19
packages/now-python/test/fixtures/14-unicode-handler/now.json
vendored
Normal file
19
packages/now-python/test/fixtures/14-unicode-handler/now.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [
|
||||
{
|
||||
"src": "*.py",
|
||||
"use": "@now/python"
|
||||
}
|
||||
],
|
||||
"probes": [
|
||||
{
|
||||
"path": "/",
|
||||
"method": "POST",
|
||||
"body": {
|
||||
"name": "Χριστοφορε"
|
||||
},
|
||||
"status": 200
|
||||
}
|
||||
]
|
||||
}
|
||||
19
packages/now-python/test/fixtures/14-unicode-handler/probe.js
vendored
Normal file
19
packages/now-python/test/fixtures/14-unicode-handler/probe.js
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
module.exports = async function({ deploymentUrl, fetch, randomness }) {
|
||||
const nowjson = require('./now.json');
|
||||
const probe = nowjson.probes[0];
|
||||
const probeUrl = `https://${deploymentUrl}${probe.path}`;
|
||||
const resp = await fetch(probeUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(probe.body),
|
||||
});
|
||||
|
||||
const text = await resp.text();
|
||||
const respBody = JSON.parse(text);
|
||||
|
||||
if (respBody.greeting !== 'hello, Χριστοφορε') {
|
||||
throw new Error(`unexpected response: ${respBody}`);
|
||||
}
|
||||
};
|
||||
11
packages/now-python/test/fixtures/15-binary-handler/index.py
vendored
Normal file
11
packages/now-python/test/fixtures/15-binary-handler/index.py
vendored
Normal 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", "image/png")
|
||||
self.end_headers()
|
||||
with open("zeit-white-triangle.png", "rb") as image:
|
||||
self.wfile.write(image.read())
|
||||
return
|
||||
16
packages/now-python/test/fixtures/15-binary-handler/now.json
vendored
Normal file
16
packages/now-python/test/fixtures/15-binary-handler/now.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [
|
||||
{
|
||||
"src": "*.py",
|
||||
"use": "@now/python"
|
||||
}
|
||||
],
|
||||
"probes": [
|
||||
{
|
||||
"path": "/",
|
||||
"method": "GET",
|
||||
"status": 200
|
||||
}
|
||||
]
|
||||
}
|
||||
19
packages/now-python/test/fixtures/15-binary-handler/probe.js
vendored
Normal file
19
packages/now-python/test/fixtures/15-binary-handler/probe.js
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = async function({ deploymentUrl, fetch, randomness }) {
|
||||
const nowjson = require('./now.json');
|
||||
const probe = nowjson.probes[0];
|
||||
const probeUrl = `https://${deploymentUrl}${probe.path}`;
|
||||
const resp = await fetch(probeUrl);
|
||||
|
||||
const bytes = await resp.arrayBuffer();
|
||||
|
||||
const image = fs.readFileSync(
|
||||
path.join(__dirname, 'zeit-white-triangle.png')
|
||||
);
|
||||
|
||||
if (!image.equals(new Uint8Array(bytes))) {
|
||||
throw new Error(`unexpected response: ${bytes}`);
|
||||
}
|
||||
};
|
||||
BIN
packages/now-python/test/fixtures/15-binary-handler/zeit-white-triangle.png
vendored
Normal file
BIN
packages/now-python/test/fixtures/15-binary-handler/zeit-white-triangle.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 383 B |
@@ -6,14 +6,21 @@ Route validation utilities
|
||||
|
||||
`yarn add @now/routing-utils`
|
||||
|
||||
exports.normalizeRoutes:
|
||||
`(routes: Array<Route> | null) => { routes: Array<Route> | null; error: NowError | null }`
|
||||
```ts
|
||||
import { normalizeRoutes } from '@now/routing-utils';
|
||||
|
||||
exports.schema:
|
||||
const { routes, error } = normalizeRoutes(inputRoutes);
|
||||
|
||||
if (error) {
|
||||
console.log(error.code, error.message);
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
import { routesSchema } from '@now/routing-utils';
|
||||
|
||||
```js
|
||||
const ajv = new Ajv();
|
||||
const validate = ajv.compile(schema);
|
||||
const validate = ajv.compile(routesSchema);
|
||||
const valid = validate([{ src: '/about', dest: '/about.html' }]);
|
||||
|
||||
if (!valid) console.log(validate.errors);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@now/routing-utils",
|
||||
"version": "1.2.4",
|
||||
"description": "ZEIT Now route validation utilities",
|
||||
"version": "1.3.1",
|
||||
"description": "ZEIT Now routing utilities",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
@@ -19,6 +19,9 @@
|
||||
"watch": "tsc --watch",
|
||||
"test-unit": "jest --env node --verbose --runInBand"
|
||||
},
|
||||
"dependencies": {
|
||||
"path-to-regexp": "3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ajv": "^6.0.0",
|
||||
"typescript": "3.5.2"
|
||||
|
||||
@@ -1,36 +1,25 @@
|
||||
export type NowError = {
|
||||
code: string;
|
||||
message: string;
|
||||
errors: {
|
||||
message: string;
|
||||
src?: string;
|
||||
handle?: string;
|
||||
}[];
|
||||
sha?: string; // File errors
|
||||
};
|
||||
|
||||
export type Source = {
|
||||
src: string;
|
||||
dest?: string;
|
||||
headers?: {};
|
||||
methods?: string[];
|
||||
continue?: boolean;
|
||||
status?: number;
|
||||
};
|
||||
|
||||
export type Handler = {
|
||||
handle: string;
|
||||
};
|
||||
|
||||
export type Route = Source | Handler;
|
||||
export * from './schemas';
|
||||
export * from './types';
|
||||
import {
|
||||
Route,
|
||||
Handler,
|
||||
NormalizedRoutes,
|
||||
GetRoutesProps,
|
||||
NowError,
|
||||
} from './types';
|
||||
import {
|
||||
convertCleanUrls,
|
||||
convertRewrites,
|
||||
convertRedirects,
|
||||
convertHeaders,
|
||||
convertTrailingSlash,
|
||||
} from './superstatic';
|
||||
|
||||
export function isHandler(route: Route): route is Handler {
|
||||
return typeof (route as Handler).handle !== 'undefined';
|
||||
}
|
||||
|
||||
export function normalizeRoutes(
|
||||
inputRoutes: Array<Route> | null
|
||||
): { routes: Array<Route> | null; error: NowError | null } {
|
||||
export function normalizeRoutes(inputRoutes: Route[] | null): NormalizedRoutes {
|
||||
if (!inputRoutes || inputRoutes.length === 0) {
|
||||
return { routes: inputRoutes, error: null };
|
||||
}
|
||||
@@ -48,19 +37,19 @@ export function normalizeRoutes(
|
||||
if (Object.keys(route).length !== 1) {
|
||||
errors.push({
|
||||
message: `Cannot have any other keys when handle is used (handle: ${route.handle})`,
|
||||
handle: route.handle
|
||||
handle: route.handle,
|
||||
});
|
||||
}
|
||||
if (!['filesystem'].includes(route.handle)) {
|
||||
errors.push({
|
||||
message: `This is not a valid handler (handle: ${route.handle})`,
|
||||
handle: route.handle
|
||||
handle: route.handle,
|
||||
});
|
||||
}
|
||||
if (handling.includes(route.handle)) {
|
||||
errors.push({
|
||||
message: `You can only handle something once (handle: ${route.handle})`,
|
||||
handle: route.handle
|
||||
handle: route.handle,
|
||||
});
|
||||
} else {
|
||||
handling.push(route.handle);
|
||||
@@ -76,23 +65,26 @@ export function normalizeRoutes(
|
||||
route.src = `${route.src}$`;
|
||||
}
|
||||
|
||||
// Route src should strip escaped forward slash, its not special
|
||||
route.src = route.src.replace(/\\\//g, '/');
|
||||
|
||||
try {
|
||||
// This feels a bit dangerous if there would be a vulnerability in RegExp.
|
||||
new RegExp(route.src);
|
||||
} catch (err) {
|
||||
errors.push({
|
||||
message: `Invalid regular expression: "${route.src}"`,
|
||||
src: route.src
|
||||
src: route.src,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
errors.push({
|
||||
message: 'A route must set either handle or src'
|
||||
message: 'A route must set either handle or src',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const error =
|
||||
const error: NowError | null =
|
||||
errors.length > 0
|
||||
? {
|
||||
code: 'invalid_routes',
|
||||
@@ -101,63 +93,79 @@ export function normalizeRoutes(
|
||||
null,
|
||||
2
|
||||
)}`,
|
||||
errors
|
||||
errors,
|
||||
}
|
||||
: null;
|
||||
|
||||
return { routes, error };
|
||||
}
|
||||
|
||||
/**
|
||||
* An ajv schema for the routes array
|
||||
*/
|
||||
export const schema = {
|
||||
type: 'array',
|
||||
maxItems: 1024,
|
||||
items: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
src: {
|
||||
type: 'string',
|
||||
maxLength: 4096
|
||||
},
|
||||
dest: {
|
||||
type: 'string',
|
||||
maxLength: 4096
|
||||
},
|
||||
methods: {
|
||||
type: 'array',
|
||||
maxItems: 10,
|
||||
items: {
|
||||
type: 'string',
|
||||
maxLength: 32
|
||||
}
|
||||
},
|
||||
headers: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
minProperties: 1,
|
||||
maxProperties: 100,
|
||||
patternProperties: {
|
||||
'^.{1,256}$': {
|
||||
type: 'string',
|
||||
maxLength: 4096
|
||||
}
|
||||
}
|
||||
},
|
||||
handle: {
|
||||
type: 'string',
|
||||
maxLength: 32
|
||||
},
|
||||
continue: {
|
||||
type: 'boolean'
|
||||
},
|
||||
status: {
|
||||
type: 'integer',
|
||||
minimum: 100,
|
||||
maximum: 999
|
||||
}
|
||||
export function getTransformedRoutes({
|
||||
nowConfig,
|
||||
filePaths,
|
||||
}: GetRoutesProps): NormalizedRoutes {
|
||||
const { cleanUrls, rewrites, redirects, headers, trailingSlash } = nowConfig;
|
||||
let { routes } = nowConfig;
|
||||
const errors: { message: string }[] = [];
|
||||
if (typeof routes !== 'undefined') {
|
||||
if (typeof cleanUrls !== 'undefined') {
|
||||
errors.push({
|
||||
message: 'Cannot define both `routes` and `cleanUrls`',
|
||||
});
|
||||
}
|
||||
if (typeof trailingSlash !== 'undefined') {
|
||||
errors.push({
|
||||
message: 'Cannot define both `routes` and `trailingSlash`',
|
||||
});
|
||||
}
|
||||
if (typeof redirects !== 'undefined') {
|
||||
errors.push({
|
||||
message: 'Cannot define both `routes` and `redirects`',
|
||||
});
|
||||
}
|
||||
if (typeof headers !== 'undefined') {
|
||||
errors.push({
|
||||
message: 'Cannot define both `routes` and `headers`',
|
||||
});
|
||||
}
|
||||
if (typeof rewrites !== 'undefined') {
|
||||
errors.push({
|
||||
message: 'Cannot define both `routes` and `rewrites`',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
routes = [];
|
||||
if (cleanUrls) {
|
||||
const clean = convertCleanUrls(filePaths);
|
||||
routes.push(...clean.redirects);
|
||||
}
|
||||
if (typeof trailingSlash !== 'undefined') {
|
||||
routes.push(...convertTrailingSlash(trailingSlash));
|
||||
}
|
||||
if (typeof redirects !== 'undefined') {
|
||||
routes.push(...convertRedirects(redirects));
|
||||
}
|
||||
if (typeof headers !== 'undefined') {
|
||||
routes.push(...convertHeaders(headers));
|
||||
}
|
||||
if (typeof rewrites !== 'undefined') {
|
||||
routes.push({ handle: 'filesystem' });
|
||||
routes.push(...convertRewrites(rewrites));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (errors.length > 0) {
|
||||
const error = {
|
||||
code: 'invalid_routes',
|
||||
message: `One or more invalid routes were found: \n${JSON.stringify(
|
||||
errors,
|
||||
null,
|
||||
2
|
||||
)}`,
|
||||
errors,
|
||||
};
|
||||
return { routes: [], error };
|
||||
}
|
||||
|
||||
return normalizeRoutes(routes);
|
||||
}
|
||||
|
||||
142
packages/now-routing-utils/src/schemas.ts
Normal file
142
packages/now-routing-utils/src/schemas.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* An ajv schema for the routes array
|
||||
*/
|
||||
export const routesSchema = {
|
||||
type: 'array',
|
||||
maxItems: 1024,
|
||||
items: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
src: {
|
||||
type: 'string',
|
||||
maxLength: 4096,
|
||||
},
|
||||
dest: {
|
||||
type: 'string',
|
||||
maxLength: 4096,
|
||||
},
|
||||
methods: {
|
||||
type: 'array',
|
||||
maxItems: 10,
|
||||
items: {
|
||||
type: 'string',
|
||||
maxLength: 32,
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
minProperties: 1,
|
||||
maxProperties: 100,
|
||||
patternProperties: {
|
||||
'^.{1,256}$': {
|
||||
type: 'string',
|
||||
maxLength: 4096,
|
||||
},
|
||||
},
|
||||
},
|
||||
handle: {
|
||||
type: 'string',
|
||||
maxLength: 32,
|
||||
},
|
||||
continue: {
|
||||
type: 'boolean',
|
||||
},
|
||||
status: {
|
||||
type: 'integer',
|
||||
minimum: 100,
|
||||
maximum: 999,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const rewritesSchema = {
|
||||
type: 'array',
|
||||
maxItems: 1024,
|
||||
items: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['source', 'destination'],
|
||||
properties: {
|
||||
source: {
|
||||
type: 'string',
|
||||
maxLength: 4096,
|
||||
},
|
||||
destination: {
|
||||
type: 'string',
|
||||
maxLength: 4096,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const redirectsSchema = {
|
||||
title: 'Redirects',
|
||||
type: 'array',
|
||||
maxItems: 1024,
|
||||
items: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['source', 'destination'],
|
||||
properties: {
|
||||
source: {
|
||||
type: 'string',
|
||||
maxLength: 4096,
|
||||
},
|
||||
destination: {
|
||||
type: 'string',
|
||||
maxLength: 4096,
|
||||
},
|
||||
statusCode: {
|
||||
type: 'integer',
|
||||
minimum: 100,
|
||||
maximum: 999,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const headersSchema = {
|
||||
type: 'array',
|
||||
maxItems: 1024,
|
||||
items: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['source', 'headers'],
|
||||
properties: {
|
||||
source: {
|
||||
type: 'string',
|
||||
maxLength: 4096,
|
||||
},
|
||||
headers: {
|
||||
type: 'array',
|
||||
maxItems: 1024,
|
||||
items: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['key', 'value'],
|
||||
properties: {
|
||||
key: {
|
||||
type: 'string',
|
||||
maxLength: 4096,
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
maxLength: 4096,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const cleanUrlsSchema = {
|
||||
type: 'boolean',
|
||||
};
|
||||
|
||||
export const trailingSlashSchema = {
|
||||
type: 'boolean',
|
||||
};
|
||||
122
packages/now-routing-utils/src/superstatic.ts
Normal file
122
packages/now-routing-utils/src/superstatic.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* This converts Superstatic configuration to Now.json Routes
|
||||
* See https://github.com/firebase/superstatic#configuration
|
||||
*/
|
||||
|
||||
import pathToRegexp from 'path-to-regexp';
|
||||
import { Route, NowRedirect, NowRewrite, NowHeader } from './types';
|
||||
|
||||
export function convertCleanUrls(
|
||||
filePaths: string[]
|
||||
): { redirects: Route[]; rewrites: Route[] } {
|
||||
const htmlFiles = filePaths
|
||||
.map(toRoute)
|
||||
.filter(f => f.endsWith('.html'))
|
||||
.map(f => ({
|
||||
html: f,
|
||||
clean: f.slice(0, -5),
|
||||
}));
|
||||
|
||||
const redirects: Route[] = htmlFiles.map(o => ({
|
||||
src: o.html,
|
||||
headers: { Location: o.clean },
|
||||
status: 301,
|
||||
}));
|
||||
|
||||
const rewrites: Route[] = htmlFiles.map(o => ({
|
||||
src: o.clean,
|
||||
dest: o.html,
|
||||
continue: true,
|
||||
}));
|
||||
|
||||
return { redirects, rewrites };
|
||||
}
|
||||
|
||||
export function convertRedirects(redirects: NowRedirect[]): Route[] {
|
||||
return redirects.map(r => {
|
||||
const { src, segments } = sourceToRegex(r.source);
|
||||
const loc = replaceSegments(segments, r.destination);
|
||||
return {
|
||||
src,
|
||||
headers: { Location: loc },
|
||||
status: r.statusCode || 307,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function convertRewrites(rewrites: NowRewrite[]): Route[] {
|
||||
const routes: Route[] = rewrites.map(r => {
|
||||
const { src, segments } = sourceToRegex(r.source);
|
||||
const dest = replaceSegments(segments, r.destination);
|
||||
return { src, dest, continue: true };
|
||||
});
|
||||
return routes;
|
||||
}
|
||||
|
||||
export function convertHeaders(headers: NowHeader[]): Route[] {
|
||||
return headers.map(h => {
|
||||
const obj: { [key: string]: string } = {};
|
||||
h.headers.forEach(kv => {
|
||||
obj[kv.key] = kv.value;
|
||||
});
|
||||
return {
|
||||
src: h.source,
|
||||
headers: obj,
|
||||
continue: true,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function convertTrailingSlash(enable: boolean): Route[] {
|
||||
const routes: Route[] = [];
|
||||
if (enable) {
|
||||
routes.push({
|
||||
src: '^(.*[^\\/])$',
|
||||
headers: { Location: '$1/' },
|
||||
status: 307,
|
||||
});
|
||||
} else {
|
||||
routes.push({
|
||||
src: '^(.*)\\/$',
|
||||
headers: { Location: '$1' },
|
||||
status: 307,
|
||||
});
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
|
||||
function sourceToRegex(source: string): { src: string; segments: string[] } {
|
||||
const keys: pathToRegexp.Key[] = [];
|
||||
const r = pathToRegexp(source, keys, { strict: true });
|
||||
const segments = keys.map(k => k.name).filter(isString);
|
||||
return { src: r.source, segments };
|
||||
}
|
||||
|
||||
function isString(key: any): key is string {
|
||||
return typeof key === 'string';
|
||||
}
|
||||
|
||||
function replaceSegments(segments: string[], destination: string): string {
|
||||
if (destination.includes(':')) {
|
||||
segments.forEach((name, index) => {
|
||||
const r = new RegExp(':' + name, 'g');
|
||||
destination = destination.replace(r, toSegmentDest(index));
|
||||
});
|
||||
} else if (segments.length > 0) {
|
||||
let prefix = '?';
|
||||
segments.forEach((name, index) => {
|
||||
destination += `${prefix}${name}=${toSegmentDest(index)}`;
|
||||
prefix = '&';
|
||||
});
|
||||
}
|
||||
return destination;
|
||||
}
|
||||
|
||||
function toSegmentDest(index: number): string {
|
||||
const i = index + 1; // js is base 0, regex is base 1
|
||||
return '$' + i.toString();
|
||||
}
|
||||
|
||||
function toRoute(filePath: string): string {
|
||||
return filePath.startsWith('/') ? filePath : '/' + filePath;
|
||||
}
|
||||
67
packages/now-routing-utils/src/types.ts
Normal file
67
packages/now-routing-utils/src/types.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
export type NowError = {
|
||||
code: string;
|
||||
message: string;
|
||||
errors: {
|
||||
message: string;
|
||||
src?: string;
|
||||
handle?: string;
|
||||
}[];
|
||||
sha?: string; // File errors
|
||||
};
|
||||
|
||||
export type Source = {
|
||||
src: string;
|
||||
dest?: string;
|
||||
headers?: { [name: string]: string };
|
||||
methods?: string[];
|
||||
continue?: boolean;
|
||||
status?: number;
|
||||
};
|
||||
|
||||
export type Handler = {
|
||||
handle: string;
|
||||
};
|
||||
|
||||
export type Route = Source | Handler;
|
||||
|
||||
export type NormalizedRoutes = {
|
||||
routes: Route[] | null;
|
||||
error: NowError | null;
|
||||
};
|
||||
|
||||
export interface GetRoutesProps {
|
||||
nowConfig: NowConfig;
|
||||
filePaths: string[];
|
||||
}
|
||||
|
||||
export interface NowConfig {
|
||||
name?: string;
|
||||
version?: number;
|
||||
routes?: Route[];
|
||||
cleanUrls?: boolean;
|
||||
rewrites?: NowRewrite[];
|
||||
redirects?: NowRedirect[];
|
||||
headers?: NowHeader[];
|
||||
trailingSlash?: boolean;
|
||||
}
|
||||
|
||||
export interface NowRewrite {
|
||||
source: string;
|
||||
destination: string;
|
||||
}
|
||||
|
||||
export interface NowRedirect {
|
||||
source: string;
|
||||
destination: string;
|
||||
statusCode?: number;
|
||||
}
|
||||
|
||||
export interface NowHeader {
|
||||
source: string;
|
||||
headers: NowHeaderKeyValue[];
|
||||
}
|
||||
|
||||
export interface NowHeaderKeyValue {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
140
packages/now-routing-utils/test/index.spec.js
vendored
140
packages/now-routing-utils/test/index.spec.js
vendored
@@ -1,18 +1,28 @@
|
||||
const assert = require('assert');
|
||||
const Ajv = require('ajv');
|
||||
const { normalizeRoutes, isHandler, schema } = require('../dist');
|
||||
const {
|
||||
normalizeRoutes,
|
||||
isHandler,
|
||||
routesSchema,
|
||||
rewritesSchema,
|
||||
redirectsSchema,
|
||||
headersSchema,
|
||||
cleanUrlsSchema,
|
||||
trailingSlashSchema,
|
||||
getTransformedRoutes,
|
||||
} = require('../');
|
||||
|
||||
const ajv = new Ajv();
|
||||
const assertValid = (routes) => {
|
||||
const assertValid = (data, schema = routesSchema) => {
|
||||
const validate = ajv.compile(schema);
|
||||
const valid = validate(routes);
|
||||
const valid = validate(data);
|
||||
|
||||
if (!valid) console.log(validate.errors);
|
||||
assert.equal(valid, true);
|
||||
};
|
||||
const assertError = (routes, errors) => {
|
||||
const assertError = (data, errors, schema = routesSchema) => {
|
||||
const validate = ajv.compile(schema);
|
||||
const valid = validate(routes);
|
||||
const valid = validate(data);
|
||||
|
||||
assert.equal(valid, false);
|
||||
assert.deepEqual(validate.errors, errors);
|
||||
@@ -50,7 +60,6 @@ describe('normalizeRoutes', () => {
|
||||
|
||||
test('normalizes src', () => {
|
||||
const expected = '^/about$';
|
||||
const expected2 = '^\\/about$';
|
||||
const sources = [
|
||||
{ src: '/about' },
|
||||
{ src: '/about$' },
|
||||
@@ -70,15 +79,13 @@ describe('normalizeRoutes', () => {
|
||||
assert.notEqual(normalized.routes, null);
|
||||
|
||||
if (normalized.routes) {
|
||||
normalized.routes.forEach((route) => {
|
||||
normalized.routes.forEach(route => {
|
||||
if (isHandler(route)) {
|
||||
assert.fail(
|
||||
`Normalizer returned: { handle: ${
|
||||
route.handle
|
||||
} } instead of { src: ${expected} }`,
|
||||
`Normalizer returned: { handle: ${route.handle} } instead of { src: ${expected} }`
|
||||
);
|
||||
} else {
|
||||
assert.ok(route.src === expected || route.src === expected2);
|
||||
assert.strictEqual(route.src, expected);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -147,7 +154,7 @@ describe('normalizeRoutes', () => {
|
||||
message: `One or more invalid routes were found: \n${JSON.stringify(
|
||||
errors,
|
||||
null,
|
||||
2,
|
||||
2
|
||||
)}`,
|
||||
errors,
|
||||
});
|
||||
@@ -202,7 +209,7 @@ describe('normalizeRoutes', () => {
|
||||
},
|
||||
schemaPath: '#/items/properties/src/type',
|
||||
},
|
||||
],
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -224,7 +231,7 @@ describe('normalizeRoutes', () => {
|
||||
},
|
||||
schemaPath: '#/items/properties/dest/type',
|
||||
},
|
||||
],
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -246,7 +253,7 @@ describe('normalizeRoutes', () => {
|
||||
},
|
||||
schemaPath: '#/items/properties/methods/type',
|
||||
},
|
||||
],
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -268,7 +275,7 @@ describe('normalizeRoutes', () => {
|
||||
},
|
||||
schemaPath: '#/items/properties/methods/items/type',
|
||||
},
|
||||
],
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -290,7 +297,7 @@ describe('normalizeRoutes', () => {
|
||||
},
|
||||
schemaPath: '#/items/properties/headers/type',
|
||||
},
|
||||
],
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -315,7 +322,7 @@ describe('normalizeRoutes', () => {
|
||||
schemaPath:
|
||||
'#/items/properties/headers/patternProperties/%5E.%7B1%2C256%7D%24/type',
|
||||
},
|
||||
],
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -337,7 +344,7 @@ describe('normalizeRoutes', () => {
|
||||
},
|
||||
schemaPath: '#/items/properties/handle/type',
|
||||
},
|
||||
],
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -359,7 +366,7 @@ describe('normalizeRoutes', () => {
|
||||
},
|
||||
schemaPath: '#/items/properties/continue/type',
|
||||
},
|
||||
],
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -381,7 +388,7 @@ describe('normalizeRoutes', () => {
|
||||
},
|
||||
schemaPath: '#/items/properties/status/type',
|
||||
},
|
||||
],
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -403,7 +410,96 @@ describe('normalizeRoutes', () => {
|
||||
},
|
||||
schemaPath: '#/items/additionalProperties',
|
||||
},
|
||||
],
|
||||
]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTransformedRoutes', () => {
|
||||
test('should normalize nowConfig.routes', () => {
|
||||
const nowConfig = { routes: [{ src: '/page', dest: '/page.html' }] };
|
||||
const filePaths = [];
|
||||
const actual = getTransformedRoutes({ nowConfig, filePaths });
|
||||
const expected = normalizeRoutes(nowConfig.routes);
|
||||
assert.deepEqual(actual, expected);
|
||||
assertValid(actual.routes);
|
||||
});
|
||||
|
||||
test('should normalize all redirects before rewrites', () => {
|
||||
const nowConfig = {
|
||||
cleanUrls: true,
|
||||
rewrites: [{ source: '/v1', destination: '/v2/api.py' }],
|
||||
redirects: [
|
||||
{ source: '/help', destination: '/support', statusCode: 302 },
|
||||
],
|
||||
};
|
||||
const filePaths = ['/index.html', '/support.html', '/v2/api.py'];
|
||||
const actual = getTransformedRoutes({ nowConfig, filePaths });
|
||||
const expected = [
|
||||
{
|
||||
src: '^/index.html$',
|
||||
headers: { Location: '/index' },
|
||||
status: 301,
|
||||
},
|
||||
{
|
||||
src: '^/support.html$',
|
||||
headers: { Location: '/support' },
|
||||
status: 301,
|
||||
},
|
||||
{
|
||||
src: '^/help$',
|
||||
headers: { Location: '/support' },
|
||||
status: 302,
|
||||
},
|
||||
{ handle: 'filesystem' },
|
||||
{ src: '^/v1$', dest: '/v2/api.py', continue: true },
|
||||
];
|
||||
assert.deepEqual(actual.error, null);
|
||||
assert.deepEqual(actual.routes, expected);
|
||||
assertValid(actual.routes, routesSchema);
|
||||
});
|
||||
|
||||
test('should validate schemas', () => {
|
||||
const nowConfig = {
|
||||
cleanUrls: true,
|
||||
rewrites: [
|
||||
{ source: '/page', destination: '/page.html' },
|
||||
{ source: '/home', destination: '/index.html' },
|
||||
],
|
||||
redirects: [
|
||||
{ source: '/version1', destination: '/api1.py' },
|
||||
{ source: '/version2', destination: '/api2.py', statusCode: 302 },
|
||||
],
|
||||
headers: [
|
||||
{
|
||||
source: '/(.*)',
|
||||
headers: [
|
||||
{
|
||||
key: 'Access-Control-Allow-Origin',
|
||||
value: '*',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
source: '/404',
|
||||
headers: [
|
||||
{
|
||||
key: 'Cache-Control',
|
||||
value: 'max-age=300',
|
||||
},
|
||||
{
|
||||
key: 'Set-Cookie',
|
||||
value: 'error=404',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
trailingSlashSchema: false,
|
||||
};
|
||||
assertValid(nowConfig.cleanUrls, cleanUrlsSchema);
|
||||
assertValid(nowConfig.rewrites, rewritesSchema);
|
||||
assertValid(nowConfig.redirects, redirectsSchema);
|
||||
assertValid(nowConfig.headers, headersSchema);
|
||||
assertValid(nowConfig.trailingSlashSchema, trailingSlashSchema);
|
||||
});
|
||||
});
|
||||
|
||||
246
packages/now-routing-utils/test/superstatic.spec.js
vendored
Normal file
246
packages/now-routing-utils/test/superstatic.spec.js
vendored
Normal file
@@ -0,0 +1,246 @@
|
||||
const { deepEqual } = require('assert');
|
||||
const { normalizeRoutes } = require('../');
|
||||
const {
|
||||
convertCleanUrls,
|
||||
convertRedirects,
|
||||
convertRewrites,
|
||||
convertHeaders,
|
||||
convertTrailingSlash,
|
||||
} = require('../dist/superstatic');
|
||||
|
||||
function routesToRegExps(routeArray) {
|
||||
const { routes, error } = normalizeRoutes(routeArray);
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
return routes.map(r => new RegExp(r.src));
|
||||
}
|
||||
|
||||
function assertMatches(actual, matches, isExpectingMatch) {
|
||||
routesToRegExps(actual).forEach((r, i) => {
|
||||
matches[i].forEach(text => {
|
||||
deepEqual(r.test(text), isExpectingMatch, `${text} ${r.source}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function assertRegexMatches(actual, mustMatch, mustNotMatch) {
|
||||
assertMatches(actual, mustMatch, true);
|
||||
assertMatches(actual, mustNotMatch, false);
|
||||
}
|
||||
|
||||
test('convertCleanUrls', () => {
|
||||
const { redirects, rewrites } = convertCleanUrls([
|
||||
'file.txt',
|
||||
'path/to/file.txt',
|
||||
'file.js',
|
||||
'path/to/file.js',
|
||||
'file.html',
|
||||
'path/to/file.html',
|
||||
]);
|
||||
const expectedRedirects = [
|
||||
{
|
||||
src: '/file.html',
|
||||
headers: { Location: '/file' },
|
||||
status: 301,
|
||||
},
|
||||
{
|
||||
src: '/path/to/file.html',
|
||||
headers: { Location: '/path/to/file' },
|
||||
status: 301,
|
||||
},
|
||||
];
|
||||
const expectedRewrites = [
|
||||
{ src: '/file', dest: '/file.html', continue: true },
|
||||
{ src: '/path/to/file', dest: '/path/to/file.html', continue: true },
|
||||
];
|
||||
deepEqual(redirects, expectedRedirects);
|
||||
deepEqual(rewrites, expectedRewrites);
|
||||
});
|
||||
|
||||
test('convertRedirects', () => {
|
||||
const actual = convertRedirects([
|
||||
{ source: '/some/old/path', destination: '/some/new/path' },
|
||||
{
|
||||
source: '/firebase/(.*)',
|
||||
destination: 'https://www.firebase.com',
|
||||
statusCode: 302,
|
||||
},
|
||||
{
|
||||
source: '/projects/:id/:action',
|
||||
destination: '/projects.html',
|
||||
},
|
||||
{ source: '/old/:segment/path', destination: '/new/path/:segment' },
|
||||
]);
|
||||
|
||||
const expected = [
|
||||
{
|
||||
src: '^\\/some\\/old\\/path$',
|
||||
headers: { Location: '/some/new/path' },
|
||||
status: 307,
|
||||
},
|
||||
{
|
||||
src: '^\\/firebase\\/(.*)$',
|
||||
headers: { Location: 'https://www.firebase.com' },
|
||||
status: 302,
|
||||
},
|
||||
{
|
||||
src: '^\\/projects\\/([^\\/]+?)\\/([^\\/]+?)$',
|
||||
headers: { Location: '/projects.html?id=$1&action=$2' },
|
||||
status: 307,
|
||||
},
|
||||
{
|
||||
src: '^\\/old\\/([^\\/]+?)\\/path$',
|
||||
headers: { Location: '/new/path/$1' },
|
||||
status: 307,
|
||||
},
|
||||
];
|
||||
|
||||
deepEqual(actual, expected);
|
||||
|
||||
const mustMatch = [
|
||||
['/some/old/path'],
|
||||
['/firebase/one', '/firebase/2', '/firebase/-', '/firebase/dir/sub'],
|
||||
['/projects/one/edit', '/projects/two/edit'],
|
||||
['/old/one/path', '/old/two/path'],
|
||||
];
|
||||
|
||||
const mustNotMatch = [
|
||||
['/nope'],
|
||||
['/fire', '/firebasejumper/two'],
|
||||
['/projects/edit', '/projects/two/three/delete', '/projects'],
|
||||
['/old/path', '/old/two/foo', '/old'],
|
||||
];
|
||||
|
||||
assertRegexMatches(actual, mustMatch, mustNotMatch);
|
||||
});
|
||||
|
||||
test('convertRewrites', () => {
|
||||
const actual = convertRewrites([
|
||||
{ source: '/some/old/path', destination: '/some/new/path' },
|
||||
{ source: '/firebase/(.*)', destination: 'https://www.firebase.com' },
|
||||
{ source: '/projects/:id/edit', destination: '/projects.html' },
|
||||
]);
|
||||
|
||||
const expected = [
|
||||
{ src: '^\\/some\\/old\\/path$', dest: '/some/new/path', continue: true },
|
||||
{
|
||||
src: '^\\/firebase\\/(.*)$',
|
||||
dest: 'https://www.firebase.com',
|
||||
continue: true,
|
||||
},
|
||||
{
|
||||
src: '^\\/projects\\/([^\\/]+?)\\/edit$',
|
||||
dest: '/projects.html?id=$1',
|
||||
continue: true,
|
||||
},
|
||||
];
|
||||
|
||||
deepEqual(actual, expected);
|
||||
|
||||
const mustMatch = [
|
||||
['/some/old/path'],
|
||||
['/firebase/one', '/firebase/two'],
|
||||
['/projects/one/edit', '/projects/two/edit'],
|
||||
['/old/one/path', '/old/two/path'],
|
||||
];
|
||||
|
||||
const mustNotMatch = [
|
||||
['/nope'],
|
||||
['/fire', '/firebasejumper/two'],
|
||||
['/projects/edit', '/projects/two/delete', '/projects'],
|
||||
['/old/path', '/old/two/foo', '/old'],
|
||||
];
|
||||
|
||||
assertRegexMatches(actual, mustMatch, mustNotMatch);
|
||||
});
|
||||
|
||||
test('convertHeaders', () => {
|
||||
const actual = convertHeaders([
|
||||
{
|
||||
source: '(.*)+/(.*)\\.(eot|otf|ttf|ttc|woff|font\\.css)',
|
||||
headers: [
|
||||
{
|
||||
key: 'Access-Control-Allow-Origin',
|
||||
value: '*',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
source: '404.html',
|
||||
headers: [
|
||||
{
|
||||
key: 'Cache-Control',
|
||||
value: 'max-age=300',
|
||||
},
|
||||
{
|
||||
key: 'Set-Cookie',
|
||||
value: 'error=404',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const expected = [
|
||||
{
|
||||
src: '(.*)+/(.*)\\.(eot|otf|ttf|ttc|woff|font\\.css)',
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
continue: true,
|
||||
},
|
||||
{
|
||||
src: '404.html',
|
||||
headers: { 'Cache-Control': 'max-age=300', 'Set-Cookie': 'error=404' },
|
||||
continue: true,
|
||||
},
|
||||
];
|
||||
|
||||
deepEqual(actual, expected);
|
||||
|
||||
const mustMatch = [
|
||||
['hello/world/file.eot', 'another/font.ttf', 'dir/arial.font.css'],
|
||||
['404.html'],
|
||||
];
|
||||
|
||||
const mustNotMatch = [
|
||||
['hello/file.jpg', 'hello/font-css', 'dir/arial.font-css'],
|
||||
['403.html', '500.html'],
|
||||
];
|
||||
|
||||
assertRegexMatches(actual, mustMatch, mustNotMatch);
|
||||
});
|
||||
|
||||
test('convertTrailingSlash enabled', () => {
|
||||
const actual = convertTrailingSlash(true);
|
||||
const expected = [
|
||||
{
|
||||
src: '^(.*[^\\/])$',
|
||||
headers: { Location: '$1/' },
|
||||
status: 307,
|
||||
},
|
||||
];
|
||||
deepEqual(actual, expected);
|
||||
|
||||
const mustMatch = [['/index.html', '/dir', '/dir/index.html', '/foo/bar']];
|
||||
|
||||
const mustNotMatch = [['/', '/dir/', '/dir/foo/', '/next.php?page=/']];
|
||||
|
||||
assertRegexMatches(actual, mustMatch, mustNotMatch);
|
||||
});
|
||||
|
||||
test('convertTrailingSlash disabled', () => {
|
||||
const actual = convertTrailingSlash(false);
|
||||
const expected = [
|
||||
{
|
||||
src: '^(.*)\\/$',
|
||||
headers: { Location: '$1' },
|
||||
status: 307,
|
||||
},
|
||||
];
|
||||
deepEqual(actual, expected);
|
||||
|
||||
const mustMatch = [['/dir/', '/index.html/', '/next.php?page=/']];
|
||||
|
||||
const mustNotMatch = [['/dirp', '/mkdir', '/dir/foo']];
|
||||
|
||||
assertRegexMatches(actual, mustMatch, mustNotMatch);
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@now/static-build",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.0",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://zeit.co/docs/v2/deployments/official-builders/static-build-now-static-build",
|
||||
@@ -19,11 +19,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cross-spawn": "6.0.0",
|
||||
"@types/ms": "0.7.31",
|
||||
"@types/promise-timeout": "1.3.0",
|
||||
"cross-spawn": "6.0.5",
|
||||
"get-port": "5.0.0",
|
||||
"is-port-reachable": "2.0.1",
|
||||
"promise-timeout": "1.3.0",
|
||||
"ms": "2.1.2",
|
||||
"typescript": "3.5.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,6 +290,20 @@ export const frameworks: Framework[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Stencil',
|
||||
dependency: '@stencil/core',
|
||||
getOutputDirName: async () => 'www',
|
||||
defaultRoutes: [
|
||||
{
|
||||
handle: 'filesystem',
|
||||
},
|
||||
{
|
||||
src: '/(.*)',
|
||||
dest: '/index.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export interface Framework {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import ms from 'ms';
|
||||
import path from 'path';
|
||||
import spawn from 'cross-spawn';
|
||||
import getPort from 'get-port';
|
||||
import { timeout } from 'promise-timeout';
|
||||
import isPortReachable from 'is-port-reachable';
|
||||
import { ChildProcess, SpawnOptions } from 'child_process';
|
||||
import { existsSync, readFileSync, statSync, readdirSync } from 'fs';
|
||||
import { frameworks, Framework } from './frameworks';
|
||||
import {
|
||||
@@ -25,11 +26,20 @@ import {
|
||||
PrepareCacheOptions,
|
||||
} from '@now/build-utils';
|
||||
|
||||
async function checkForPort(port: number | undefined): Promise<void> {
|
||||
const sleep = (n: number) => new Promise(resolve => setTimeout(resolve, n));
|
||||
|
||||
const DEV_SERVER_PORT_BIND_TIMEOUT = ms('5m');
|
||||
|
||||
async function checkForPort(
|
||||
port: number | undefined,
|
||||
timeout: number
|
||||
): Promise<void> {
|
||||
const start = Date.now();
|
||||
while (!(await isPortReachable(port))) {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
if (Date.now() - start > timeout) {
|
||||
throw new Error(`Detecting port ${port} timed out after ${ms(timeout)}`);
|
||||
}
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +102,20 @@ function getCommand(pkg: PackageJson, cmd: string, { zeroConfig }: Config) {
|
||||
|
||||
export const version = 2;
|
||||
|
||||
const nowDevScriptPorts = new Map();
|
||||
const nowDevScriptPorts = new Map<string, number>();
|
||||
const nowDevChildProcesses = new Set<ChildProcess>();
|
||||
|
||||
['SIGINT', 'SIGTERM'].forEach(signal => {
|
||||
process.once(signal as NodeJS.Signals, () => {
|
||||
for (const child of nowDevChildProcesses) {
|
||||
debug(
|
||||
`Got ${signal}, killing dev server child process (pid=${child.pid})`
|
||||
);
|
||||
process.kill(child.pid, signal);
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
const getDevRoute = (srcBase: string, devPort: number, route: Route) => {
|
||||
const basic: Route = {
|
||||
@@ -256,32 +279,21 @@ export async function build({
|
||||
devPort = await getPort();
|
||||
nowDevScriptPorts.set(entrypoint, devPort);
|
||||
|
||||
const opts = {
|
||||
const opts: SpawnOptions = {
|
||||
cwd: entrypointDir,
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env, PORT: String(devPort) },
|
||||
};
|
||||
|
||||
const child = spawn('yarn', ['run', devScript], opts);
|
||||
const child: ChildProcess = spawn('yarn', ['run', devScript], opts);
|
||||
child.on('exit', () => nowDevScriptPorts.delete(entrypoint));
|
||||
if (child.stdout) {
|
||||
child.stdout.setEncoding('utf8');
|
||||
child.stdout.pipe(process.stdout);
|
||||
}
|
||||
if (child.stderr) {
|
||||
child.stderr.setEncoding('utf8');
|
||||
child.stderr.pipe(process.stderr);
|
||||
}
|
||||
nowDevChildProcesses.add(child);
|
||||
|
||||
// Now wait for the server to have listened on `$PORT`, after which we
|
||||
// will ProxyPass any requests to that development server that come in
|
||||
// for this builder.
|
||||
try {
|
||||
await timeout(
|
||||
new Promise(resolve => {
|
||||
checkForPort(devPort).then(resolve);
|
||||
}),
|
||||
5 * 60 * 1000
|
||||
);
|
||||
await checkForPort(devPort, DEV_SERVER_PORT_BIND_TIMEOUT);
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Failed to detect a server running on port ${devPort}.\nDetails: https://err.sh/zeit/now/now-static-build-failed-to-detect-a-server`
|
||||
|
||||
15
packages/now-static-build/src/typings.d.ts
vendored
15
packages/now-static-build/src/typings.d.ts
vendored
@@ -1,7 +1,10 @@
|
||||
declare module 'is-port-reachable' {
|
||||
export interface IsPortReachableOptions {
|
||||
timeout?: number | undefined;
|
||||
host?: string;
|
||||
}
|
||||
export default function(port: number | undefined, options?: IsPortReachableOptions): Promise<boolean>;
|
||||
}
|
||||
export interface IsPortReachableOptions {
|
||||
timeout?: number | undefined;
|
||||
host?: string;
|
||||
}
|
||||
export default function(
|
||||
port: number | undefined,
|
||||
options?: IsPortReachableOptions
|
||||
): Promise<boolean>;
|
||||
}
|
||||
|
||||
15
packages/now-static-build/test/fixtures/42-stencil/.editorconfig
vendored
Normal file
15
packages/now-static-build/test/fixtures/42-stencil/.editorconfig
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# http://editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
||||
26
packages/now-static-build/test/fixtures/42-stencil/.gitignore
vendored
Normal file
26
packages/now-static-build/test/fixtures/42-stencil/.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
dist/
|
||||
!www/favicon.ico
|
||||
www/
|
||||
|
||||
*~
|
||||
*.sw[mnpcod]
|
||||
*.log
|
||||
*.lock
|
||||
*.tmp
|
||||
*.tmp.*
|
||||
log.txt
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
|
||||
.stencil/
|
||||
.idea/
|
||||
.vscode/
|
||||
.sass-cache/
|
||||
.versions/
|
||||
node_modules/
|
||||
$RECYCLE.BIN/
|
||||
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
UserInterfaceState.xcuserstate
|
||||
.env
|
||||
21
packages/now-static-build/test/fixtures/42-stencil/LICENSE
vendored
Normal file
21
packages/now-static-build/test/fixtures/42-stencil/LICENSE
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
17
packages/now-static-build/test/fixtures/42-stencil/now.json
vendored
Normal file
17
packages/now-static-build/test/fixtures/42-stencil/now.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [
|
||||
{
|
||||
"src": "package.json",
|
||||
"use": "@now/static-build",
|
||||
"config": { "zeroConfig": true }
|
||||
}
|
||||
],
|
||||
"probes": [
|
||||
{ "path": "/", "mustContain": "Welcome to the Stencil App Starter" },
|
||||
{
|
||||
"path": "/profile/stencil",
|
||||
"mustContain": "Welcome to the Stencil App Starter"
|
||||
}
|
||||
]
|
||||
}
|
||||
18
packages/now-static-build/test/fixtures/42-stencil/package.json
vendored
Normal file
18
packages/now-static-build/test/fixtures/42-stencil/package.json
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "42-stencil",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"description": "Stencil App Starter",
|
||||
"scripts": {
|
||||
"build": "stencil build",
|
||||
"start": "stencil build --dev --watch --serve",
|
||||
"test": "stencil test --spec --e2e",
|
||||
"test.watch": "stencil test --spec --e2e --watchAll",
|
||||
"generate": "stencil generate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@stencil/core": "^1.3.3",
|
||||
"@stencil/router": "^1.0.1"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
41
packages/now-static-build/test/fixtures/42-stencil/readme.md
vendored
Normal file
41
packages/now-static-build/test/fixtures/42-stencil/readme.md
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
# Stencil App Starter
|
||||
|
||||
Stencil is a compiler for building fast web apps using Web Components.
|
||||
|
||||
Stencil combines the best concepts of the most popular frontend frameworks into a compile-time rather than run-time tool. Stencil takes TypeScript, JSX, a tiny virtual DOM layer, efficient one-way data binding, an asynchronous rendering pipeline (similar to React Fiber), and lazy-loading out of the box, and generates 100% standards-based Web Components that run in any browser supporting the Custom Elements v1 spec.
|
||||
|
||||
Stencil components are just Web Components, so they work in any major framework or with no framework at all. In many cases, Stencil can be used as a drop in replacement for traditional frontend frameworks given the capabilities now available in the browser, though using it as such is certainly not required.
|
||||
|
||||
Stencil also enables a number of key capabilities on top of Web Components, in particular Server Side Rendering (SSR) without the need to run a headless browser, pre-rendering, and objects-as-properties (instead of just strings).
|
||||
|
||||
## Getting Started
|
||||
|
||||
To start a new project using Stencil, clone this repo to a new directory:
|
||||
|
||||
```bash
|
||||
npm init stencil app
|
||||
```
|
||||
|
||||
and run:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
To build the app for production, run:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
To run the unit tests once, run:
|
||||
|
||||
```
|
||||
npm test
|
||||
```
|
||||
|
||||
To run the unit tests and watch for file changes during development, run:
|
||||
|
||||
```
|
||||
npm run test.watch
|
||||
```
|
||||
BIN
packages/now-static-build/test/fixtures/42-stencil/src/assets/icon/favicon.ico
vendored
Normal file
BIN
packages/now-static-build/test/fixtures/42-stencil/src/assets/icon/favicon.ico
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 549 B |
BIN
packages/now-static-build/test/fixtures/42-stencil/src/assets/icon/icon.png
vendored
Normal file
BIN
packages/now-static-build/test/fixtures/42-stencil/src/assets/icon/icon.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
65
packages/now-static-build/test/fixtures/42-stencil/src/components.d.ts
vendored
Normal file
65
packages/now-static-build/test/fixtures/42-stencil/src/components.d.ts
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
/* tslint:disable */
|
||||
/**
|
||||
* This is an autogenerated file created by the Stencil compiler.
|
||||
* It contains typing information for all components that exist in this project.
|
||||
*/
|
||||
|
||||
import { HTMLStencilElement, JSXBase } from '@stencil/core/internal';
|
||||
import { MatchResults } from '@stencil/router';
|
||||
|
||||
export namespace Components {
|
||||
interface AppHome {}
|
||||
interface AppProfile {
|
||||
match: MatchResults;
|
||||
}
|
||||
interface AppRoot {}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLAppHomeElement extends Components.AppHome, HTMLStencilElement {}
|
||||
var HTMLAppHomeElement: {
|
||||
prototype: HTMLAppHomeElement;
|
||||
new (): HTMLAppHomeElement;
|
||||
};
|
||||
|
||||
interface HTMLAppProfileElement
|
||||
extends Components.AppProfile,
|
||||
HTMLStencilElement {}
|
||||
var HTMLAppProfileElement: {
|
||||
prototype: HTMLAppProfileElement;
|
||||
new (): HTMLAppProfileElement;
|
||||
};
|
||||
|
||||
interface HTMLAppRootElement extends Components.AppRoot, HTMLStencilElement {}
|
||||
var HTMLAppRootElement: {
|
||||
prototype: HTMLAppRootElement;
|
||||
new (): HTMLAppRootElement;
|
||||
};
|
||||
interface HTMLElementTagNameMap {
|
||||
'app-home': HTMLAppHomeElement;
|
||||
'app-profile': HTMLAppProfileElement;
|
||||
'app-root': HTMLAppRootElement;
|
||||
}
|
||||
}
|
||||
|
||||
declare namespace LocalJSX {
|
||||
interface AppHome extends JSXBase.HTMLAttributes<HTMLAppHomeElement> {}
|
||||
interface AppProfile extends JSXBase.HTMLAttributes<HTMLAppProfileElement> {
|
||||
match?: MatchResults;
|
||||
}
|
||||
interface AppRoot extends JSXBase.HTMLAttributes<HTMLAppRootElement> {}
|
||||
|
||||
interface IntrinsicElements {
|
||||
'app-home': AppHome;
|
||||
'app-profile': AppProfile;
|
||||
'app-root': AppRoot;
|
||||
}
|
||||
}
|
||||
|
||||
export { LocalJSX as JSX };
|
||||
|
||||
declare module '@stencil/core' {
|
||||
export namespace JSX {
|
||||
interface IntrinsicElements extends LocalJSX.IntrinsicElements {}
|
||||
}
|
||||
}
|
||||
26
packages/now-static-build/test/fixtures/42-stencil/src/components/app-home/app-home.css
vendored
Normal file
26
packages/now-static-build/test/fixtures/42-stencil/src/components/app-home/app-home.css
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
.app-home {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #5851ff;
|
||||
color: white;
|
||||
margin: 8px;
|
||||
border: none;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
padding: 16px 20px;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 8px 16px rgba(0,0,0,.1), 0 3px 6px rgba(0,0,0,.08);
|
||||
outline: 0;
|
||||
letter-spacing: .04em;
|
||||
transition: all .15s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,.1), 0 1px 3px rgba(0,0,0,.1);
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
28
packages/now-static-build/test/fixtures/42-stencil/src/components/app-home/app-home.tsx
vendored
Normal file
28
packages/now-static-build/test/fixtures/42-stencil/src/components/app-home/app-home.tsx
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Component, h } from '@stencil/core';
|
||||
|
||||
@Component({
|
||||
tag: 'app-home',
|
||||
styleUrl: 'app-home.css',
|
||||
shadow: true
|
||||
})
|
||||
export class AppHome {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class='app-home'>
|
||||
<p>
|
||||
Welcome to the Stencil App Starter.
|
||||
You can use this starter to build entire apps all with
|
||||
web components using Stencil!
|
||||
Check out our docs on <a href='https://stenciljs.com'>stenciljs.com</a> to get started.
|
||||
</p>
|
||||
|
||||
<stencil-route-link url='/profile/stencil'>
|
||||
<button>
|
||||
Profile page
|
||||
</button>
|
||||
</stencil-route-link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.app-profile {
|
||||
padding: 10px;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user