Temporarily revert @TooTallNate's now dev updates (#4231)

Needs more time for testing, and we are preparing a new stable release.

This will be un-reverted after the stable release is tagged.
This commit is contained in:
Nathan Rajlich
2020-05-01 17:52:21 -07:00
committed by GitHub
parent fe0d762aca
commit 099bc6dbf6
31 changed files with 553 additions and 7007 deletions

2
.gitignore vendored
View File

@@ -9,6 +9,8 @@ coverage
*.swp *.swp
*.bak *.bak
*.tgz *.tgz
packages/now-cli/.builders
packages/now-cli/assets
packages/now-cli/src/util/dev/templates/*.ts packages/now-cli/src/util/dev/templates/*.ts
packages/now-cli/src/util/constants.ts packages/now-cli/src/util/constants.ts
packages/now-cli/test/**/yarn.lock packages/now-cli/test/**/yarn.lock

View File

@@ -50,7 +50,7 @@ export interface Config {
export interface Meta { export interface Meta {
isDev?: boolean; isDev?: boolean;
skipDownload?: boolean; skipDownload?: boolean;
requestPath?: string | null; requestPath?: string;
filesChanged?: string[]; filesChanged?: string[];
filesRemoved?: string[]; filesRemoved?: string[];
env?: Env; env?: Env;
@@ -187,60 +187,12 @@ export interface ShouldServeOptions {
config: Config; config: Config;
} }
export interface StartDevServerOptions {
/**
* Name of entrypoint file for this particular build job. Value
* `files[entrypoint]` is guaranteed to exist and be a valid File reference.
* `entrypoint` is always a discrete file and never a glob, since globs are
* expanded into separate builds at deployment time.
*/
entrypoint: string;
/**
* A writable temporary directory where you are encouraged to perform your
* build process. This directory will be populated with the restored cache.
*/
workPath: string;
/**
* An arbitrary object passed by the user in the build definition defined
* in `now.json`.
*/
config: Config;
/**
* Runtime environment variables configuration from the project's `now.json`
* and local `.env` file.
*/
env: Env;
}
export interface StartDevServerSuccess {
/**
* Port number where the dev server can be connected to, assumed to be running
* on `localhost`.
*/
port: number;
/**
* Process ID number of the dev server. Useful for the `now dev` server to
* shut down the dev server once an HTTP request has been fulfilled.
*/
pid: number;
}
/**
* `startDevServer()` may return `null` to opt-out of spawning a dev server for
* a given `entrypoint`.
*/
export type StartDevServerResult = StartDevServerSuccess | null;
/** /**
* Credit to Iain Reid, MIT license. * Credit to Iain Reid, MIT license.
* Source: https://gist.github.com/iainreid820/5c1cc527fe6b5b7dba41fec7fe54bf6e * Source: https://gist.github.com/iainreid820/5c1cc527fe6b5b7dba41fec7fe54bf6e
*/ */
// eslint-disable-next-line @typescript-eslint/no-namespace // eslint-disable-next-line @typescript-eslint/no-namespace
export namespace PackageJson { namespace PackageJson {
/** /**
* An author or contributor * An author or contributor
*/ */

View File

@@ -60,19 +60,9 @@
"engines": { "engines": {
"node": ">= 10" "node": ">= 10"
}, },
"dependencies": {
"@now/build-utils": "2.2.2-canary.4",
"@now/go": "1.0.8-canary.0",
"@now/next": "2.5.5-canary.5",
"@now/node": "1.5.2-canary.6",
"@now/python": "1.1.7-canary.1",
"@now/ruby": "1.1.1-canary.0",
"@now/static-build": "0.16.1-canary.3"
},
"devDependencies": { "devDependencies": {
"@sentry/node": "5.5.0", "@sentry/node": "5.5.0",
"@sindresorhus/slugify": "0.11.0", "@sindresorhus/slugify": "0.11.0",
"@tootallnate/once": "1.1.2",
"@types/ansi-escapes": "3.0.0", "@types/ansi-escapes": "3.0.0",
"@types/ansi-regex": "4.0.0", "@types/ansi-regex": "4.0.0",
"@types/async-retry": "1.2.1", "@types/async-retry": "1.2.1",
@@ -188,6 +178,7 @@
"which": "2.0.2", "which": "2.0.2",
"which-promise": "1.0.0", "which-promise": "1.0.0",
"write-json-file": "2.2.0", "write-json-file": "2.2.0",
"xdg-app-paths": "5.1.0" "xdg-app-paths": "5.1.0",
"yarn": "1.22.0"
} }
} }

View File

@@ -1,10 +1,58 @@
import cpy from 'cpy'; import cpy from 'cpy';
import tar from 'tar-fs';
import execa from 'execa'; import execa from 'execa';
import { join } from 'path'; import { join } from 'path';
import { remove, writeFile } from 'fs-extra'; import pipe from 'promisepipe';
import { createGzip } from 'zlib';
import {
createWriteStream,
mkdirp,
remove,
writeJSON,
writeFile,
} from 'fs-extra';
import { getDistTag } from '../src/util/get-dist-tag';
import pkg from '../package.json';
import { getBundledBuilders } from '../src/util/dev/get-bundled-builders';
const dirRoot = join(__dirname, '..'); const dirRoot = join(__dirname, '..');
async function createBuildersTarball() {
const distTag = getDistTag(pkg.version);
const builders = Array.from(getBundledBuilders()).map(b => `${b}@${distTag}`);
console.log(`Creating builders tarball with: ${builders.join(', ')}`);
const buildersDir = join(dirRoot, '.builders');
const assetsDir = join(dirRoot, 'assets');
await mkdirp(buildersDir);
await mkdirp(assetsDir);
const buildersTarballPath = join(assetsDir, 'builders.tar.gz');
try {
const buildersPkg = join(buildersDir, 'package.json');
await writeJSON(buildersPkg, { private: true }, { flag: 'wx' });
} catch (err) {
if (err.code !== 'EEXIST') {
throw err;
}
}
const yarn = join(dirRoot, '../../node_modules/yarn/bin/yarn.js');
await execa(process.execPath, [yarn, 'add', '--no-lockfile', ...builders], {
cwd: buildersDir,
stdio: 'inherit',
});
const packer = tar.pack(buildersDir);
await pipe(
packer,
createGzip(),
createWriteStream(buildersTarballPath)
);
}
async function createConstants() { async function createConstants() {
console.log('Creating constants.ts'); console.log('Creating constants.ts');
const filename = join(dirRoot, 'src/util/constants.ts'); const filename = join(dirRoot, 'src/util/constants.ts');
@@ -33,6 +81,10 @@ async function main() {
// During local development, these secrets will be empty. // During local development, these secrets will be empty.
await createConstants(); await createConstants();
// Create a tarball from all the `@now` scoped builders which will be bundled
// with Now CLI
await createBuildersTarball();
// `now dev` uses chokidar to watch the filesystem, but opts-out of the // `now dev` uses chokidar to watch the filesystem, but opts-out of the
// `fsevents` feature using `useFsEvents: false`, so delete the module here so // `fsevents` feature using `useFsEvents: false`, so delete the module here so
// that it is not compiled by ncc, which makes the npm package size larger // that it is not compiled by ncc, which makes the npm package size larger

View File

@@ -86,7 +86,6 @@ export default async function dev(
}); });
process.once('SIGINT', () => devServer.stop()); process.once('SIGINT', () => devServer.stop());
process.once('SIGTERM', () => devServer.stop());
await devServer.start(...listen); await devServer.start(...listen);
} }

View File

@@ -1,13 +1,23 @@
import execa from 'execa'; import execa from 'execa';
import semver from 'semver'; import semver from 'semver';
import pipe from 'promisepipe';
import retry from 'async-retry'; import retry from 'async-retry';
import npa from 'npm-package-arg'; import npa from 'npm-package-arg';
import pluralize from 'pluralize'; import pluralize from 'pluralize';
import { basename, join, resolve } from 'path'; import { extract } from 'tar-fs';
import { createHash } from 'crypto';
import { createGunzip } from 'zlib';
import { join, resolve } from 'path';
import { PackageJson } from '@now/build-utils'; import { PackageJson } from '@now/build-utils';
import XDGAppPaths from 'xdg-app-paths'; import XDGAppPaths from 'xdg-app-paths';
import { mkdirp, readJSON, writeJSON } from 'fs-extra'; import {
import nowCliPkg from '../pkg'; createReadStream,
mkdirp,
readFile,
readJSON,
writeFile,
} from 'fs-extra';
import pkg from '../../../package.json';
import { NoBuilderCacheError } from '../errors-ts'; import { NoBuilderCacheError } from '../errors-ts';
import { Output } from '../output'; import { Output } from '../output';
@@ -24,16 +34,41 @@ const registryTypes = new Set(['version', 'tag', 'range']);
const localBuilders: { [key: string]: BuilderWithPackage } = { const localBuilders: { [key: string]: BuilderWithPackage } = {
'@now/static': { '@now/static': {
runInProcess: true, runInProcess: true,
requirePath: '@now/static',
builder: Object.freeze(staticBuilder), builder: Object.freeze(staticBuilder),
package: Object.freeze({ name: '@now/static', version: '' }), package: Object.freeze({ name: '@now/static', version: '' }),
}, },
}; };
const distTag = nowCliPkg.version ? getDistTag(nowCliPkg.version) : 'canary'; const distTag = getDistTag(pkg.version);
export const cacheDirPromise = prepareCacheDir(); export const cacheDirPromise = prepareCacheDir();
export const builderDirPromise = prepareBuilderDir(); export const builderDirPromise = prepareBuilderDir();
export const builderModulePathPromise = prepareBuilderModulePath();
function readFileOrNull(
filePath: string,
encoding?: null
): Promise<Buffer | null>;
function readFileOrNull(
filePath: string,
encoding: string
): Promise<string | null>;
async function readFileOrNull(
filePath: string,
encoding?: string | null
): Promise<Buffer | string | null> {
try {
if (encoding) {
return await readFile(filePath, encoding);
}
return await readFile(filePath);
} catch (err) {
if (err.code === 'ENOENT') {
return null;
}
throw err;
}
}
/** /**
* Prepare cache directory for installing now-builders * Prepare cache directory for installing now-builders
@@ -57,19 +92,51 @@ export async function prepareBuilderDir() {
const builderDir = join(await cacheDirPromise, 'builders'); const builderDir = join(await cacheDirPromise, 'builders');
await mkdirp(builderDir); await mkdirp(builderDir);
// Create an empty `package.json` file, only if one does not already exist // Extract the bundled `builders.tar.gz` file, if necessary
try { const bundledTarballPath = join(__dirname, '../../../assets/builders.tar.gz');
const buildersPkg = join(builderDir, 'package.json');
await writeJSON(buildersPkg, { private: true }, { flag: 'wx' }); const existingPackageJson =
} catch (err) { (await readFileOrNull(join(builderDir, 'package.json'), 'utf8')) || '{}';
if (err.code !== 'EEXIST') { const { dependencies = {} } = JSON.parse(existingPackageJson);
throw err;
} if (!hasBundledBuilders(dependencies)) {
const extractor = extract(builderDir);
await pipe(
createReadStream(bundledTarballPath),
createGunzip(),
extractor
);
} }
return builderDir; return builderDir;
} }
export async function prepareBuilderModulePath() {
const [builderDir, builderContents] = await Promise.all([
builderDirPromise,
readFile(join(__dirname, 'builder-worker.js')),
]);
let needsWrite = false;
const builderSha = getSha(builderContents);
const cachedBuilderPath = join(builderDir, 'builder.js');
const cachedBuilderContents = await readFileOrNull(cachedBuilderPath);
if (cachedBuilderContents) {
const cachedBuilderSha = getSha(cachedBuilderContents);
if (builderSha !== cachedBuilderSha) {
needsWrite = true;
}
} else {
needsWrite = true;
}
if (needsWrite) {
await writeFile(cachedBuilderPath, builderContents);
}
return cachedBuilderPath;
}
function getNpmVersion(use = ''): string { function getNpmVersion(use = ''): string {
const parsed = npa(use); const parsed = npa(use);
if (registryTypes.has(parsed.type)) { if (registryTypes.has(parsed.type)) {
@@ -99,20 +166,12 @@ function parseVersionSafe(rawSpec: string) {
export function filterPackage( export function filterPackage(
builderSpec: string, builderSpec: string,
distTag: string, distTag: string,
buildersPkg: PackageJson, buildersPkg: PackageJson
nowCliPkg: PackageJson
) { ) {
if (builderSpec in localBuilders) return false; if (builderSpec in localBuilders) return false;
const parsed = npa(builderSpec); const parsed = npa(builderSpec);
const parsedVersion = parseVersionSafe(parsed.rawSpec); const parsedVersion = parseVersionSafe(parsed.rawSpec);
// skip install of already installed Runtime
// If it's a builder that is part of Now CLI's `dependencies` then
// the builder is already installed into `node_modules`
if (isBundledBuilder(parsed, nowCliPkg)) {
return false;
}
// Skip install of already installed Runtime
if ( if (
parsed.name && parsed.name &&
parsed.type === 'version' && parsed.type === 'version' &&
@@ -154,6 +213,7 @@ export function filterPackage(
*/ */
export async function installBuilders( export async function installBuilders(
packagesSet: Set<string>, packagesSet: Set<string>,
yarnDir: string,
output: Output, output: Output,
builderDir?: string builderDir?: string
): Promise<void> { ): Promise<void> {
@@ -169,18 +229,15 @@ export async function installBuilders(
if (!builderDir) { if (!builderDir) {
builderDir = await builderDirPromise; builderDir = await builderDirPromise;
} }
const yarnPath = join(yarnDir, 'yarn');
const buildersPkgPath = join(builderDir, 'package.json'); const buildersPkgPath = join(builderDir, 'package.json');
const buildersPkgBefore = await readJSON(buildersPkgPath); const buildersPkgBefore = await readJSON(buildersPkgPath);
const depsBefore = {
...buildersPkgBefore.devDependencies,
...buildersPkgBefore.dependencies,
};
packages.push(getBuildUtils(packages)); packages.push(getBuildUtils(packages));
// Filter out any packages that come packaged with `now-cli` // Filter out any packages that come packaged with `now-cli`
const packagesToInstall = packages.filter(p => const packagesToInstall = packages.filter(p =>
filterPackage(p, distTag, buildersPkgBefore, nowCliPkg) filterPackage(p, distTag, buildersPkgBefore)
); );
if (packagesToInstall.length === 0) { if (packagesToInstall.length === 0) {
@@ -199,11 +256,13 @@ export async function installBuilders(
await retry( await retry(
() => () =>
execa( execa(
'npm', process.execPath,
[ [
'install', yarnPath,
'--save-exact', 'add',
'--no-package-lock', '--exact',
'--no-lockfile',
'--non-interactive',
...packagesToInstall, ...packagesToInstall,
], ],
{ {
@@ -218,12 +277,8 @@ export async function installBuilders(
const updatedPackages: string[] = []; const updatedPackages: string[] = [];
const buildersPkgAfter = await readJSON(buildersPkgPath); const buildersPkgAfter = await readJSON(buildersPkgPath);
const depsAfter = { for (const [name, version] of Object.entries(buildersPkgAfter.dependencies)) {
...buildersPkgAfter.devDependencies, if (version !== buildersPkgBefore.dependencies[name]) {
...buildersPkgAfter.dependencies,
};
for (const [name, version] of Object.entries(depsAfter)) {
if (version !== depsBefore[name]) {
output.debug(`Runtime "${name}" updated to version \`${version}\``); output.debug(`Runtime "${name}" updated to version \`${version}\``);
updatedPackages.push(name); updatedPackages.push(name);
} }
@@ -234,6 +289,7 @@ export async function installBuilders(
export async function updateBuilders( export async function updateBuilders(
packagesSet: Set<string>, packagesSet: Set<string>,
yarnDir: string,
output: Output, output: Output,
builderDir?: string builderDir?: string
): Promise<string[]> { ): Promise<string[]> {
@@ -241,57 +297,43 @@ export async function updateBuilders(
builderDir = await builderDirPromise; builderDir = await builderDirPromise;
} }
const updatedPackages: string[] = [];
const packages = Array.from(packagesSet); const packages = Array.from(packagesSet);
const yarnPath = join(yarnDir, 'yarn');
const buildersPkgPath = join(builderDir, 'package.json'); const buildersPkgPath = join(builderDir, 'package.json');
const buildersPkgBefore = await readJSON(buildersPkgPath); const buildersPkgBefore = await readJSON(buildersPkgPath);
const depsBefore = {
...buildersPkgBefore.devDependencies,
...buildersPkgBefore.dependencies,
};
const packagesToUpdate = packages.filter(p => { packages.push(getBuildUtils(packages));
if (p in localBuilders) return false;
// If it's a builder that is part of Now CLI's `dependencies` then await retry(
// don't update it () =>
if (isBundledBuilder(npa(p), nowCliPkg)) { execa(
return false; process.execPath,
[
yarnPath,
'add',
'--exact',
'--no-lockfile',
'--non-interactive',
...packages.filter(p => p !== '@now/static'),
],
{
cwd: builderDir,
}
),
{ retries: 2 }
);
const updatedPackages: string[] = [];
const buildersPkgAfter = await readJSON(buildersPkgPath);
for (const [name, version] of Object.entries(buildersPkgAfter.dependencies)) {
if (version !== buildersPkgBefore.dependencies[name]) {
output.debug(`Runtime "${name}" updated to version \`${version}\``);
updatedPackages.push(name);
} }
return true;
});
if (packagesToUpdate.length > 0) {
packages.push(getBuildUtils(packages));
await retry(
() =>
execa(
'npm',
['install', '--save-exact', '--no-package-lock', ...packagesToUpdate],
{
cwd: builderDir,
}
),
{ retries: 2 }
);
const buildersPkgAfter = await readJSON(buildersPkgPath);
const depsAfter = {
...buildersPkgAfter.devDependencies,
...buildersPkgAfter.dependencies,
};
for (const [name, version] of Object.entries(depsAfter)) {
if (version !== depsBefore[name]) {
output.debug(`Runtime "${name}" updated to version \`${version}\``);
updatedPackages.push(name);
}
}
purgeRequireCache(updatedPackages, builderDir, output);
} }
purgeRequireCache(updatedPackages, builderDir, output);
return updatedPackages; return updatedPackages;
} }
@@ -300,83 +342,43 @@ export async function updateBuilders(
*/ */
export async function getBuilder( export async function getBuilder(
builderPkg: string, builderPkg: string,
yarnDir: string,
output: Output, output: Output,
builderDir?: string, builderDir?: string
isRetry = false
): Promise<BuilderWithPackage> { ): Promise<BuilderWithPackage> {
let builderWithPkg: BuilderWithPackage = localBuilders[builderPkg]; let builderWithPkg: BuilderWithPackage = localBuilders[builderPkg];
if (!builderWithPkg) { if (!builderWithPkg) {
if (!builderDir) { if (!builderDir) {
builderDir = await builderDirPromise; builderDir = await builderDirPromise;
} }
let requirePath: string;
const parsed = npa(builderPkg); const parsed = npa(builderPkg);
const buildersPkg = await readJSON(join(builderDir, 'package.json'));
// First check if it's a bundled Runtime in Now CLI's `node_modules` const pkgName = getPackageName(parsed, buildersPkg) || builderPkg;
const bundledBuilder = isBundledBuilder(parsed, nowCliPkg); const dest = join(builderDir, 'node_modules', pkgName);
if (bundledBuilder && parsed.name) {
requirePath = parsed.name;
} else {
const buildersPkg = await readJSON(join(builderDir, 'package.json'));
const pkgName = getPackageName(parsed, buildersPkg) || builderPkg;
requirePath = join(builderDir, 'node_modules', pkgName);
}
try { try {
output.debug(`Requiring runtime: "${requirePath}"`); const mod = require(dest);
const mod = require(requirePath); const pkg = require(join(dest, 'package.json'));
const pkg = require(join(requirePath, 'package.json'));
builderWithPkg = { builderWithPkg = {
requirePath,
builder: Object.freeze(mod), builder: Object.freeze(mod),
package: Object.freeze(pkg), package: Object.freeze(pkg),
}; };
} catch (err) { } catch (err) {
if (err.code === 'MODULE_NOT_FOUND' && !isRetry) { if (err.code === 'MODULE_NOT_FOUND') {
output.debug( output.debug(
`Attempted to require ${requirePath}, but it is not installed` `Attempted to require ${builderPkg}, but it is not installed`
); );
const pkgSet = new Set([builderPkg]); const pkgSet = new Set([builderPkg]);
await installBuilders(pkgSet, output, builderDir); await installBuilders(pkgSet, yarnDir, output, builderDir);
// Run `getBuilder()` again now that the builder has been installed // Run `getBuilder()` again now that the builder has been installed
return getBuilder(builderPkg, output, builderDir, true); return getBuilder(builderPkg, yarnDir, output, builderDir);
} }
throw err; throw err;
} }
// If it's a bundled builder, then cache the require call
if (bundledBuilder) {
localBuilders[builderPkg] = builderWithPkg;
}
} }
return builderWithPkg; return builderWithPkg;
} }
export function isBundledBuilder(
parsed: npa.Result,
pkg: PackageJson
): boolean {
if (!parsed.name || !pkg.dependencies) {
return false;
}
const bundledVersion = pkg.dependencies[parsed.name];
if (bundledVersion) {
if (parsed.type === 'tag') {
if (parsed.fetchSpec === 'canary') {
return bundledVersion.includes('canary');
} else if (parsed.fetchSpec === 'latest') {
return !bundledVersion.includes('canary');
}
} else if (parsed.type === 'version') {
return parsed.fetchSpec === bundledVersion;
}
}
return false;
}
function getPackageName( function getPackageName(
parsed: npa.Result, parsed: npa.Result,
buildersPkg: PackageJson buildersPkg: PackageJson
@@ -384,18 +386,30 @@ function getPackageName(
if (registryTypes.has(parsed.type)) { if (registryTypes.has(parsed.type)) {
return parsed.name; return parsed.name;
} }
const deps: { [name: string]: string } = { const deps = { ...buildersPkg.devDependencies, ...buildersPkg.dependencies };
...buildersPkg.devDependencies,
...buildersPkg.dependencies,
};
for (const [name, dep] of Object.entries(deps)) { for (const [name, dep] of Object.entries(deps)) {
if (dep === parsed.raw || basename(dep) === basename(parsed.raw)) { if (dep === parsed.raw) {
return name; return name;
} }
} }
return null; return null;
} }
function getSha(buffer: Buffer): string {
const hash = createHash('sha256');
hash.update(buffer);
return hash.digest('hex');
}
function hasBundledBuilders(dependencies: { [name: string]: string }): boolean {
for (const name of getBundledBuilders()) {
if (!(name in dependencies)) {
return false;
}
}
return true;
}
function purgeRequireCache( function purgeRequireCache(
packages: string[], packages: string[],
builderDir: string, builderDir: string,

View File

@@ -24,19 +24,19 @@ function onMessage(message) {
} }
async function processMessage(message) { async function processMessage(message) {
const { requirePath, buildOptions } = message; const { builderName, buildParams } = message;
const builder = require(requirePath); const builder = require(builderName);
// Convert the `files` to back into `FileFsRef` instances // Convert the `files` to back into `FileFsRef` instances
for (const name of Object.keys(buildOptions.files)) { for (const name of Object.keys(buildParams.files)) {
const ref = Object.assign( const ref = Object.assign(
Object.create(FileFsRef.prototype), Object.create(FileFsRef.prototype),
buildOptions.files[name] buildParams.files[name]
); );
buildOptions.files[name] = ref; buildParams.files[name] = ref;
} }
const result = await builder.build(buildOptions); const result = await builder.build(buildParams);
// `@now/next` sets this, but it causes "Converting circular // `@now/next` sets this, but it causes "Converting circular
// structure to JSON" errors, so delete the property... // structure to JSON" errors, so delete the property...

View File

@@ -2,30 +2,24 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-non-null-assertion */
import ms from 'ms'; import ms from 'ms';
import bytes from 'bytes'; import bytes from 'bytes';
import { promisify } from 'util';
import { delimiter, dirname, join } from 'path'; import { delimiter, dirname, join } from 'path';
import { fork, ChildProcess } from 'child_process'; import { fork, ChildProcess } from 'child_process';
import { createFunction } from '@zeit/fun'; import { createFunction } from '@zeit/fun';
import { import { Builder, File, Lambda, FileBlob, FileFsRef } from '@now/build-utils';
Builder,
BuildOptions,
Env,
File,
Lambda,
FileBlob,
FileFsRef,
} from '@now/build-utils';
import plural from 'pluralize'; import plural from 'pluralize';
import minimatch from 'minimatch'; import minimatch from 'minimatch';
import _treeKill from 'tree-kill';
import { Output } from '../output'; import { Output } from '../output';
import highlight from '../output/highlight'; import highlight from '../output/highlight';
import { treeKill } from '../tree-kill';
import { relative } from '../path-helpers'; import { relative } from '../path-helpers';
import { LambdaSizeExceededError } from '../errors-ts'; import { LambdaSizeExceededError } from '../errors-ts';
import DevServer from './server'; import DevServer from './server';
import { getBuilder } from './builder-cache'; import { builderModulePathPromise, getBuilder } from './builder-cache';
import { import {
EnvConfig,
NowConfig, NowConfig,
BuildMatch, BuildMatch,
BuildResult, BuildResult,
@@ -33,6 +27,7 @@ import {
BuilderOutput, BuilderOutput,
BuildResultV3, BuildResultV3,
BuilderOutputs, BuilderOutputs,
BuilderParams,
EnvConfigs, EnvConfigs,
} from './types'; } from './types';
import { normalizeRoutes } from '@now/routing-utils'; import { normalizeRoutes } from '@now/routing-utils';
@@ -48,27 +43,38 @@ interface BuildMessageResult extends BuildMessage {
error?: object; error?: object;
} }
const treeKill = promisify(_treeKill);
async function createBuildProcess( async function createBuildProcess(
match: BuildMatch, match: BuildMatch,
envConfigs: EnvConfigs, envConfigs: EnvConfigs,
workPath: string, workPath: string,
output: Output output: Output,
yarnPath?: string
): Promise<ChildProcess> { ): Promise<ChildProcess> {
const builderWorkerPath = join(__dirname, 'builder-worker.js'); const { execPath } = process;
const modulePath = await builderModulePathPromise;
// Ensure that `node` is in the builder's `PATH` // Ensure that `node` is in the builder's `PATH`
let PATH = `${dirname(process.execPath)}${delimiter}${process.env.PATH}`; let PATH = `${dirname(execPath)}${delimiter}${process.env.PATH}`;
const env: Env = { // Ensure that `yarn` is in the builder's `PATH`
if (yarnPath) {
PATH = `${yarnPath}${delimiter}${PATH}`;
}
const env: EnvConfig = {
...process.env, ...process.env,
PATH, PATH,
...envConfigs.allEnv, ...envConfigs.allEnv,
NOW_REGION: 'dev1', NOW_REGION: 'dev1',
}; };
const buildProcess = fork(builderWorkerPath, [], { const buildProcess = fork(modulePath, [], {
cwd: workPath, cwd: workPath,
env, env,
execPath,
execArgv: [],
}); });
match.buildProcess = buildProcess; match.buildProcess = buildProcess;
@@ -102,10 +108,10 @@ export async function executeBuild(
filesRemoved?: string[] filesRemoved?: string[]
): Promise<void> { ): Promise<void> {
const { const {
builderWithPkg: { runInProcess, requirePath, builder, package: pkg }, builderWithPkg: { runInProcess, builder, package: pkg },
} = match; } = match;
const { entrypoint } = match; const { entrypoint } = match;
const { debug, envConfigs, cwd: workPath } = devServer; const { debug, envConfigs, yarnPath, cwd: workPath } = devServer;
const startTime = Date.now(); const startTime = Date.now();
const showBuildTimestamp = const showBuildTimestamp =
@@ -129,11 +135,12 @@ export async function executeBuild(
match, match,
envConfigs, envConfigs,
workPath, workPath,
devServer.output devServer.output,
yarnPath
); );
} }
const buildOptions: BuildOptions = { const buildParams: BuilderParams = {
files, files,
entrypoint, entrypoint,
workPath, workPath,
@@ -154,8 +161,8 @@ export async function executeBuild(
if (buildProcess) { if (buildProcess) {
buildProcess.send({ buildProcess.send({
type: 'build', type: 'build',
requirePath, builderName: pkg.name,
buildOptions, buildParams,
}); });
buildResultOrOutputs = await new Promise((resolve, reject) => { buildResultOrOutputs = await new Promise((resolve, reject) => {
@@ -186,7 +193,7 @@ export async function executeBuild(
buildProcess!.on('message', onMessage); buildProcess!.on('message', onMessage);
}); });
} else { } else {
buildResultOrOutputs = await builder.build(buildOptions); buildResultOrOutputs = await builder.build(buildParams);
} }
// Sort out build result to builder v2 shape // Sort out build result to builder v2 shape
@@ -383,6 +390,7 @@ export async function executeBuild(
export async function getBuildMatches( export async function getBuildMatches(
nowConfig: NowConfig, nowConfig: NowConfig,
cwd: string, cwd: string,
yarnDir: string,
output: Output, output: Output,
devServer: DevServer, devServer: DevServer,
fileList: string[] fileList: string[]
@@ -434,7 +442,7 @@ export async function getBuildMatches(
for (const file of files) { for (const file of files) {
src = relative(cwd, file); src = relative(cwd, file);
const builderWithPkg = await getBuilder(use, output); const builderWithPkg = await getBuilder(use, yarnDir, output);
matches.push({ matches.push({
...buildConfig, ...buildConfig,
src, src,

View File

@@ -4,7 +4,7 @@ import PCRE from 'pcre-to-regexp';
import isURL from './is-url'; import isURL from './is-url';
import DevServer from './server'; import DevServer from './server';
import { HttpHeadersConfig, RouteResult } from './types'; import { HttpHeadersConfig, RouteConfig, RouteResult } from './types';
import { isHandler, Route, HandleValue } from '@now/routing-utils'; import { isHandler, Route, HandleValue } from '@now/routing-utils';
export function resolveRouteParameters( export function resolveRouteParameters(
@@ -48,10 +48,10 @@ export function getRoutesTypes(routes: Route[] = []) {
export async function devRouter( export async function devRouter(
reqUrl: string = '/', reqUrl: string = '/',
reqMethod?: string, reqMethod?: string,
routes?: Route[], routes?: RouteConfig[],
devServer?: DevServer, devServer?: DevServer,
previousHeaders?: HttpHeadersConfig, previousHeaders?: HttpHeadersConfig,
missRoutes?: Route[], missRoutes?: RouteConfig[],
phase?: HandleValue | null phase?: HandleValue | null
): Promise<RouteResult> { ): Promise<RouteResult> {
let result: RouteResult | undefined; let result: RouteResult | undefined;

View File

@@ -16,9 +16,7 @@ import {
getTransformedRoutes, getTransformedRoutes,
appendRoutesToPhase, appendRoutesToPhase,
HandleValue, HandleValue,
Route,
} from '@now/routing-utils'; } from '@now/routing-utils';
import once from '@tootallnate/once';
import directoryTemplate from 'serve-handler/src/directory'; import directoryTemplate from 'serve-handler/src/directory';
import getPort from 'get-port'; import getPort from 'get-port';
import { ChildProcess } from 'child_process'; import { ChildProcess } from 'child_process';
@@ -27,8 +25,6 @@ import which from 'which';
import { import {
Builder, Builder,
Env,
StartDevServerResult,
FileFsRef, FileFsRef,
PackageJson, PackageJson,
detectBuilders, detectBuilders,
@@ -37,6 +33,7 @@ import {
spawnCommand, spawnCommand,
} from '@now/build-utils'; } from '@now/build-utils';
import { once } from '../once';
import link from '../output/link'; import link from '../output/link';
import { Output } from '../output'; import { Output } from '../output';
import { relative } from '../path-helpers'; import { relative } from '../path-helpers';
@@ -62,13 +59,10 @@ import {
import { devRouter, getRoutesTypes } from './router'; import { devRouter, getRoutesTypes } from './router';
import getMimeType from './mime-type'; import getMimeType from './mime-type';
import { getYarnPath } from './yarn-installer';
import { executeBuild, getBuildMatches, shutdownBuilder } from './builder'; import { executeBuild, getBuildMatches, shutdownBuilder } from './builder';
import { generateErrorMessage, generateHttpStatusDescription } from './errors'; import { generateErrorMessage, generateHttpStatusDescription } from './errors';
import { import { installBuilders, updateBuilders } from './builder-cache';
installBuilders,
updateBuilders,
builderDirPromise,
} from './builder-cache';
// HTML templates // HTML templates
import errorTemplate from './templates/error'; import errorTemplate from './templates/error';
@@ -78,6 +72,7 @@ import errorTemplate502 from './templates/error_502';
import redirectTemplate from './templates/redirect'; import redirectTemplate from './templates/redirect';
import { import {
EnvConfig,
NowConfig, NowConfig,
DevServerOptions, DevServerOptions,
BuildMatch, BuildMatch,
@@ -88,6 +83,7 @@ import {
InvokePayload, InvokePayload,
InvokeResult, InvokeResult,
ListenSpec, ListenSpec,
RouteConfig,
RouteResult, RouteResult,
HttpHeadersConfig, HttpHeadersConfig,
EnvConfigs, EnvConfigs,
@@ -117,6 +113,7 @@ export default class DevServer {
public envConfigs: EnvConfigs; public envConfigs: EnvConfigs;
public frameworkSlug: string | null; public frameworkSlug: string | null;
public files: BuilderInputs; public files: BuilderInputs;
public yarnPath: string;
public address: string; public address: string;
private cachedNowConfig: NowConfig | null; private cachedNowConfig: NowConfig | null;
@@ -135,7 +132,6 @@ export default class DevServer {
private devCommand?: string; private devCommand?: string;
private devProcess?: ChildProcess; private devProcess?: ChildProcess;
private devProcessPort?: number; private devProcessPort?: number;
private devServerPids: Set<number>;
private getNowConfigPromise: Promise<NowConfig> | null; private getNowConfigPromise: Promise<NowConfig> | null;
private blockingBuildsPromise: Promise<void> | null; private blockingBuildsPromise: Promise<void> | null;
@@ -150,9 +146,13 @@ export default class DevServer {
this.address = ''; this.address = '';
this.devCommand = options.devCommand; this.devCommand = options.devCommand;
this.frameworkSlug = options.frameworkSlug; this.frameworkSlug = options.frameworkSlug;
// This gets updated when `start()` is invoked
this.yarnPath = '/';
this.cachedNowConfig = null; this.cachedNowConfig = null;
this.apiDir = null; this.apiDir = null;
this.apiExtensions = new Set(); this.apiExtensions = new Set<string>();
this.server = http.createServer(this.devServerHandler); this.server = http.createServer(this.devServerHandler);
this.server.timeout = 0; // Disable timeout this.server.timeout = 0; // Disable timeout
this.stopping = false; this.stopping = false;
@@ -171,8 +171,6 @@ export default class DevServer {
this.podId = Math.random() this.podId = Math.random()
.toString(32) .toString(32)
.slice(-5); .slice(-5);
this.devServerPids = new Set();
} }
async exit(code = 1) { async exit(code = 1) {
@@ -359,6 +357,7 @@ export default class DevServer {
const matches = await getBuildMatches( const matches = await getBuildMatches(
nowConfig, nowConfig,
this.cwd, this.cwd,
this.yarnPath,
this.output, this.output,
this, this,
fileList fileList
@@ -450,11 +449,11 @@ export default class DevServer {
await this.updateBuildMatches(nowConfig); await this.updateBuildMatches(nowConfig);
} }
async getLocalEnv(fileName: string, base?: Env): Promise<Env> { async getLocalEnv(fileName: string, base?: EnvConfig): Promise<EnvConfig> {
// TODO: use the file watcher to only invalidate the env `dotfile` // TODO: use the file watcher to only invalidate the env `dotfile`
// once a change to the `fileName` occurs // once a change to the `fileName` occurs
const filePath = join(this.cwd, fileName); const filePath = join(this.cwd, fileName);
let env: Env = {}; let env: EnvConfig = {};
try { try {
const dotenv = await fs.readFile(filePath, 'utf8'); const dotenv = await fs.readFile(filePath, 'utf8');
this.output.debug(`Using local env: ${filePath}`); this.output.debug(`Using local env: ${filePath}`);
@@ -579,7 +578,7 @@ export default class DevServer {
config.builds = config.builds || []; config.builds = config.builds || [];
config.builds.push(...builders); config.builds.push(...builders);
const routes: Route[] = []; const routes: RouteConfig[] = [];
const { routes: nowConfigRoutes } = config; const { routes: nowConfigRoutes } = config;
routes.push(...(redirectRoutes || [])); routes.push(...(redirectRoutes || []));
routes.push( routes.push(
@@ -651,7 +650,6 @@ export default class DevServer {
if (config.version === 1) { if (config.version === 1) {
this.output.error('Only `version: 2` is supported by `now dev`'); this.output.error('Only `version: 2` is supported by `now dev`');
await this.exit(1); await this.exit(1);
return;
} }
await this.tryValidateOrExit(config, validateNowConfigBuilds); await this.tryValidateOrExit(config, validateNowConfigBuilds);
@@ -664,7 +662,11 @@ export default class DevServer {
await this.tryValidateOrExit(config, validateNowConfigFunctions); await this.tryValidateOrExit(config, validateNowConfigFunctions);
} }
validateEnvConfig(type: string, env: Env = {}, localEnv: Env = {}): Env { validateEnvConfig(
type: string,
env: EnvConfig = {},
localEnv: EnvConfig = {}
): EnvConfig {
// Validate if there are any missing env vars defined in `now.json`, // Validate if there are any missing env vars defined in `now.json`,
// but not in the `.env` / `.build.env` file // but not in the `.env` / `.build.env` file
const missing: string[] = Object.entries(env) const missing: string[] = Object.entries(env)
@@ -680,7 +682,7 @@ export default class DevServer {
throw new MissingDotenvVarsError(type, missing); throw new MissingDotenvVarsError(type, missing);
} }
const merged: Env = { ...env, ...localEnv }; const merged: EnvConfig = { ...env, ...localEnv };
// Validate that the env var name matches what AWS Lambda allows: // Validate that the env var name matches what AWS Lambda allows:
// - https://docs.aws.amazon.com/lambda/latest/dg/env_variables.html // - https://docs.aws.amazon.com/lambda/latest/dg/env_variables.html
@@ -727,6 +729,8 @@ export default class DevServer {
throw new Error(`${chalk.bold(this.cwd)} is not a directory`); throw new Error(`${chalk.bold(this.cwd)} is not a directory`);
} }
this.yarnPath = await getYarnPath(this.output);
const ig = await createIgnore(join(this.cwd, '.nowignore')); const ig = await createIgnore(join(this.cwd, '.nowignore'));
this.filter = ig.createFilter(); this.filter = ig.createFilter();
@@ -760,12 +764,16 @@ export default class DevServer {
.map((b: Builder) => b.use) .map((b: Builder) => b.use)
); );
await installBuilders(builders, this.output); await installBuilders(builders, this.yarnPath, this.output);
await this.updateBuildMatches(nowConfig, true); await this.updateBuildMatches(nowConfig, true);
// Updating builders happens lazily, and any builders that were updated // Updating builders happens lazily, and any builders that were updated
// get their "build matches" invalidated so that the new version is used. // get their "build matches" invalidated so that the new version is used.
this.updateBuildersPromise = updateBuilders(builders, this.output) this.updateBuildersPromise = updateBuilders(
builders,
this.yarnPath,
this.output
)
.then(updatedBuilders => { .then(updatedBuilders => {
this.updateBuildersPromise = null; this.updateBuildersPromise = null;
this.invalidateBuildMatches(nowConfig, updatedBuilders); this.invalidateBuildMatches(nowConfig, updatedBuilders);
@@ -861,7 +869,6 @@ export default class DevServer {
*/ */
async stop(exitCode?: number): Promise<void> { async stop(exitCode?: number): Promise<void> {
const { devProcess } = this; const { devProcess } = this;
const { debug } = this.output;
if (this.stopping) return; if (this.stopping) return;
this.stopping = true; this.stopping = true;
@@ -892,22 +899,15 @@ export default class DevServer {
ops.push(close(this.server)); ops.push(close(this.server));
if (this.watcher) { if (this.watcher) {
debug(`Closing file watcher`); this.output.debug(`Closing file watcher`);
this.watcher.close(); this.watcher.close();
} }
if (this.updateBuildersPromise) { if (this.updateBuildersPromise) {
debug(`Waiting for builders update to complete`); this.output.debug(`Waiting for builders update to complete`);
ops.push(this.updateBuildersPromise); ops.push(this.updateBuildersPromise);
} }
for (const pid of this.devServerPids) {
ops.push(this.killBuilderDevServer(pid));
}
// Ensure that the `builders.tar.gz` file has finished extracting
ops.push(builderDirPromise);
try { try {
await Promise.all(ops); await Promise.all(ops);
} catch (err) { } catch (err) {
@@ -923,18 +923,6 @@ export default class DevServer {
} }
} }
async killBuilderDevServer(pid: number) {
const { debug } = this.output;
debug(`Killing builder dev server with PID ${pid}`);
this.devServerPids.delete(pid);
try {
process.kill(pid, 'SIGTERM');
debug(`Killed builder dev server with PID ${pid}`);
} catch (err) {
debug(`Failed to kill builder dev server with PID ${pid}: ${err}`);
}
}
async send404( async send404(
req: http.IncomingMessage, req: http.IncomingMessage,
res: http.ServerResponse, res: http.ServerResponse,
@@ -1228,11 +1216,9 @@ export default class DevServer {
res: http.ServerResponse, res: http.ServerResponse,
nowRequestId: string, nowRequestId: string,
nowConfig: NowConfig, nowConfig: NowConfig,
routes: Route[] | undefined = nowConfig.routes, routes: RouteConfig[] | undefined = nowConfig.routes,
callLevel: number = 0 callLevel: number = 0
) => { ) => {
const { debug } = this.output;
// If there is a double-slash present in the URL, // If there is a double-slash present in the URL,
// then perform a redirect to make it "clean". // then perform a redirect to make it "clean".
const parsed = url.parse(req.url || '/'); const parsed = url.parse(req.url || '/');
@@ -1249,16 +1235,16 @@ export default class DevServer {
return; return;
} }
debug(`Rewriting URL from "${req.url}" to "${location}"`); this.output.debug(`Rewriting URL from "${req.url}" to "${location}"`);
req.url = location; req.url = location;
} }
if (callLevel === 0) { await this.updateBuildMatches(nowConfig);
await this.updateBuildMatches(nowConfig);
}
if (this.blockingBuildsPromise) { if (this.blockingBuildsPromise) {
debug('Waiting for builds to complete before handling request'); this.output.debug(
'Waiting for builds to complete before handling request'
);
await this.blockingBuildsPromise; await this.blockingBuildsPromise;
} }
@@ -1299,7 +1285,7 @@ export default class DevServer {
Object.assign(destParsed.query, routeResult.uri_args); Object.assign(destParsed.query, routeResult.uri_args);
const destUrl = url.format(destParsed); const destUrl = url.format(destParsed);
debug(`ProxyPass: ${destUrl}`); this.output.debug(`ProxyPass: ${destUrl}`);
this.setResponseHeaders(res, nowRequestId); this.setResponseHeaders(res, nowRequestId);
return proxyPass(req, res, destUrl, this.output); return proxyPass(req, res, destUrl, this.output);
} }
@@ -1390,25 +1376,15 @@ export default class DevServer {
}); });
if (statusCode) { if (statusCode) {
// Set the `statusCode` as read-only so that `http-proxy` res.statusCode = statusCode;
// is not able to modify the value in the future
Object.defineProperty(res, 'statusCode', {
get() {
return statusCode;
},
/* eslint-disable @typescript-eslint/no-unused-vars */
set(_: number) {
/* ignore */
},
});
} }
const requestPath = dest.replace(/^\//, ''); const requestPath = dest.replace(/^\//, '');
if (!match) { if (!match) {
// If the dev command is started, then proxy to it // if the dev command is started, proxy to it
if (this.devProcessPort) { if (this.devProcessPort) {
debug('Proxying to frontend dev server'); this.output.debug('Proxy to dev command server');
this.setResponseHeaders(res, nowRequestId); this.setResponseHeaders(res, nowRequestId);
return proxyPass( return proxyPass(
req, req,
@@ -1441,7 +1417,7 @@ export default class DevServer {
origUrl.pathname = dest; origUrl.pathname = dest;
Object.assign(origUrl.query, uri_args); Object.assign(origUrl.query, uri_args);
const newUrl = url.format(origUrl); const newUrl = url.format(origUrl);
debug( this.output.debug(
`Checking build result's ${buildResult.routes.length} \`routes\` to match ${newUrl}` `Checking build result's ${buildResult.routes.length} \`routes\` to match ${newUrl}`
); );
const matchedRoute = await devRouter( const matchedRoute = await devRouter(
@@ -1451,7 +1427,9 @@ export default class DevServer {
this this
); );
if (matchedRoute.found && callLevel === 0) { if (matchedRoute.found && callLevel === 0) {
debug(`Found matching route ${matchedRoute.dest} for ${newUrl}`); this.output.debug(
`Found matching route ${matchedRoute.dest} for ${newUrl}`
);
req.url = newUrl; req.url = newUrl;
await this.serveProjectAsNowV2( await this.serveProjectAsNowV2(
req, req,
@@ -1465,76 +1443,7 @@ export default class DevServer {
} }
} }
// Before doing any asset matching, check if this builder supports the
// `startDevServer()` "optimization". In this case, the now dev server invokes
// `startDevServer()` on the builder for every HTTP request so that it boots
// up a single-serve dev HTTP server that now dev will proxy this HTTP request
// to. Once the proxied request is finished, now dev shuts down the dev
// server child process.
const { builder, package: builderPkg } = match.builderWithPkg;
if (typeof builder.startDevServer === 'function') {
let devServerResult: StartDevServerResult = null;
try {
devServerResult = await builder.startDevServer({
entrypoint: match.entrypoint,
workPath: this.cwd,
config: match.config || {},
env: this.envConfigs.runEnv || {},
});
} catch (err) {
// `startDevServer()` threw an error. Most likely this means the dev
// server process exited before sending the port information message
// (missing dependency at runtime, for example).
debug(`Error starting "${builderPkg.name}" dev server: ${err}`);
await this.sendError(
req,
res,
nowRequestId,
'NO_STATUS_CODE_FROM_DEV_SERVER',
502
);
return;
}
if (devServerResult) {
// When invoking lambda functions, the region where the lambda was invoked
// is also included in the request ID. So use the same `dev1` fake region.
nowRequestId = generateRequestId(this.podId, true);
const { port, pid } = devServerResult;
this.devServerPids.add(pid);
res.once('close', () => {
this.killBuilderDevServer(pid);
});
debug(
`Proxying to "${builderPkg.name}" dev server (port=${port}, pid=${pid})`
);
// Mix in the routing based query parameters
const parsed = url.parse(req.url || '/', true);
Object.assign(parsed.query, uri_args);
req.url = url.format({
pathname: parsed.pathname,
query: parsed.query,
});
this.setResponseHeaders(res, nowRequestId);
return proxyPass(
req,
res,
`http://localhost:${port}`,
this.output,
false
);
} else {
debug(`Skipping \`startDevServer()\` for ${match.entrypoint}`);
}
}
let foundAsset = findAsset(match, requestPath, nowConfig); let foundAsset = findAsset(match, requestPath, nowConfig);
if (!foundAsset && callLevel === 0) { if (!foundAsset && callLevel === 0) {
await this.triggerBuild(match, buildRequestPath, req); await this.triggerBuild(match, buildRequestPath, req);
@@ -1549,7 +1458,7 @@ export default class DevServer {
this.devProcessPort && this.devProcessPort &&
(!foundAsset || (foundAsset && foundAsset.asset.type !== 'Lambda')) (!foundAsset || (foundAsset && foundAsset.asset.type !== 'Lambda'))
) { ) {
debug('Proxying to frontend dev server'); this.output.debug('Proxy to dev command server');
this.setResponseHeaders(res, nowRequestId); this.setResponseHeaders(res, nowRequestId);
return proxyPass( return proxyPass(
req, req,
@@ -1566,7 +1475,7 @@ export default class DevServer {
} }
const { asset, assetKey } = foundAsset; const { asset, assetKey } = foundAsset;
debug( this.output.debug(
`Serving asset: [${asset.type}] ${assetKey} ${(asset as any) `Serving asset: [${asset.type}] ${assetKey} ${(asset as any)
.contentType || ''}` .contentType || ''}`
); );
@@ -1635,7 +1544,7 @@ export default class DevServer {
body: body.toString('base64'), body: body.toString('base64'),
}; };
debug(`Invoking lambda: "${assetKey}" with ${path}`); this.output.debug(`Invoking lambda: "${assetKey}" with ${path}`);
let result: InvokeResult; let result: InvokeResult;
try { try {
@@ -1778,7 +1687,7 @@ export default class DevServer {
const port = await getPort(); const port = await getPort();
const env: Env = { const env: EnvConfig = {
// Because of child process 'pipe' below, isTTY will be false. // Because of child process 'pipe' below, isTTY will be false.
// Most frameworks use `chalk`/`supports-color` so we enable it anyway. // Most frameworks use `chalk`/`supports-color` so we enable it anyway.
FORCE_COLOR: process.stdout.isTTY ? '1' : '0', FORCE_COLOR: process.stdout.isTTY ? '1' : '0',
@@ -1990,7 +1899,7 @@ async function shouldServe(
const shouldServe = await builder.shouldServe({ const shouldServe = await builder.shouldServe({
entrypoint: src, entrypoint: src,
files, files,
config: config || {}, config,
requestPath, requestPath,
workPath: devServer.cwd, workPath: devServer.cwd,
}); });

View File

@@ -1,6 +1,5 @@
import { basename, extname, join } from 'path'; import { basename, extname, join } from 'path';
import { FileFsRef, BuildOptions, ShouldServeOptions } from '@now/build-utils'; import { BuilderParams, BuildResult, ShouldServeParams } from './types';
import { BuildResult } from './types';
export const version = 2; export const version = 2;
@@ -8,7 +7,7 @@ export function build({
files, files,
entrypoint, entrypoint,
config, config,
}: BuildOptions): BuildResult { }: BuilderParams): BuildResult {
let path = entrypoint; let path = entrypoint;
const outputDir = config.zeroConfig ? config.outputDirectory : ''; const outputDir = config.zeroConfig ? config.outputDirectory : '';
const outputMatch = outputDir + '/'; const outputMatch = outputDir + '/';
@@ -17,7 +16,7 @@ export function build({
path = path.slice(outputMatch.length); path = path.slice(outputMatch.length);
} }
const output = { const output = {
[path]: files[entrypoint] as FileFsRef, [path]: files[entrypoint],
}; };
const watch = [path]; const watch = [path];
@@ -29,7 +28,7 @@ export function shouldServe({
files, files,
requestPath, requestPath,
config = {}, config = {},
}: ShouldServeOptions) { }: ShouldServeParams) {
let outputPrefix = ''; let outputPrefix = '';
const outputDir = config.zeroConfig ? config.outputDirectory : ''; const outputDir = config.zeroConfig ? config.outputDirectory : '';
const outputMatch = outputDir + '/'; const outputMatch = outputDir + '/';

View File

@@ -3,16 +3,11 @@ import { ChildProcess } from 'child_process';
import { Lambda as FunLambda } from '@zeit/fun'; import { Lambda as FunLambda } from '@zeit/fun';
import { import {
Builder as BuildConfig, Builder as BuildConfig,
BuildOptions,
PrepareCacheOptions,
ShouldServeOptions,
StartDevServerOptions,
StartDevServerResult,
Env,
FileBlob, FileBlob,
FileFsRef, FileFsRef,
Lambda, Lambda,
PackageJson, PackageJson,
Config,
} from '@now/build-utils'; } from '@now/build-utils';
import { NowConfig } from 'now-client'; import { NowConfig } from 'now-client';
import { HandleValue, Route } from '@now/routing-utils'; import { HandleValue, Route } from '@now/routing-utils';
@@ -27,21 +22,25 @@ export interface DevServerOptions {
frameworkSlug: string | null; frameworkSlug: string | null;
} }
export interface EnvConfig {
[name: string]: string | undefined;
}
export interface EnvConfigs { export interface EnvConfigs {
/** /**
* environment variables from `.env.build` file (deprecated) * environment variables from `.env.build` file (deprecated)
*/ */
buildEnv: Env; buildEnv: EnvConfig;
/** /**
* environment variables from `.env` file * environment variables from `.env` file
*/ */
runEnv: Env; runEnv: EnvConfig;
/** /**
* environment variables from `.env` and `.env.build` * environment variables from `.env` and `.env.build`
*/ */
allEnv: Env; allEnv: EnvConfig;
} }
export interface BuildMatch extends BuildConfig { export interface BuildMatch extends BuildConfig {
@@ -53,6 +52,8 @@ export interface BuildMatch extends BuildConfig {
buildProcess?: ChildProcess; buildProcess?: ChildProcess;
} }
export type RouteConfig = Route;
export interface HttpHandler { export interface HttpHandler {
(req: http.IncomingMessage, res: http.ServerResponse): void; (req: http.IncomingMessage, res: http.ServerResponse): void;
} }
@@ -61,9 +62,9 @@ export interface BuilderInputs {
[path: string]: FileFsRef; [path: string]: FileFsRef;
} }
export interface BuiltLambda extends Lambda { export type BuiltLambda = Lambda & {
fn?: FunLambda; fn?: FunLambda;
} };
export type BuilderOutput = BuiltLambda | FileFsRef | FileBlob; export type BuilderOutput = BuiltLambda | FileFsRef | FileBlob;
@@ -77,6 +78,28 @@ export interface CacheOutputs {
[path: string]: CacheOutput; [path: string]: CacheOutput;
} }
export interface BuilderParamsBase {
files: BuilderInputs;
entrypoint: string;
config: Config;
meta?: {
isDev?: boolean;
requestPath?: string | null;
filesChanged?: string[];
filesRemoved?: string[];
env?: EnvConfig;
buildEnv?: EnvConfig;
};
}
export interface BuilderParams extends BuilderParamsBase {
workPath: string;
}
export interface PrepareCacheParams extends BuilderParams {
cachePath: string;
}
export interface BuilderConfigAttr { export interface BuilderConfigAttr {
maxLambdaSize?: string | number; maxLambdaSize?: string | number;
} }
@@ -85,43 +108,49 @@ export interface Builder {
version?: 1 | 2 | 3 | 4; version?: 1 | 2 | 3 | 4;
config?: BuilderConfigAttr; config?: BuilderConfigAttr;
build( build(
opts: BuildOptions params: BuilderParams
): ):
| BuilderOutputs | BuilderOutputs
| BuildResult | BuildResult
| Promise<BuilderOutputs> | Promise<BuilderOutputs>
| Promise<BuildResult>; | Promise<BuildResult>;
shouldServe?(params: ShouldServeParams): boolean | Promise<boolean>;
prepareCache?( prepareCache?(
opts: PrepareCacheOptions params: PrepareCacheParams
): CacheOutputs | Promise<CacheOutputs>; ): CacheOutputs | Promise<CacheOutputs>;
shouldServe?(params: ShouldServeOptions): boolean | Promise<boolean>;
startDevServer?(opts: StartDevServerOptions): Promise<StartDevServerResult>;
} }
export interface BuildResult { export interface BuildResult {
output: BuilderOutputs; output: BuilderOutputs;
routes: Route[]; routes: RouteConfig[];
watch: string[]; watch: string[];
distPath?: string; distPath?: string;
} }
export interface BuildResultV3 { export interface BuildResultV3 {
output: Lambda; output: Lambda;
routes: Route[]; routes: RouteConfig[];
watch: string[]; watch: string[];
distPath?: string; distPath?: string;
} }
export interface BuildResultV4 { export interface BuildResultV4 {
output: { [filePath: string]: Lambda }; output: { [filePath: string]: Lambda };
routes: Route[]; routes: RouteConfig[];
watch: string[]; watch: string[];
distPath?: string; distPath?: string;
} }
export interface ShouldServeParams {
files: BuilderInputs;
entrypoint: string;
config?: Config;
requestPath: string;
workPath: string;
}
export interface BuilderWithPackage { export interface BuilderWithPackage {
runInProcess?: boolean; runInProcess?: boolean;
requirePath: string;
builder: Readonly<Builder>; builder: Readonly<Builder>;
package: Readonly<PackageJson>; package: Readonly<PackageJson>;
} }
@@ -144,7 +173,7 @@ export interface RouteResult {
// "uri_args": <object (key=value) list of new uri args to be passed along to dest > // "uri_args": <object (key=value) list of new uri args to be passed along to dest >
uri_args?: { [key: string]: any }; uri_args?: { [key: string]: any };
// "matched_route": <object of the route spec that matched> // "matched_route": <object of the route spec that matched>
matched_route?: Route; matched_route?: RouteConfig;
// "matched_route_idx": <integer of the index of the route matched> // "matched_route_idx": <integer of the index of the route matched>
matched_route_idx?: number; matched_route_idx?: number;
// "userDest": <boolean in case the destination was user defined> // "userDest": <boolean in case the destination was user defined>

View File

@@ -0,0 +1,103 @@
import { createHash } from 'crypto';
import {
mkdirp,
createWriteStream,
writeFile,
statSync,
chmodSync,
createReadStream,
} from 'fs-extra';
import pipe from 'promisepipe';
import { join } from 'path';
import fetch from 'node-fetch';
import { Output } from '../output/create-output';
import { builderDirPromise } from './builder-cache';
const YARN_VERSION = '1.17.3';
const YARN_SHA = '77f28b2793ca7d0ab5bd5da072afc423f7fdf733';
const YARN_URL = `https://github.com/yarnpkg/yarn/releases/download/v${YARN_VERSION}/yarn-${YARN_VERSION}.js`;
function plusxSync(file: string): void {
const s = statSync(file);
const newMode = s.mode | 64 | 8 | 1;
if (s.mode === newMode) {
return;
}
const base8 = newMode.toString(8).slice(-3);
chmodSync(file, base8);
}
function getSha1(filePath: string): Promise<string | null> {
return new Promise((resolve, reject) => {
const hash = createHash('sha1');
const stream = createReadStream(filePath);
stream.on('error', err => {
if (err.code === 'ENOENT') {
resolve(null);
} else {
reject(err);
}
});
stream.on('data', chunk => hash.update(chunk));
stream.on('end', () => resolve(hash.digest('hex')));
});
}
async function installYarn(output: Output): Promise<string> {
// Loosely based on https://yarnpkg.com/install.sh
const dirName = await builderDirPromise;
const yarnBin = join(dirName, 'yarn');
const sha1 = await getSha1(yarnBin);
if (sha1 === YARN_SHA) {
output.debug('The yarn executable is already cached, not re-downloading');
return dirName;
}
output.debug(`Creating directory ${dirName}`);
await mkdirp(dirName);
output.debug(`Finished creating ${dirName}`);
output.debug(`Downloading ${YARN_URL}`);
const response = await fetch(YARN_URL, {
compress: false,
redirect: 'follow',
});
if (response.status !== 200) {
throw new Error(`Received invalid response: ${await response.text()}`);
}
const target = createWriteStream(yarnBin);
await pipe(
response.body,
target
);
output.debug(`Finished downloading yarn ${yarnBin}`);
output.debug(`Making the yarn binary executable`);
plusxSync(yarnBin);
output.debug(`Finished making the yarn binary executable`);
if (process.platform === 'win32') {
// The `yarn.cmd` file is necessary for `yarn` to be executable
// when running `now dev` through cmd.exe
await writeFile(
`${yarnBin}.cmd`,
[
'@echo off',
'@SETLOCAL',
'@SET PATHEXT=%PATHEXT:;.JS;=;%',
'node "%~dp0\\yarn" %*',
].join('\r\n')
);
}
return dirName;
}
export async function getYarnPath(output: Output): Promise<string> {
return installYarn(output);
}

View File

@@ -0,0 +1,20 @@
import { EventEmitter } from 'events';
export function once<T>(emitter: EventEmitter, name: string): Promise<T> {
return new Promise((resolve, reject) => {
function cleanup() {
emitter.removeListener(name, onEvent);
emitter.removeListener('error', onError);
}
function onEvent(arg: T) {
cleanup();
resolve(arg);
}
function onError(err: Error) {
cleanup();
reject(err);
}
emitter.on(name, onEvent);
emitter.on('error', onError);
});
}

View File

@@ -1,6 +1,13 @@
import _pkg from '../../package.json'; import path from 'path';
import { PackageJson } from '@now/build-utils'; import pkg from '../../package.json';
const pkg: PackageJson = _pkg; try {
const distDir = path.dirname(process.execPath);
// @ts-ignore
pkg._npmPkg = require(`${path.join(distDir, '../../package.json')}`);
} catch (err) {
// @ts-ignore
pkg._npmPkg = null;
}
export default pkg; export default pkg;

View File

@@ -1,4 +0,0 @@
import _treeKill from 'tree-kill';
import { promisify } from 'util';
export const treeKill = promisify(_treeKill);

View File

@@ -1,18 +1,17 @@
import test from 'ava'; import test from 'ava';
import npa from 'npm-package-arg'; import { filterPackage } from '../src/util/dev/builder-cache';
import { filterPackage, isBundledBuilder } from '../src/util/dev/builder-cache';
test('[dev-builder] filter install "latest", cached canary', t => { test('[dev-builder] filter install "latest", cached canary', async t => {
const buildersPkg = { const buildersPkg = {
dependencies: { dependencies: {
'@now/build-utils': '0.0.1-canary.0', '@now/build-utils': '0.0.1-canary.0',
}, },
}; };
const result = filterPackage('@now/build-utils', 'canary', buildersPkg, {}); const result = filterPackage('@now/build-utils', 'canary', buildersPkg);
t.is(result, true); t.is(result, true);
}); });
test('[dev-builder] filter install "canary", cached stable', t => { test('[dev-builder] filter install "canary", cached stable', async t => {
const buildersPkg = { const buildersPkg = {
dependencies: { dependencies: {
'@now/build-utils': '0.0.1', '@now/build-utils': '0.0.1',
@@ -21,23 +20,22 @@ test('[dev-builder] filter install "canary", cached stable', t => {
const result = filterPackage( const result = filterPackage(
'@now/build-utils@canary', '@now/build-utils@canary',
'latest', 'latest',
buildersPkg, buildersPkg
{}
); );
t.is(result, true); t.is(result, true);
}); });
test('[dev-builder] filter install "latest", cached stable', t => { test('[dev-builder] filter install "latest", cached stable', async t => {
const buildersPkg = { const buildersPkg = {
dependencies: { dependencies: {
'@now/build-utils': '0.0.1', '@now/build-utils': '0.0.1',
}, },
}; };
const result = filterPackage('@now/build-utils', 'latest', buildersPkg, {}); const result = filterPackage('@now/build-utils', 'latest', buildersPkg);
t.is(result, false); t.is(result, false);
}); });
test('[dev-builder] filter install "canary", cached canary', t => { test('[dev-builder] filter install "canary", cached canary', async t => {
const buildersPkg = { const buildersPkg = {
dependencies: { dependencies: {
'@now/build-utils': '0.0.1-canary.0', '@now/build-utils': '0.0.1-canary.0',
@@ -46,199 +44,87 @@ test('[dev-builder] filter install "canary", cached canary', t => {
const result = filterPackage( const result = filterPackage(
'@now/build-utils@canary', '@now/build-utils@canary',
'canary', 'canary',
buildersPkg, buildersPkg
{}
); );
t.is(result, false); t.is(result, false);
}); });
test('[dev-builder] filter install URL, cached stable', t => { test('[dev-builder] filter install URL, cached stable', async t => {
const buildersPkg = { const buildersPkg = {
dependencies: { dependencies: {
'@now/build-utils': '0.0.1', '@now/build-utils': '0.0.1',
}, },
}; };
const result = filterPackage( const result = filterPackage('https://tarball.now.sh', 'latest', buildersPkg);
'https://tarball.now.sh',
'latest',
buildersPkg,
{}
);
t.is(result, true); t.is(result, true);
}); });
test('[dev-builder] filter install URL, cached canary', t => { test('[dev-builder] filter install URL, cached canary', async t => {
const buildersPkg = { const buildersPkg = {
dependencies: { dependencies: {
'@now/build-utils': '0.0.1-canary.0', '@now/build-utils': '0.0.1-canary.0',
}, },
}; };
const result = filterPackage( const result = filterPackage('https://tarball.now.sh', 'canary', buildersPkg);
'https://tarball.now.sh',
'canary',
buildersPkg,
{}
);
t.is(result, true); t.is(result, true);
}); });
test('[dev-builder] filter install "latest", cached URL - stable', t => { test('[dev-builder] filter install "latest", cached URL - stable', async t => {
const buildersPkg = { const buildersPkg = {
dependencies: { dependencies: {
'@now/build-utils': 'https://tarball.now.sh', '@now/build-utils': 'https://tarball.now.sh',
}, },
}; };
const result = filterPackage('@now/build-utils', 'latest', buildersPkg, {}); const result = filterPackage('@now/build-utils', 'latest', buildersPkg);
t.is(result, true); t.is(result, true);
}); });
test('[dev-builder] filter install "latest", cached URL - canary', t => { test('[dev-builder] filter install "latest", cached URL - canary', async t => {
const buildersPkg = { const buildersPkg = {
dependencies: { dependencies: {
'@now/build-utils': 'https://tarball.now.sh', '@now/build-utils': 'https://tarball.now.sh',
}, },
}; };
const result = filterPackage('@now/build-utils', 'canary', buildersPkg, {}); const result = filterPackage('@now/build-utils', 'canary', buildersPkg);
t.is(result, true); t.is(result, true);
}); });
test('[dev-builder] filter install not bundled version, cached same version', t => { test('[dev-builder] filter install not bundled version, cached same version', async t => {
const buildersPkg = { const buildersPkg = {
dependencies: { dependencies: {
'not-bundled-package': '0.0.1', 'not-bundled-package': '0.0.1',
}, },
}; };
const result = filterPackage( const result = filterPackage('not-bundled-package@0.0.1', '_', buildersPkg);
'not-bundled-package@0.0.1',
'_',
buildersPkg,
{}
);
t.is(result, false); t.is(result, false);
}); });
test('[dev-builder] filter install not bundled version, cached different version', t => { test('[dev-builder] filter install not bundled version, cached different version', async t => {
const buildersPkg = { const buildersPkg = {
dependencies: { dependencies: {
'not-bundled-package': '0.0.9', 'not-bundled-package': '0.0.9',
}, },
}; };
const result = filterPackage( const result = filterPackage('not-bundled-package@0.0.1', '_', buildersPkg);
'not-bundled-package@0.0.1',
'_',
buildersPkg,
{}
);
t.is(result, true); t.is(result, true);
}); });
test('[dev-builder] filter install not bundled stable, cached version', t => { test('[dev-builder] filter install not bundled stable, cached version', async t => {
const buildersPkg = { const buildersPkg = {
dependencies: { dependencies: {
'not-bundled-package': '0.0.1', 'not-bundled-package': '0.0.1',
}, },
}; };
const result = filterPackage('not-bundled-package', '_', buildersPkg, {}); const result = filterPackage('not-bundled-package', '_', buildersPkg);
t.is(result, true); t.is(result, true);
}); });
test('[dev-builder] filter install not bundled tagged, cached tagged', t => { test('[dev-builder] filter install not bundled tagged, cached tagged', async t => {
const buildersPkg = { const buildersPkg = {
dependencies: { dependencies: {
'not-bundled-package': '16.9.0-alpha.0', 'not-bundled-package': '16.9.0-alpha.0',
}, },
}; };
const result = filterPackage( const result = filterPackage('not-bundled-package@alpha', '_', buildersPkg);
'not-bundled-package@alpha',
'_',
buildersPkg,
{}
);
t.is(result, true); t.is(result, true);
}); });
test('[dev-builder] isBundledBuilder() - stable', t => {
const nowCliPkg = {
dependencies: {
'@now/node': '1.5.2',
},
};
// "canary" tag
{
const parsed = npa('@now/node@canary');
const result = isBundledBuilder(parsed, nowCliPkg);
t.is(result, false);
}
// "latest" tag
{
const parsed = npa('@now/node');
const result = isBundledBuilder(parsed, nowCliPkg);
t.is(result, true);
}
// specific matching version
{
const parsed = npa('@now/node@1.5.2');
const result = isBundledBuilder(parsed, nowCliPkg);
t.is(result, true);
}
// specific non-matching version
{
const parsed = npa('@now/node@1.5.1');
const result = isBundledBuilder(parsed, nowCliPkg);
t.is(result, false);
}
// URL
{
const parsed = npa('https://example.com');
const result = isBundledBuilder(parsed, nowCliPkg);
t.is(result, false);
}
});
test('[dev-builder] isBundledBuilder() - canary', t => {
const nowCliPkg = {
dependencies: {
'@now/node': '1.5.2-canary.3',
},
};
// "canary" tag
{
const parsed = npa('@now/node@canary');
const result = isBundledBuilder(parsed, nowCliPkg);
t.is(result, true);
}
// "latest" tag
{
const parsed = npa('@now/node');
const result = isBundledBuilder(parsed, nowCliPkg);
t.is(result, false);
}
// specific matching version
{
const parsed = npa('@now/node@1.5.2-canary.3');
const result = isBundledBuilder(parsed, nowCliPkg);
t.is(result, true);
}
// specific non-matching version
{
const parsed = npa('@now/node@1.5.2-canary.2');
const result = isBundledBuilder(parsed, nowCliPkg);
t.is(result, false);
}
// URL
{
const parsed = npa('https://example.com');
const result = isBundledBuilder(parsed, nowCliPkg);
t.is(result, false);
}
});

View File

@@ -259,7 +259,6 @@ test(
testFixture('now-dev-static-routes', async (t, server) => { testFixture('now-dev-static-routes', async (t, server) => {
{ {
const res = await fetch(`${server.address}/`); const res = await fetch(`${server.address}/`);
t.is(res.status, 200);
const body = await res.text(); const body = await res.text();
t.is(body, '<body>Hello!</body>\n'); t.is(body, '<body>Hello!</body>\n');
} }
@@ -271,7 +270,6 @@ test(
testFixture('now-dev-static-build-routing', async (t, server) => { testFixture('now-dev-static-build-routing', async (t, server) => {
{ {
const res = await fetch(`${server.address}/api/date`); const res = await fetch(`${server.address}/api/date`);
t.is(res.status, 200);
const body = await res.text(); const body = await res.text();
t.is(body.startsWith('The current date:'), true); t.is(body.startsWith('The current date:'), true);
} }

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,4 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1

View File

@@ -1,3 +1,3 @@
module.exports = (_req, res) => { export default (_req, res) => {
res.end('current date: ' + new Date().toISOString()); res.end('current date: ' + new Date().toISOString());
}; };

View File

@@ -1,3 +1,3 @@
module.exports = (_req, res) => { export default (_req, res) => {
res.end('random number: ' + Math.random()); res.end('random number: ' + Math.random());
}; };

View File

@@ -110,10 +110,7 @@ async function exec(directory, args = []) {
async function runNpmInstall(fixturePath) { async function runNpmInstall(fixturePath) {
if (await fs.exists(join(fixturePath, 'package.json'))) { if (await fs.exists(join(fixturePath, 'package.json'))) {
await execa('yarn', ['install'], { return execa('yarn', ['install'], { cwd: fixturePath, shell: true });
cwd: fixturePath,
shell: true,
});
} }
} }
@@ -294,9 +291,6 @@ function testFixtureStdio(
{ env } { env }
); );
dev.stdout.pipe(process.stdout);
dev.stderr.pipe(process.stderr);
dev.stdout.on('data', data => { dev.stdout.on('data', data => {
stdoutList.push(data); stdoutList.push(data);
}); });

View File

@@ -30,6 +30,14 @@ ncc build ../../node_modules/source-map-support/register -e @now/build-utils -o
mv dist/source-map-support/index.js dist/source-map-support.js mv dist/source-map-support/index.js dist/source-map-support.js
rm -rf dist/source-map-support rm -rf dist/source-map-support
ncc build src/index.ts -e @now/build-utils -e typescript -o dist/main # build typescript
ncc build ../../node_modules/typescript/lib/typescript -e @now/build-utils -o dist/typescript
mv dist/typescript/index.js dist/typescript.js
mkdir -p dist/typescript/lib
mv dist/typescript/typescript/lib/*.js dist/typescript/lib/
mv dist/typescript/typescript/lib/*.d.ts dist/typescript/lib/
rm -r dist/typescript/typescript
ncc build src/index.ts -e @now/build-utils -o dist/main
mv dist/main/index.js dist/index.js mv dist/main/index.js dist/index.js
rm -rf dist/main rm -rf dist/main

View File

@@ -11,36 +11,32 @@
}, },
"scripts": { "scripts": {
"build": "./build.sh", "build": "./build.sh",
"test-unit": "jest --env node --verbose --runInBand --bail test/helpers.test.js", "test-unit": "jest --env node --verbose --runInBand test/helpers.test.js",
"test-integration-once": "jest --env node --verbose --runInBand --bail test/integration.test.js", "test-integration-once": "jest --env node --verbose --runInBand test/integration.test.js",
"prepublishOnly": "npm run build" "prepublishOnly": "npm run build"
}, },
"files": [ "files": [
"dist" "dist"
], ],
"dependencies": { "dependencies": {
"@types/node": "*", "@types/node": "*"
"ts-node": "8.9.1",
"typescript": "3.8.3"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.5.0", "@babel/core": "7.5.0",
"@babel/plugin-transform-modules-commonjs": "7.5.0", "@babel/plugin-transform-modules-commonjs": "7.5.0",
"@tootallnate/once": "1.1.2",
"@types/aws-lambda": "8.10.19", "@types/aws-lambda": "8.10.19",
"@types/content-type": "1.1.3", "@types/content-type": "1.1.3",
"@types/cookie": "0.3.3", "@types/cookie": "0.3.3",
"@types/etag": "1.8.0", "@types/etag": "1.8.0",
"@types/fs-extra": "^5.0.5",
"@types/test-listen": "1.1.0", "@types/test-listen": "1.1.0",
"@zeit/ncc": "0.20.4", "@zeit/ncc": "0.20.4",
"@zeit/node-file-trace": "0.5.1", "@zeit/node-file-trace": "0.5.1",
"content-type": "1.0.4", "content-type": "1.0.4",
"cookie": "0.4.0", "cookie": "0.4.0",
"etag": "1.8.1", "etag": "1.8.1",
"fs-extra": "7.0.0",
"node-fetch": "2.6.0", "node-fetch": "2.6.0",
"source-map-support": "0.5.12", "source-map-support": "0.5.12",
"test-listen": "1.1.0" "test-listen": "1.1.0",
"typescript": "3.5.2"
} }
} }

View File

@@ -1,75 +0,0 @@
import { register } from 'ts-node';
// Use the project's version of TypeScript if available,
// otherwise fall back to using the copy that `@now/node` uses.
let compiler: string;
try {
compiler = require.resolve('typescript', {
paths: [process.cwd(), __dirname],
});
} catch (e) {
compiler = 'typescript';
}
register({
compiler,
compilerOptions: {
allowJs: true,
esModuleInterop: true,
jsx: 'react',
},
transpileOnly: true,
});
import http from 'http';
import path from 'path';
import { createServerWithHelpers } from './helpers';
function listen(
server: http.Server,
port: number,
host: string
): Promise<void> {
return new Promise(resolve => {
server.listen(port, host, () => {
resolve();
});
});
}
async function main() {
const entrypoint = process.env.NOW_DEV_ENTRYPOINT;
delete process.env.NOW_DEV_ENTRYPOINT;
if (!entrypoint) {
throw new Error('`NOW_DEV_ENTRYPOINT` must be defined');
}
const config = JSON.parse(process.env.NOW_DEV_CONFIG || '{}');
delete process.env.NOW_DEV_CONFIG;
const shouldAddHelpers = !(
config.helpers === false || process.env.NODEJS_HELPERS === '0'
);
const entrypointPath = path.join(process.cwd(), entrypoint);
const handler = await import(entrypointPath);
const server = shouldAddHelpers
? createServerWithHelpers(handler.default)
: http.createServer(handler.default);
await listen(server, 0, '127.0.0.1');
const address = server.address();
if (typeof process.send === 'function') {
process.send(address);
} else {
console.log('Dev server listening:', address);
}
}
main().catch(err => {
console.error(err);
process.exit(1);
});

View File

@@ -6,7 +6,6 @@ import {
NowRequestBody, NowRequestBody,
} from './types'; } from './types';
import { Server } from 'http'; import { Server } from 'http';
import { Readable } from 'stream';
import { Bridge } from './bridge'; import { Bridge } from './bridge';
function getBodyParser(req: NowRequest, body: Buffer) { function getBodyParser(req: NowRequest, body: Buffer) {
@@ -82,7 +81,6 @@ function setCharset(type: string, charset: string) {
return format(parsed); return format(parsed);
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function createETag(body: any, encoding: 'utf8' | undefined) { function createETag(body: any, encoding: 'utf8' | undefined) {
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const etag = require('etag'); const etag = require('etag');
@@ -90,7 +88,6 @@ function createETag(body: any, encoding: 'utf8' | undefined) {
return etag(buf, { weak: true }); return etag(buf, { weak: true });
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function send(req: NowRequest, res: NowResponse, body: any): NowResponse { function send(req: NowRequest, res: NowResponse, body: any): NowResponse {
let chunk: unknown = body; let chunk: unknown = body;
let encoding: 'utf8' | undefined; let encoding: 'utf8' | undefined;
@@ -188,7 +185,6 @@ function send(req: NowRequest, res: NowResponse, body: any): NowResponse {
return res; return res;
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function json(req: NowRequest, res: NowResponse, jsonBody: any): NowResponse { function json(req: NowRequest, res: NowResponse, jsonBody: any): NowResponse {
const body = JSON.stringify(jsonBody); const body = JSON.stringify(jsonBody);
@@ -237,24 +233,9 @@ function setLazyProp<T>(req: NowRequest, prop: string, getter: () => T) {
}); });
} }
export function rawBody(readable: Readable): Promise<Buffer> {
return new Promise((resolve, reject) => {
let bytes = 0;
const chunks: Buffer[] = [];
readable.on('error', reject);
readable.on('data', chunk => {
chunks.push(chunk);
bytes += chunk.length;
});
readable.on('end', () => {
resolve(Buffer.concat(chunks, bytes));
});
});
}
export function createServerWithHelpers( export function createServerWithHelpers(
handler: (req: NowRequest, res: NowResponse) => void | Promise<void>, listener: (req: NowRequest, res: NowResponse) => void | Promise<void>,
bridge?: Bridge bridge: Bridge
) { ) {
const server = new Server(async (_req, _res) => { const server = new Server(async (_req, _res) => {
const req = _req as NowRequest; const req = _req as NowRequest;
@@ -266,23 +247,21 @@ export function createServerWithHelpers(
// don't expose this header to the client // don't expose this header to the client
delete req.headers['x-now-bridge-request-id']; delete req.headers['x-now-bridge-request-id'];
let body: Buffer; if (typeof reqId !== 'string') {
if (typeof reqId === 'string' && bridge) { throw new ApiError(500, 'Internal Server Error');
const event = bridge.consumeEvent(reqId);
body = event.body;
} else {
body = await rawBody(req);
} }
const event = bridge.consumeEvent(reqId);
setLazyProp<NowRequestCookies>(req, 'cookies', getCookieParser(req)); setLazyProp<NowRequestCookies>(req, 'cookies', getCookieParser(req));
setLazyProp<NowRequestQuery>(req, 'query', getQueryParser(req)); setLazyProp<NowRequestQuery>(req, 'query', getQueryParser(req));
setLazyProp<NowRequestBody>(req, 'body', getBodyParser(req, body)); setLazyProp<NowRequestBody>(req, 'body', getBodyParser(req, event.body));
res.status = statusCode => status(res, statusCode); res.status = statusCode => status(res, statusCode);
res.send = body => send(req, res, body); res.send = body => send(req, res, body);
res.json = jsonBody => json(req, res, jsonBody); res.json = jsonBody => json(req, res, jsonBody);
await handler(req, res); await listener(req, res);
} catch (err) { } catch (err) {
if (err instanceof ApiError) { if (err instanceof ApiError) {
sendError(res, err.statusCode, err.message); sendError(res, err.statusCode, err.message);

View File

@@ -1,16 +1,13 @@
import { fork, spawn } from 'child_process';
import { readFileSync, lstatSync, readlinkSync, statSync } from 'fs';
import { writeJSON, remove } from 'fs-extra';
import { import {
basename, basename,
dirname, dirname,
extname,
join, join,
relative, relative,
resolve, resolve,
sep, sep,
parse as parsePath, parse as parsePath,
} from 'path'; } from 'path';
import nodeFileTrace from '@zeit/node-file-trace';
import { import {
glob, glob,
download, download,
@@ -26,21 +23,16 @@ import {
getSpawnOptions, getSpawnOptions,
PrepareCacheOptions, PrepareCacheOptions,
BuildOptions, BuildOptions,
StartDevServerOptions,
StartDevServerResult,
shouldServe, shouldServe,
Config, Config,
debug, debug,
isSymbolicLink, isSymbolicLink,
walkParentDirs,
} from '@now/build-utils'; } from '@now/build-utils';
import once from '@tootallnate/once';
import nodeFileTrace from '@zeit/node-file-trace';
import { makeNowLauncher, makeAwsLauncher } from './launcher';
import { Register, register } from './typescript';
export { shouldServe }; export { shouldServe };
export { NowRequest, NowResponse } from './types'; export { NowRequest, NowResponse } from './types';
import { makeNowLauncher, makeAwsLauncher } from './launcher';
import { readFileSync, lstatSync, readlinkSync, statSync } from 'fs';
import { Register, register } from './typescript';
interface CompilerConfig { interface CompilerConfig {
debug?: boolean; debug?: boolean;
@@ -56,19 +48,6 @@ interface DownloadOptions {
meta: Meta; meta: Meta;
} }
interface PortInfo {
port: number;
}
function isPortInfo(v: any): v is PortInfo {
return v && typeof v.port === 'number';
}
const tscPath = resolve(
dirname(require.resolve(eval('"typescript"'))),
'../bin/tsc'
);
// eslint-disable-next-line no-useless-escape // eslint-disable-next-line no-useless-escape
const libPathRegEx = /^node_modules|[\/\\]node_modules[\/\\]/; const libPathRegEx = /^node_modules|[\/\\]node_modules[\/\\]/;
@@ -335,7 +314,6 @@ export async function build({
const shouldAddHelpers = !( const shouldAddHelpers = !(
config.helpers === false || process.env.NODEJS_HELPERS === '0' config.helpers === false || process.env.NODEJS_HELPERS === '0'
); );
const awsLambdaHandler = getAWSLambdaHandler(entrypoint, config); const awsLambdaHandler = getAWSLambdaHandler(entrypoint, config);
const { const {
@@ -415,89 +393,3 @@ export async function prepareCache({
const cache = await glob('node_modules/**', workPath); const cache = await glob('node_modules/**', workPath);
return cache; return cache;
} }
export async function startDevServer(
opts: StartDevServerOptions
): Promise<StartDevServerResult> {
const { entrypoint, workPath, config, env } = opts;
const devServerPath = join(__dirname, 'dev-server.js');
const child = fork(devServerPath, [], {
cwd: workPath,
env: {
...process.env,
...env,
NOW_DEV_ENTRYPOINT: entrypoint,
NOW_DEV_CONFIG: JSON.stringify(config),
},
});
const { pid } = child;
const onMessage = once<{ port: number }>(child, 'message');
const onExit = once.spread<[number, string | null]>(child, 'exit');
const result = await Promise.race([onMessage, onExit]);
onExit.cancel();
onMessage.cancel();
if (isPortInfo(result)) {
// "message" event
const ext = extname(entrypoint);
if (ext === '.ts' || ext === '.tsx') {
// Invoke `tsc --noEmit` asynchronously in the background, so
// that the HTTP request is not blocked by the type checking.
doTypeCheck(opts).catch((err: Error) => {
console.error('Type check for %j failed:', entrypoint, err);
});
}
return { port: result.port, pid };
} else {
// "exit" event
throw new Error(
`Failed to start dev server for "${entrypoint}" (code=${result[0]}, signal=${result[1]})`
);
}
}
async function doTypeCheck({
entrypoint,
workPath,
}: StartDevServerOptions): Promise<void> {
// In order to type-check a single file, a temporary tsconfig
// file needs to be created that inherits from the base one :(
// See: https://stackoverflow.com/a/44748041/376773
const id = Math.random()
.toString(32)
.substring(2);
const tempConfigName = `.tsconfig-${id}.json`;
const projectTsConfig = await walkParentDirs({
base: workPath,
start: join(workPath, dirname(entrypoint)),
filename: 'tsconfig.json',
});
const tsconfig = {
extends: projectTsConfig || undefined,
include: [entrypoint],
};
await writeJSON(tempConfigName, tsconfig);
const child = spawn(
process.execPath,
[
tscPath,
'--project',
tempConfigName,
'--noEmit',
'--allowJs',
'--esModuleInterop',
'--jsx',
'react',
],
{
cwd: workPath,
stdio: 'inherit',
}
);
await once.spread<[number, string | null]>(child, 'exit');
await remove(tempConfigName);
}

View File

@@ -156,7 +156,7 @@ export function register(opts: Options = {}): Register {
paths: [options.project || cwd, nowNodeBase], paths: [options.project || cwd, nowNodeBase],
}); });
} catch (e) { } catch (e) {
compiler = require.resolve(eval('"typescript"')); compiler = require.resolve(eval('"./typescript"'));
} }
//eslint-disable-next-line @typescript-eslint/no-var-requires //eslint-disable-next-line @typescript-eslint/no-var-requires
const ts: typeof _ts = require(compiler); const ts: typeof _ts = require(compiler);

View File

@@ -1422,10 +1422,10 @@
dependencies: dependencies:
defer-to-connect "^1.0.1" defer-to-connect "^1.0.1"
"@tootallnate/once@1", "@tootallnate/once@1.1.2": "@tootallnate/once@1":
version "1.1.2" version "1.0.0"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.0.0.tgz#9c13c2574c92d4503b005feca8f2e16cc1611506"
integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== integrity sha512-KYyTT/T6ALPkIRd2Ge080X/BsXvy9O0hcWTtMWkPvwAwF99+vn6Dv4GzrFT/Nn1LePr+FFDbRXXlqmsy9lw2zA==
"@types/ansi-escapes@3.0.0": "@types/ansi-escapes@3.0.0":
version "3.0.0" version "3.0.0"
@@ -9914,14 +9914,6 @@ source-map-support@^0.5.12, source-map-support@^0.5.6:
buffer-from "^1.0.0" buffer-from "^1.0.0"
source-map "^0.6.0" source-map "^0.6.0"
source-map-support@^0.5.17:
version "0.5.19"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
dependencies:
buffer-from "^1.0.0"
source-map "^0.6.0"
source-map-url@^0.4.0: source-map-url@^0.4.0:
version "0.4.0" version "0.4.0"
resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
@@ -10734,17 +10726,6 @@ ts-node@8.3.0:
source-map-support "^0.5.6" source-map-support "^0.5.6"
yn "^3.0.0" yn "^3.0.0"
ts-node@8.9.1:
version "8.9.1"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.9.1.tgz#2f857f46c47e91dcd28a14e052482eb14cfd65a5"
integrity sha512-yrq6ODsxEFTLz0R3BX2myf0WBCSQh9A+py8PBo1dCzWIOcvisbyH6akNKqDHMgXePF2kir5mm5JXJTH3OUJYOQ==
dependencies:
arg "^4.1.0"
diff "^4.0.1"
make-error "^1.1.1"
source-map-support "^0.5.17"
yn "3.1.1"
tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
version "1.10.0" version "1.10.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
@@ -10828,11 +10809,6 @@ typescript@3.6.4:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.4.tgz#b18752bb3792bc1a0281335f7f6ebf1bbfc5b91d" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.4.tgz#b18752bb3792bc1a0281335f7f6ebf1bbfc5b91d"
integrity sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg== integrity sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==
typescript@3.8.3:
version "3.8.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061"
integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==
uglify-js@^3.1.4: uglify-js@^3.1.4:
version "3.7.3" version "3.7.3"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.7.3.tgz#f918fce9182f466d5140f24bb0ff35c2d32dcc6a" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.7.3.tgz#f918fce9182f466d5140f24bb0ff35c2d32dcc6a"
@@ -11461,6 +11437,11 @@ yargs@^13.3.0:
y18n "^4.0.0" y18n "^4.0.0"
yargs-parser "^13.1.1" yargs-parser "^13.1.1"
yarn@1.22.0:
version "1.22.0"
resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.0.tgz#acf82906e36bcccd1ccab1cfb73b87509667c881"
integrity sha512-KMHP/Jq53jZKTY9iTUt3dIVl/be6UPs2INo96+BnZHLKxYNTfwMmlgHTaMWyGZoO74RI4AIFvnWhYrXq2USJkg==
yauzl-clone@^1.0.4: yauzl-clone@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/yauzl-clone/-/yauzl-clone-1.0.4.tgz#8bc6d293b17cc98802bbbed2e289d18e7697c96c" resolved "https://registry.yarnpkg.com/yauzl-clone/-/yauzl-clone-1.0.4.tgz#8bc6d293b17cc98802bbbed2e289d18e7697c96c"
@@ -11497,7 +11478,7 @@ yazl@2.4.3:
dependencies: dependencies:
buffer-crc32 "~0.2.3" buffer-crc32 "~0.2.3"
yn@3.1.1, yn@^3.0.0: yn@^3.0.0:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==