Compare commits

..

35 Commits

Author SHA1 Message Date
Andy Bitz
bd6e0f9f93 Publish Stable
- @now/next@2.3.0
2019-12-17 15:30:37 +01:00
Andy
aa8d002309 [now-cli][now-build-utils] Adjust tests for new detectors (#3443)
* Adjust README

* Fix now-dev test

* Add hugo to the PATH

* Fix Hugo build

* Add more logging

* Resolve path

* Do not use the build script as dev command

* Update yarn.lock file

* fetch with retry

* Remove sh from README

* Use Set for Hugo config files
2019-12-17 15:27:12 +01:00
Andy Bitz
42ce9aca86 Publish Canary
- @now/build-utils@1.2.1-canary.0
 - now@16.7.1-canary.0
 - @now/next@2.2.1-canary.1
 - @now/routing-utils@1.4.1-canary.0
 - @now/static-build@0.14.1-canary.0
2019-12-17 00:25:35 +01:00
Andy
156f596189 [now-cli][now-static-build] Add support for buildCommand, devCommand and outputDirectory (#3434)
* Revert "Revert "[now-static-build] Add support `buildCommand`, `devCommand` and `outputDirectory` (#3422)" (#3428)"

This reverts commit f7b4dd4458.

* Handle generic node projects with /public

* Remove .only

* Ensure node_modules/.bin is also available during `now dev`

* Remove config log

* Adjust test

* Fix integration tests

* Fix public check

* Remove build + public

* Remove _scan

* Remove any casting

* Use `spawnCommand` for dev

* Remove unused import

* Remove cross-spawn

* Fix null config

* Fix build

* Only do a single Buffer.concat
2019-12-17 00:21:58 +01:00
JJ Kasper
8acfd5bf71 [now-next] Add testing for export handling (#3437)
Follow up to #3431 adding tests for this behavior

Tests to add:
- [x] custom routes: redirects
- [x] custom routes: rewrites
- [x] confirming each test is actually a `next export` deploy somehow
2019-12-16 22:47:36 +00:00
Andy
76c99ebb28 [now-build-utils] Restore previous detectors (#3441) 2019-12-16 23:20:48 +01:00
Alejandro Pacheco
5fb119b99c [now-cli] Changed scale error message (#3394)
The error message does not give a clear reason of why it failed
2019-12-16 21:44:40 +00:00
Steven
99b766d9cb [now-routing-utils] Add validation for handle:hit and handle:miss (#3438)
This PR adds initial support for `handle: hit` and `handle: miss` routes.
2019-12-16 20:56:48 +00:00
Andy
c207cf9b40 [now-cli] Change --team warning style (#3439)
* [now-cli] Change warning style

* Adjust test

* Fix screen error
2019-12-16 19:38:43 +01:00
Andy
dd00ac4621 [now-cli] Change automatic version detection message (#3440)
* [now-cli] Change automatic version detection message

* Fix unit test
2019-12-16 19:16:23 +01:00
Allen Hai
3d18a067a0 remove unused vars from builder-cache file (#3411) 2019-12-16 09:03:58 -05:00
Tim Neutkens
c48571a799 Publish Canary
- @now/next@2.2.1-canary.0
2019-12-16 12:49:02 +01:00
Joe Haddad
6eeb6983d9 [now-next] Support for next export (#3431)
* Add Support for `next export`

* Add some test cases

* tests require canary next.js

* bump

* fix test cases

* Update packages/now-next/src/index.ts

Co-Authored-By: Joe Haddad <joe.haddad@zeit.co>

* Update packages/now-next/src/index.ts

Co-Authored-By: Joe Haddad <joe.haddad@zeit.co>
2019-12-16 12:46:39 +01:00
Steven
aee33f040d Publish Stable
- @now/build-utils@1.2.0
 - @now/cgi@1.0.1
 - now@16.7.0
 - @now/go@1.0.1
 - @now/next@2.2.0
 - @now/node@1.3.0
 - @now/python@1.0.1
 - @now/ruby@1.0.1
 - @now/static-build@0.14.0
2019-12-14 07:58:04 -05:00
Steven
b100677b3b Publish Canary
- @now/next@2.1.2-canary.3
2019-12-14 07:18:41 -05:00
Joe Haddad
9241b3ae2f [now-next] Compute Rewrites & Redirects Earlier (#3430)
This PR strictly moves code to make the diff for an upcoming PR cleaner.
2019-12-14 03:04:35 +00:00
Steven
1088da6871 Publish Canary
- @now/build-utils@1.1.2-canary.5
 - now@16.6.4-canary.6
 - @now/next@2.1.2-canary.2
 - @now/node@1.2.2-canary.2
 - @now/static-build@0.13.2-canary.3
2019-12-13 18:12:54 -05:00
Andy
f7b4dd4458 Revert "[now-static-build] Add support buildCommand, devCommand and outputDirectory (#3422)" (#3428)
This reverts commit 5a6d1a135f.
2019-12-13 18:12:16 -05:00
Steven
fb85b6b27a Publish Canary
- @now/build-utils@1.1.2-canary.4
 - now@16.6.4-canary.5
 - @now/next@2.1.2-canary.1
 - @now/node@1.2.2-canary.1
 - @now/static-build@0.13.2-canary.2
2019-12-13 17:24:55 -05:00
Steven
2e5e9b9a6f [now-build-utils] Fix error tput: No value for $TERM and no -T specified (#3425) 2019-12-13 17:23:41 -05:00
Steven
d3cc306e5b [now-build-utils] Remove unused execa (#3427)
~~Reverts the execa bump from #3422~~

Removes `execa` since it is no longer used.
2019-12-13 17:23:18 -05:00
Steven
d6c6a2a271 [docs] Fix broken link to runtimes (#3424) 2019-12-13 15:48:06 -05:00
Andy Bitz
6171a58ae3 Publish Canary
- @now/build-utils@1.1.2-canary.3
 - now@16.6.4-canary.4
 - @now/static-build@0.13.2-canary.1
2019-12-13 19:39:50 +01:00
Andy
5a6d1a135f [now-static-build] Add support buildCommand, devCommand and outputDirectory (#3422)
* [now-static-build] Handle `buildCommand`, `devCommand` and `outputDirectory`

* Adjust tests

* Swap order

* Add `node_modules/.bin` to PATH

* Remove @types/execa

* Append PATH only to spawn options

* Remove test check

* Only add when there is a command
2019-12-13 19:30:09 +01:00
Steven
68deab9007 [tests] Fix unit test coverage (#3420)
This PR reduces the time running Circle CI tests.

Since creating the monorepo in #2812, the coverage broke and then was fixed in #2876 with a workaround which would run unit tests twice.

More recently, we enabled Now CLI to always run tests in #3305 so that means coverage data is always generated.

This PR is a final proper fix so that unit tests run once which saves approximately 2 minutes per push (CI workflow).
2019-12-12 22:13:02 +00:00
Steven
d6114e2bef [deps] Fix yarn.lock signal-exit (#3419)
This patch was lost in a previous PR so I added it back
2019-12-12 20:16:21 +00:00
Steven
5fdc55f3fb [now-cli] Remove dead link to max lambda size (#3418)
We used to have a default `maxLambdaSize` and allow the user to increase to 50 MB.

However, this is no longer true. Today, the `maxLambdaSize` for every function is 50 MB and is not configurable, it's a hard limit.

This PR removes the dead link to avoid confusion like in Issue #3416.
2019-12-12 19:41:54 +00:00
Mark Glagola
751b166536 [now-cli] Add renewal price to now domains inspect (#3401)
Adds `Renewal Price` to `now domains inspect` command if the domain was bought with ZEIT.
2019-12-12 18:03:19 +00:00
Andy Bitz
6ffc8d97f4 Publish Canary
- @now/build-utils@1.1.2-canary.2
 - now@16.6.4-canary.3
2019-12-12 18:37:10 +01:00
Andy
67a80d6b83 [now-cli][now-build-utils] Update detectors (#3402)
* [now-build-utils] Consider `yarn build` and `npm run build` as `buildCommand`

* [@now/build-utils] Update new detectors

* Update unit tests

* [@now/build-utils] Update detect-builder and detect-routes

* Update tests

* Run prettier

* Add more tests

* [now-cli] Use default detectors

* Add now-dev test

* Add a generic node project fallback

* Fix build

* Use public as default

* Ensure generic node project is last

* Update tests

* Update tests again

* Update packages/now-build-utils/src/detectors/filesystem.ts

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

* Remove parentheses

* Revert "Remove parentheses"

This reverts commit 03f9aba07b0a6d4088719ca9afd602ce8fb1e9c1.

* Use getDependencyVersion instead of hasDependency
2019-12-12 18:28:24 +01:00
Steven
934cf772bc Publish Canary
- @now/build-utils@1.1.2-canary.1
 - now@16.6.4-canary.2
2019-12-12 10:58:21 -05:00
Andy
b01a24afdb [now-build-utils][now-cli] Move builds schema and functions schema to build-utils (#3417)
* [@now/build-utils] Add functions schema

* [now-cli] Use functions schema from build-utils

* Move buildsSchema to build-utils

* Add retries to test

* Add await
2019-12-12 16:02:19 +01:00
hi_Haowen
0ad75b52bf [now dev] Fix validate functions config failed in now json (#3414)
Follow up to #3408 .

```
> Error! Checking for updates failed
> Now CLI 16.6.3 dev (beta) — https://zeit.co/feedback/dev
> Error! Invalid `functions` property: ['api/test.js'] should NOT have additional properties
```
2019-12-12 13:59:39 +00:00
Andy Bitz
050772e78a Publish Canary
- now@16.6.4-canary.1
2019-12-12 13:23:12 +01:00
Andy
7c05dc1420 [now-cli] Do not handle cert errors for deployments (#3409)
Domain related things for deployment will now happen async
2019-12-12 13:19:26 +01:00
105 changed files with 2704 additions and 660 deletions

View File

@@ -340,6 +340,10 @@ jobs:
- run:
name: Running Unit Tests
command: yarn test-unit --clean false
- persist_to_workspace:
root: .
paths:
- packages/now-cli/.nyc_output
coverage:
docker:
@@ -349,12 +353,6 @@ jobs:
- checkout
- attach_workspace:
at: .
- run:
name: Compiling `now dev` HTML error templates
command: node packages/now-cli/scripts/compile-templates.js
- run:
name: Run unit tests
command: yarn workspace now run test-unit
- run:
name: Run coverage report
command: yarn workspace now run coverage

View File

@@ -6,7 +6,7 @@ A Runtime is an npm module that exposes a `build` function and optionally an `an
Official Runtimes are published to [npmjs.com](https://npmjs.com) as a package and referenced in the `use` property of the `now.json` configuration file.
However, the `use` property will work with any [npm install argument](https://docs.npmjs.com/cli/install) such as a git repo url which is useful for testing your Runtime.
See the [Runtimes Documentation](https://zeit.co/docs/v2/advanced/runtimes) to view example usage.
See the [Runtimes Documentation](https://zeit.co/docs/runtimes) to view example usage.
## Runtime Exports

View File

@@ -1,6 +1,6 @@
{
"name": "@now/build-utils",
"version": "1.1.2-canary.0",
"version": "1.2.1-canary.0",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.js",
@@ -35,7 +35,6 @@
"boxen": "4.2.0",
"cross-spawn": "6.0.5",
"end-of-stream": "1.4.1",
"execa": "^1.0.0",
"fs-extra": "7.0.0",
"glob": "7.1.3",
"into-stream": "5.0.0",

View File

@@ -0,0 +1,432 @@
import minimatch from 'minimatch';
import { valid as validSemver } from 'semver';
import { PackageJson, Builder, Config, BuilderFunctions } from './types';
interface ErrorResponse {
code: string;
message: string;
}
interface Options {
tag?: 'canary' | 'latest' | string;
functions?: BuilderFunctions;
}
const src = 'package.json';
const config: Config = { zeroConfig: true };
const MISSING_BUILD_SCRIPT_ERROR: ErrorResponse = {
code: 'missing_build_script',
message:
'Your `package.json` file is missing a `build` property inside the `scripts` property.' +
'\nMore details: https://zeit.co/docs/v2/platform/frequently-asked-questions#missing-build-script',
};
// Static builders are special cased in `@now/static-build`
function getBuilders({ tag }: Options = {}): Map<string, Builder> {
const withTag = tag ? `@${tag}` : '';
const config = { zeroConfig: true };
return new Map<string, Builder>([
['next', { src, use: `@now/next${withTag}`, config }],
]);
}
// Must be a function to ensure that the returned
// object won't be a reference
function getApiBuilders({ tag }: Pick<Options, 'tag'> = {}): Builder[] {
const withTag = tag ? `@${tag}` : '';
const config = { zeroConfig: true };
return [
{ src: 'api/**/*.js', use: `@now/node${withTag}`, config },
{ src: 'api/**/*.ts', use: `@now/node${withTag}`, config },
{ src: 'api/**/*.go', use: `@now/go${withTag}`, config },
{ src: 'api/**/*.py', use: `@now/python${withTag}`, config },
{ src: 'api/**/*.rb', use: `@now/ruby${withTag}`, config },
];
}
function hasPublicDirectory(files: string[]) {
return files.some(name => name.startsWith('public/'));
}
function hasBuildScript(pkg: PackageJson | undefined) {
const { scripts = {} } = pkg || {};
return Boolean(scripts && scripts['build']);
}
function getApiFunctionBuilder(
file: string,
prevBuilder: Builder | undefined,
{ functions = {} }: Pick<Options, 'functions'>
) {
const key = Object.keys(functions).find(
k => file === k || minimatch(file, k)
);
const fn = key ? functions[key] : undefined;
if (!fn || (!fn.runtime && !prevBuilder)) {
return prevBuilder;
}
const src = (prevBuilder && prevBuilder.src) || file;
const use = fn.runtime || (prevBuilder && prevBuilder.use);
const config: Config = { zeroConfig: true };
if (key) {
Object.assign(config, {
functions: {
[key]: fn,
},
});
}
const { includeFiles, excludeFiles } = fn;
if (includeFiles) Object.assign(config, { includeFiles });
if (excludeFiles) Object.assign(config, { excludeFiles });
return use ? { use, src, config } : prevBuilder;
}
async function detectFrontBuilder(
pkg: PackageJson,
builders: Builder[],
options: Options
): Promise<Builder> {
const { tag } = options;
const withTag = tag ? `@${tag}` : '';
for (const [dependency, builder] of getBuilders(options)) {
const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
// Return the builder when a dependency matches
if (deps[dependency]) {
if (options.functions) {
Object.entries(options.functions).forEach(([key, func]) => {
// When the builder is not used yet we'll use it for the frontend
if (
builders.every(
b => !(b.config && b.config.functions && b.config.functions[key])
)
) {
if (!builder.config) builder.config = {};
if (!builder.config.functions) builder.config.functions = {};
builder.config.functions[key] = { ...func };
}
});
}
return builder;
}
}
// By default we'll choose the `static-build` builder
return { src, use: `@now/static-build${withTag}`, config };
}
// Files that match a specific pattern will get ignored
export function getIgnoreApiFilter(optionsOrBuilders: Options | Builder[]) {
const possiblePatterns: string[] = getApiBuilders().map(b => b.src);
if (Array.isArray(optionsOrBuilders)) {
optionsOrBuilders.forEach(({ src }) => possiblePatterns.push(src));
} else if (optionsOrBuilders.functions) {
Object.keys(optionsOrBuilders.functions).forEach(p =>
possiblePatterns.push(p)
);
}
return (file: string) => {
if (!file.startsWith('api/')) {
return false;
}
if (file.includes('/.')) {
return false;
}
if (file.includes('/_')) {
return false;
}
if (file.endsWith('.d.ts')) {
return false;
}
if (possiblePatterns.every(p => !(file === p || minimatch(file, p)))) {
return false;
}
return true;
};
}
// We need to sort the file paths by alphabet to make
// sure the routes stay in the same order e.g. for deduping
export function sortFiles(fileA: string, fileB: string) {
return fileA.localeCompare(fileB);
}
async function detectApiBuilders(
files: string[],
options: Options
): Promise<Builder[]> {
const builds = files
.sort(sortFiles)
.filter(getIgnoreApiFilter(options))
.map(file => {
const apiBuilders = getApiBuilders(options);
const apiBuilder = apiBuilders.find(b => minimatch(file, b.src));
const fnBuilder = getApiFunctionBuilder(file, apiBuilder, options);
return fnBuilder ? { ...fnBuilder, src: file } : null;
});
return builds.filter(Boolean) as Builder[];
}
// When a package has files that conflict with `/api` routes
// e.g. Next.js pages/api we'll check it here and return an error.
async function checkConflictingFiles(
files: string[],
builders: Builder[]
): Promise<ErrorResponse | null> {
// For Next.js
if (builders.some(b => b.use.startsWith('@now/next'))) {
const hasApiPages = files.some(file => file.startsWith('pages/api/'));
const hasApiBuilders = builders.some(b => b.src.startsWith('api/'));
if (hasApiPages && hasApiBuilders) {
return {
code: 'conflicting_files',
message:
'It is not possible to use `api` and `pages/api` at the same time, please only use one option',
};
}
}
return null;
}
// When e.g. Next.js receives a `functions` property it has to make sure,
// that it can handle those files, otherwise there are unused functions.
async function checkUnusedFunctionsOnFrontendBuilder(
files: string[],
builder: Builder
): Promise<ErrorResponse | null> {
const { config: { functions = undefined } = {} } = builder;
if (!functions) return null;
if (builder.use.startsWith('@now/next')) {
const matchingFiles = files.filter(file =>
Object.keys(functions).some(key => file === key || minimatch(file, key))
);
for (const matchedFile of matchingFiles) {
if (
!matchedFile.startsWith('src/pages/') &&
!matchedFile.startsWith('pages/')
) {
return {
code: 'unused_function',
message: `The function for ${matchedFile} can't be handled by any builder`,
};
}
}
}
return null;
}
function validateFunctions(files: string[], { functions = {} }: Options) {
const apiBuilders = getApiBuilders();
for (const [path, func] of Object.entries(functions)) {
if (path.length > 256) {
return {
code: 'invalid_function_glob',
message: 'Function globs must be less than 256 characters long.',
};
}
if (!func || typeof func !== 'object') {
return {
code: 'invalid_function',
message: 'Function must be an object.',
};
}
if (Object.keys(func).length === 0) {
return {
code: 'invalid_function',
message: 'Function must contain at least one property.',
};
}
if (
func.maxDuration !== undefined &&
(func.maxDuration < 1 ||
func.maxDuration > 900 ||
!Number.isInteger(func.maxDuration))
) {
return {
code: 'invalid_function_duration',
message: 'Functions must have a duration between 1 and 900.',
};
}
if (
func.memory !== undefined &&
(func.memory < 128 || func.memory > 3008 || func.memory % 64 !== 0)
) {
return {
code: 'invalid_function_memory',
message:
'Functions must have a memory value between 128 and 3008 in steps of 64.',
};
}
if (path.startsWith('/')) {
return {
code: 'invalid_function_source',
message: `The function path "${path}" is invalid. The path must be relative to your project root and therefore cannot start with a slash.`,
};
}
if (files.some(f => f === path || minimatch(f, path)) === false) {
return {
code: 'invalid_function_source',
message: `No source file matched the function for ${path}.`,
};
}
if (func.runtime !== undefined) {
const tag = `${func.runtime}`.split('@').pop();
if (!tag || !validSemver(tag)) {
return {
code: 'invalid_function_runtime',
message:
'Function Runtimes must have a valid version, for example `now-php@1.0.0`.',
};
}
if (
apiBuilders.some(b => func.runtime && func.runtime.startsWith(b.use))
) {
return {
code: 'invalid_function_runtime',
message: `The function Runtime ${func.runtime} is not a Community Runtime and must not be specified.`,
};
}
}
if (func.includeFiles !== undefined) {
if (typeof func.includeFiles !== 'string') {
return {
code: 'invalid_function_property',
message: `The property \`includeFiles\` must be a string.`,
};
}
}
if (func.excludeFiles !== undefined) {
if (typeof func.excludeFiles !== 'string') {
return {
code: 'invalid_function_property',
message: `The property \`excludeFiles\` must be a string.`,
};
}
}
}
return null;
}
// When zero config is used we can call this function
// to determine what builders to use
export async function detectBuildersLegacy(
files: string[],
pkg?: PackageJson | undefined | null,
options: Options = {}
): Promise<{
builders: Builder[] | null;
errors: ErrorResponse[] | null;
warnings: ErrorResponse[];
}> {
const errors: ErrorResponse[] = [];
const warnings: ErrorResponse[] = [];
const functionError = validateFunctions(files, options);
if (functionError) {
return {
builders: null,
errors: [functionError],
warnings,
};
}
// Detect all builders for the `api` directory before anything else
const builders = await detectApiBuilders(files, options);
if (pkg && hasBuildScript(pkg)) {
const frontendBuilder = await detectFrontBuilder(pkg, builders, options);
builders.push(frontendBuilder);
const conflictError = await checkConflictingFiles(files, builders);
if (conflictError) {
warnings.push(conflictError);
}
const unusedFunctionError = await checkUnusedFunctionsOnFrontendBuilder(
files,
frontendBuilder
);
if (unusedFunctionError) {
return {
builders: null,
errors: [unusedFunctionError],
warnings,
};
}
} else {
if (pkg && builders.length === 0) {
// We only show this error when there are no api builders
// since the dependencies of the pkg could be used for those
errors.push(MISSING_BUILD_SCRIPT_ERROR);
return { errors, warnings, builders: null };
}
// We allow a `public` directory
// when there are no build steps
if (hasPublicDirectory(files)) {
builders.push({
use: '@now/static',
src: 'public/**/*',
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,
});
}
}
return {
builders: builders.length ? builders : null,
errors: errors.length ? errors : null,
warnings,
};
}

View File

@@ -1,6 +1,12 @@
import minimatch from 'minimatch';
import { valid as validSemver } from 'semver';
import { PackageJson, Builder, Config, BuilderFunctions } from './types';
import {
Builder,
Config,
BuilderFunctions,
DetectorResult,
DetectorOutput,
} from './types';
interface ErrorResponse {
code: string;
@@ -12,26 +18,6 @@ interface Options {
functions?: BuilderFunctions;
}
const src = 'package.json';
const config: Config = { zeroConfig: true };
const MISSING_BUILD_SCRIPT_ERROR: ErrorResponse = {
code: 'missing_build_script',
message:
'Your `package.json` file is missing a `build` property inside the `scripts` property.' +
'\nMore details: https://zeit.co/docs/v2/platform/frequently-asked-questions#missing-build-script',
};
// Static builders are special cased in `@now/static-build`
function getBuilders({ tag }: Options = {}): Map<string, Builder> {
const withTag = tag ? `@${tag}` : '';
const config = { zeroConfig: true };
return new Map<string, Builder>([
['next', { src, use: `@now/next${withTag}`, config }],
]);
}
// Must be a function to ensure that the returned
// object won't be a reference
function getApiBuilders({ tag }: Pick<Options, 'tag'> = {}): Builder[] {
@@ -47,13 +33,8 @@ function getApiBuilders({ tag }: Pick<Options, 'tag'> = {}): Builder[] {
];
}
function hasPublicDirectory(files: string[]) {
return files.some(name => name.startsWith('public/'));
}
function hasBuildScript(pkg: PackageJson | undefined) {
const { scripts = {} } = pkg || {};
return Boolean(scripts && scripts['build']);
function hasDirectory(fileName: string, files: string[]) {
return files.some(name => name.startsWith(`${fileName}/`));
}
function getApiFunctionBuilder(
@@ -91,39 +72,71 @@ function getApiFunctionBuilder(
return use ? { use, src, config } : prevBuilder;
}
async function detectFrontBuilder(
pkg: PackageJson,
function detectFrontBuilder(
detectorResult: Partial<DetectorOutput>,
builders: Builder[],
files: string[],
options: Options
): Promise<Builder> {
): Builder {
const { tag } = options;
const withTag = tag ? `@${tag}` : '';
for (const [dependency, builder] of getBuilders(options)) {
const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
// Return the builder when a dependency matches
if (deps[dependency]) {
if (options.functions) {
Object.entries(options.functions).forEach(([key, func]) => {
// When the builder is not used yet we'll use it for the frontend
if (
builders.every(
b => !(b.config && b.config.functions && b.config.functions[key])
)
) {
if (!builder.config) builder.config = {};
if (!builder.config.functions) builder.config.functions = {};
builder.config.functions[key] = { ...func };
}
});
const {
framework,
buildCommand,
outputDirectory,
devCommand,
} = detectorResult;
const frameworkSlug = framework ? framework.slug : null;
const config: Config = {
zeroConfig: true,
};
if (framework) {
config.framework = framework;
}
if (devCommand) {
config.devCommand = devCommand;
}
if (buildCommand) {
config.buildCommand = buildCommand;
}
if (outputDirectory) {
config.outputDirectory = outputDirectory;
}
// All unused functions will be used for the frontend
if (options.functions) {
Object.entries(options.functions).forEach(([key, func]) => {
if (
builders.every(
b => !(b.config && b.config.functions && b.config.functions[key])
)
) {
config.functions = config.functions || {};
config.functions[key] = { ...func };
}
});
}
return builder;
if (frameworkSlug === 'next') {
return { src: 'package.json', use: `@now/next${withTag}`, config };
}
if (frameworkSlug === 'hugo') {
const configFiles = new Set(['config.yaml', 'config.toml', 'config.json']);
const source = files.find(file => configFiles.has(file));
if (source) {
return { src: source, use: `@now/static-build${withTag}`, config };
}
}
// By default we'll choose the `static-build` builder
return { src, use: `@now/static-build${withTag}`, config };
return { src: 'package.json', use: `@now/static-build${withTag}`, config };
}
// Files that match a specific pattern will get ignored
@@ -139,10 +152,6 @@ export function getIgnoreApiFilter(optionsOrBuilders: Options | Builder[]) {
}
return (file: string) => {
if (!file.startsWith('api/')) {
return false;
}
if (file.includes('/.')) {
return false;
}
@@ -169,10 +178,7 @@ export function sortFiles(fileA: string, fileB: string) {
return fileA.localeCompare(fileB);
}
async function detectApiBuilders(
files: string[],
options: Options
): Promise<Builder[]> {
function detectApiBuilders(files: string[], options: Options): Builder[] {
const builds = files
.sort(sortFiles)
.filter(getIgnoreApiFilter(options))
@@ -188,10 +194,10 @@ async function detectApiBuilders(
// When a package has files that conflict with `/api` routes
// e.g. Next.js pages/api we'll check it here and return an error.
async function checkConflictingFiles(
function checkConflictingFiles(
files: string[],
builders: Builder[]
): Promise<ErrorResponse | null> {
): ErrorResponse | null {
// For Next.js
if (builders.some(b => b.use.startsWith('@now/next'))) {
const hasApiPages = files.some(file => file.startsWith('pages/api/'));
@@ -211,10 +217,10 @@ async function checkConflictingFiles(
// When e.g. Next.js receives a `functions` property it has to make sure,
// that it can handle those files, otherwise there are unused functions.
async function checkUnusedFunctionsOnFrontendBuilder(
function checkUnusedFunctionsOnFrontendBuilder(
files: string[],
builder: Builder
): Promise<ErrorResponse | null> {
): ErrorResponse | null {
const { config: { functions = undefined } = {} } = builder;
if (!functions) return null;
@@ -231,7 +237,9 @@ async function checkUnusedFunctionsOnFrontendBuilder(
) {
return {
code: 'unused_function',
message: `The function for ${matchedFile} can't be handled by any builder`,
message:
`The function for "${matchedFile}" can't be handled by any runtime. ` +
`Please provide one with the "runtime" option.`,
};
}
}
@@ -241,8 +249,6 @@ async function checkUnusedFunctionsOnFrontendBuilder(
}
function validateFunctions(files: string[], { functions = {} }: Options) {
const apiBuilders = getApiBuilders();
for (const [path, func] of Object.entries(functions)) {
if (path.length > 256) {
return {
@@ -312,15 +318,6 @@ function validateFunctions(files: string[], { functions = {} }: Options) {
'Function Runtimes must have a valid version, for example `now-php@1.0.0`.',
};
}
if (
apiBuilders.some(b => func.runtime && func.runtime.startsWith(b.use))
) {
return {
code: 'invalid_function_runtime',
message: `The function Runtime ${func.runtime} is not a Community Runtime and must not be specified.`,
};
}
}
if (func.includeFiles !== undefined) {
@@ -349,7 +346,7 @@ function validateFunctions(files: string[], { functions = {} }: Options) {
// to determine what builders to use
export async function detectBuilders(
files: string[],
pkg?: PackageJson | undefined | null,
detectorResult: Partial<DetectorResult> | null = null,
options: Options = {}
): Promise<{
builders: Builder[] | null;
@@ -370,19 +367,24 @@ export async function detectBuilders(
}
// Detect all builders for the `api` directory before anything else
const builders = await detectApiBuilders(files, options);
const builders = detectApiBuilders(files, options);
if (pkg && hasBuildScript(pkg)) {
const frontendBuilder = await detectFrontBuilder(pkg, builders, options);
if (detectorResult && detectorResult.buildCommand) {
const frontendBuilder = detectFrontBuilder(
detectorResult,
builders,
files,
options
);
builders.push(frontendBuilder);
const conflictError = await checkConflictingFiles(files, builders);
const conflictError = checkConflictingFiles(files, builders);
if (conflictError) {
warnings.push(conflictError);
}
const unusedFunctionError = await checkUnusedFunctionsOnFrontendBuilder(
const unusedFunctionError = checkUnusedFunctionsOnFrontendBuilder(
files,
frontendBuilder
);
@@ -394,34 +396,35 @@ export async function detectBuilders(
warnings,
};
}
} else {
if (pkg && builders.length === 0) {
// We only show this error when there are no api builders
// since the dependencies of the pkg could be used for those
errors.push(MISSING_BUILD_SCRIPT_ERROR);
return { errors, warnings, builders: null };
}
// We allow a `public` directory
// when there are no build steps
if (hasPublicDirectory(files)) {
builders.push({
use: '@now/static',
src: 'public/**/*',
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,
});
}
} else if (
detectorResult &&
detectorResult.outputDirectory &&
hasDirectory(detectorResult.outputDirectory, files)
) {
builders.push({
use: '@now/static',
src: [...detectorResult.outputDirectory.split('/'), '**', '*']
.filter(Boolean)
.join('/'),
config: { zeroConfig: true },
});
} else if (hasDirectory('public', files)) {
builders.push({
use: '@now/static',
src: 'public/**/*',
config: { zeroConfig: true },
});
} 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: { zeroConfig: true },
});
}
return {

View File

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

View File

@@ -217,7 +217,10 @@ async function detectApiRoutes(
for (const file of sortedFiles) {
// We only consider every file in the api directory
// as we will strip extensions as well as resolving "[segments]"
if (!file.startsWith('api/')) {
if (
!file.startsWith('api/') &&
!builders.some(b => b.src === file && b.config!.functions)
) {
continue;
}
@@ -271,14 +274,16 @@ async function detectApiRoutes(
return { defaultRoutes, error: null };
}
function hasPublicBuilder(builders: Builder[]): boolean {
return builders.some(
function getPublicBuilder(builders: Builder[]): Builder | null {
const builder = builders.find(
builder =>
builder.use === '@now/static' &&
builder.src === 'public/**/*' &&
/^.*\/\*\*\/\*$/.test(builder.src) &&
builder.config &&
builder.config.zeroConfig === true
);
return builder || null;
}
export async function detectRoutes(
@@ -286,11 +291,14 @@ export async function detectRoutes(
builders: Builder[]
): Promise<RoutesResult> {
const routesResult = await detectApiRoutes(files, builders);
const publicBuilder = getPublicBuilder(builders);
if (routesResult.defaultRoutes && publicBuilder) {
const directory = publicBuilder.src.replace('/**/*', '');
if (routesResult.defaultRoutes && hasPublicBuilder(builders)) {
routesResult.defaultRoutes.push({
src: '/(.*)',
dest: '/public/$1',
dest: `/${directory}/$1`,
});
}

View File

@@ -1,14 +1,18 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectAngular({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasAngular = await hasDependency('@angular/cli');
if (!hasAngular) return false;
const version = await getDependencyVersion('@angular/cli');
if (!version) return false;
return {
buildCommand: 'ng build',
buildDirectory: 'dist',
buildCommand: (await getPackageJsonBuildCommand()) || 'ng build',
outputDirectory: 'dist',
devCommand: 'ng serve --port $PORT',
framework: {
slug: '@angular/cli',
version,
},
minNodeRange: '10.x',
routes: [
{

View File

@@ -1,17 +1,22 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectBrunch({
fs: { hasDependency, exists },
fs: { exists, getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasBrunch = await hasDependency('brunch');
if (!hasBrunch) return false;
const version = await getDependencyVersion('brunch');
if (!version) return false;
const hasConfig = await exists('brunch-config.js');
if (!hasConfig) return false;
return {
buildCommand: 'brunch build --production',
buildDirectory: 'public',
buildCommand:
(await getPackageJsonBuildCommand()) || 'brunch build --production',
outputDirectory: 'public',
devCommand: 'brunch watch --server --port $PORT',
framework: {
slug: 'brunch',
version,
},
};
}

View File

@@ -1,16 +1,20 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectCreateReactAppEjected({
fs: { hasDependency },
fs: { getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasReactDevUtils = await hasDependency('react-dev-utils');
if (!hasReactDevUtils) {
const version = await getDependencyVersion('react-dev-utils');
if (!version) {
return false;
}
return {
buildCommand: 'node scripts/build.js',
buildDirectory: 'build',
outputDirectory: 'build',
devCommand: 'node scripts/start.js',
framework: {
slug: 'react-dev-utils',
version,
},
devVariables: { BROWSER: 'none' },
routes: [
{

View File

@@ -1,17 +1,21 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectCreateReactApp({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasReactScripts = await hasDependency('react-scripts');
if (!hasReactScripts) {
const version = await getDependencyVersion('react-scripts');
if (!version) {
return false;
}
return {
buildCommand: 'react-scripts build',
buildDirectory: 'build',
buildCommand: (await getPackageJsonBuildCommand()) || 'react-scripts build',
outputDirectory: 'build',
devCommand: 'react-scripts start',
devVariables: { BROWSER: 'none' },
framework: {
slug: 'react-scripts',
version,
},
routes: [
{
src: '/static/(.*)',

View File

@@ -1,13 +1,17 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectDocusaurus({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasDocusaurus = await hasDependency('docusaurus');
if (!hasDocusaurus) return false;
const version = await getDependencyVersion('docusaurus');
if (!version) return false;
return {
buildCommand: 'docusaurus-build',
buildDirectory: 'build',
buildCommand: (await getPackageJsonBuildCommand()) || 'docusaurus-build',
outputDirectory: 'build',
devCommand: 'docusaurus-start --port $PORT',
framework: {
slug: 'docusaurus',
version,
},
};
}

View File

@@ -1,13 +1,17 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectEleventy({
fs: { hasDependency },
fs: { getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasEleventy = await hasDependency('@11ty/eleventy');
if (!hasEleventy) return false;
const version = await getDependencyVersion('@11ty/eleventy');
if (!version) return false;
return {
buildCommand: 'npx @11ty/eleventy',
buildDirectory: '_site',
outputDirectory: '_site',
devCommand: 'npx @11ty/eleventy --serve --watch --port $PORT',
framework: {
slug: '@11ty/eleventy',
version,
},
};
}

View File

@@ -1,14 +1,18 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectEmber({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasEmber = await hasDependency('ember-cli');
if (!hasEmber) return false;
const version = await getDependencyVersion('ember-cli');
if (!version) return false;
return {
buildCommand: 'ember build',
buildDirectory: 'dist',
buildCommand: (await getPackageJsonBuildCommand()) || 'ember build',
outputDirectory: 'dist',
devCommand: 'ember serve --port $PORT',
framework: {
slug: 'ember-cli',
version,
},
routes: [
{
handle: 'filesystem',

View File

@@ -102,6 +102,31 @@ export default abstract class DetectorFilesystem {
const { dependencies = {}, devDependencies = {} } = pkg || {};
return name in dependencies || name in devDependencies;
};
public isNpm = async (): Promise<boolean> => {
return this.exists('package-lock.json');
};
public getPackageJsonCommand = async (
name: string
): Promise<string | null> => {
const pkg = await this.readPackageJson();
const { scripts = {} } = pkg || {};
return scripts[name] || null;
};
public getPackageJsonBuildCommand = async (): Promise<string | null> => {
const buildCommand = (await this.isNpm())
? 'npm run build'
: 'yarn run build';
return (await this.getPackageJsonCommand('build')) ? buildCommand : null;
};
public getDependencyVersion = async (name: string): Promise<string> => {
const pkg = await this.readPackageJson();
const { dependencies = {}, devDependencies = {} } = pkg || {};
return dependencies[name] || devDependencies[name];
};
}
async function nullEnoent<T>(p: Promise<T>): Promise<T | null> {

View File

@@ -1,16 +1,20 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectGatsby({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasGatsby = await hasDependency('gatsby');
if (!hasGatsby) {
const version = await getDependencyVersion('gatsby');
if (!version) {
return false;
}
return {
buildCommand: 'gatsby build',
buildDirectory: 'public',
buildCommand: (await getPackageJsonBuildCommand()) || 'gatsby build',
outputDirectory: 'public',
devCommand: 'gatsby develop -p $PORT',
framework: {
slug: 'gatsby',
version,
},
cachePattern: '.cache/**',
};
}

View File

@@ -0,0 +1,19 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectGenericNodeProject({
fs: { isNpm, getPackageJsonCommand },
}: DetectorParameters): Promise<DetectorResult> {
const useNpm = await isNpm();
const devCommand = await getPackageJsonCommand('dev');
const buildCommand = await getPackageJsonCommand('build');
if (!buildCommand) {
return false;
}
return {
buildCommand: `${useNpm ? 'npm' : 'yarn'} run build`,
devCommand: useNpm && devCommand ? `yarn run ${devCommand}` : undefined,
outputDirectory: 'public',
};
}

View File

@@ -1,15 +1,19 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectGridsome({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasGridsome = await hasDependency('gridsome');
if (!hasGridsome) {
const version = await getDependencyVersion('gridsome');
if (!version) {
return false;
}
return {
buildCommand: 'gridsome build',
buildDirectory: 'dist',
buildCommand: (await getPackageJsonBuildCommand()) || 'gridsome build',
outputDirectory: 'dist',
devCommand: 'gridsome develop -p $PORT',
framework: {
slug: 'gridsom',
version,
},
};
}

View File

@@ -1,13 +1,17 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectHexo({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasHexo = await hasDependency('hexo');
if (!hasHexo) return false;
const version = await getDependencyVersion('hexo');
if (!version) return false;
return {
buildCommand: 'hexo generate',
buildDirectory: 'public',
buildCommand: (await getPackageJsonBuildCommand()) || 'hexo generate',
outputDirectory: 'public',
devCommand: 'hexo server --port $PORT',
framework: {
slug: 'hexo',
version,
},
};
}

View File

@@ -20,7 +20,11 @@ export default async function detectHugo({
}
return {
buildCommand: 'hugo',
buildDirectory: config.publishDir || 'public',
outputDirectory: config.publishDir || 'public',
devCommand: 'hugo server -D -w -p $PORT',
framework: {
slug: 'hugo',
version: 'latest',
},
};
}

View File

@@ -9,6 +9,7 @@ import docusaurus from './docusaurus';
import eleventy from './eleventy';
import ember from './ember';
import gatsby from './gatsby';
import genericNodeProject from './generic-node-project';
import gridsome from './gridsome';
import hexo from './hexo';
import hugo from './hugo';
@@ -81,5 +82,8 @@ export async function detectDefaults(
d = params.detectors || detectors;
result = await firstTruthy(d.map(detector => detector(params)));
}
if (!result) {
result = await genericNodeProject(params);
}
return result;
}

View File

@@ -16,7 +16,11 @@ export default async function detectJekyll({
}
return {
buildCommand: 'jekyll build',
buildDirectory: config.destination || '_site',
outputDirectory: config.destination || '_site',
devCommand: 'bundle exec jekyll serve --watch --port $PORT',
framework: {
slug: 'jekyll',
version: 'latest',
},
};
}

View File

@@ -8,7 +8,11 @@ export default async function detectMiddleman({
return {
buildCommand: 'bundle exec middleman build',
buildDirectory: 'build',
outputDirectory: 'build',
devCommand: 'bundle exec middleman server -p $PORT',
framework: {
slug: 'middleman',
version: 'latest',
},
};
}

View File

@@ -1,13 +1,17 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectNext({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasNext = await hasDependency('next');
if (!hasNext) return false;
const version = await getDependencyVersion('next');
if (!version) return false;
return {
buildCommand: 'next build',
buildDirectory: 'build',
buildCommand: (await getPackageJsonBuildCommand()) || 'next build',
outputDirectory: '.next/static',
devCommand: 'next -p $PORT',
framework: {
slug: 'next',
version,
},
};
}

View File

@@ -1,14 +1,18 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectPolymer({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasPolymer = await hasDependency('polymer-cli');
if (!hasPolymer) return false;
const version = await getDependencyVersion('polymer-cli');
if (!version) return false;
return {
buildCommand: 'polymer build',
buildDirectory: 'build',
buildCommand: (await getPackageJsonBuildCommand()) || 'polymer build',
outputDirectory: 'build',
devCommand: 'polymer serve --port $PORT',
framework: {
slug: 'polymer-cli',
version,
},
routes: [
{
handle: 'filesystem',

View File

@@ -1,14 +1,18 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectPreact({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasPreact = await hasDependency('preact-cli');
if (!hasPreact) return false;
const version = await getDependencyVersion('preact-cli');
if (!version) return false;
return {
buildCommand: 'preact build',
buildDirectory: 'build',
buildCommand: (await getPackageJsonBuildCommand()) || 'preact build',
outputDirectory: 'build',
devCommand: 'preact watch --port $PORT',
framework: {
slug: 'preact-cli',
version,
},
routes: [
{
handle: 'filesystem',

View File

@@ -1,13 +1,13 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectSaber({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasSaber = await hasDependency('saber');
if (!hasSaber) return false;
const version = await getDependencyVersion('saber');
if (!version) return false;
return {
buildCommand: 'saber build',
buildDirectory: 'public',
buildCommand: (await getPackageJsonBuildCommand()) || 'saber build',
outputDirectory: 'public',
devCommand: 'saber --port $PORT',
routes: [
{
@@ -23,5 +23,9 @@ export default async function detectSaber({
dest: '404.html',
},
],
framework: {
slug: 'saber',
version,
},
};
}

View File

@@ -1,13 +1,17 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectSapper({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasSapper = await hasDependency('sapper');
if (!hasSapper) return false;
const version = await getDependencyVersion('sapper');
if (!version) return false;
return {
buildCommand: 'sapper export',
buildDirectory: '__sapper__/export',
buildCommand: (await getPackageJsonBuildCommand()) || 'sapper export',
outputDirectory: '__sapper__/export',
devCommand: 'sapper dev --port $PORT',
framework: {
slug: 'sapper',
version,
},
};
}

View File

@@ -1,14 +1,18 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectStencil({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasStencil = await hasDependency('@stencil/core');
if (!hasStencil) return false;
const version = await getDependencyVersion('@stencil/core');
if (!version) return false;
return {
buildCommand: 'stencil build',
buildDirectory: 'www',
buildCommand: (await getPackageJsonBuildCommand()) || 'stencil build',
outputDirectory: 'www',
devCommand: 'stencil build --dev --watch --serve --port $PORT',
framework: {
slug: '@stencil/core',
version,
},
routes: [
{
handle: 'filesystem',

View File

@@ -1,14 +1,18 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectSvelte({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasSvelte = await hasDependency('sirv-cli');
if (!hasSvelte) return false;
const version = await getDependencyVersion('sirv-cli');
if (!version) return false;
return {
buildCommand: 'rollup -c',
buildDirectory: 'public',
buildCommand: (await getPackageJsonBuildCommand()) || 'rollup -c',
outputDirectory: 'public',
devCommand: 'sirv public --single --dev --port $PORT',
framework: {
slug: 'sirv-cli',
version,
},
routes: [
{
handle: 'filesystem',

View File

@@ -1,14 +1,18 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectUmiJS({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasUmi = await hasDependency('umi');
if (!hasUmi) return false;
const version = await getDependencyVersion('umi');
if (!version) return false;
return {
buildCommand: 'umi build',
buildDirectory: 'dist',
buildCommand: (await getPackageJsonBuildCommand()) || 'umi build',
outputDirectory: 'dist',
devCommand: 'umi dev --port $PORT',
framework: {
slug: 'umi',
version,
},
routes: [
{
handle: 'filesystem',

View File

@@ -1,14 +1,19 @@
import { DetectorParameters, DetectorResult } from '../types';
export default async function detectVue({
fs: { hasDependency },
fs: { getPackageJsonBuildCommand, getDependencyVersion },
}: DetectorParameters): Promise<DetectorResult> {
const hasVue = await hasDependency('@vue/cli-service');
if (!hasVue) return false;
const version = await getDependencyVersion('@vue/cli-service');
if (!version) return false;
return {
buildCommand: 'vue-cli-service build',
buildDirectory: 'dist',
buildCommand:
(await getPackageJsonBuildCommand()) || 'vue-cli-service build',
outputDirectory: 'dist',
devCommand: 'vue-cli-service serve --port $PORT',
framework: {
slug: '@vue/cli-service',
version,
},
routes: [
{
src: '^/[^/]*\\.(js|txt|ico|json)',

View File

@@ -70,12 +70,17 @@ export async function getSupportedNodeVersion(
const validRanges = supportedOptions
.filter(o => !o.discontinueDate)
.map(o => o.range);
const prevTerm = process.env.TERM;
if (!prevTerm) {
// workaround for https://github.com/sindresorhus/term-size/issues/13
process.env.TERM = 'xterm';
}
console.warn(
boxen(
'WARNING' +
'NOTICE' +
'\n' +
`\nNode.js ${range} will be discontinued on ${d}.` +
`\nDeployments created on or after ${d} will fail to build.` +
`\nNode.js version ${range} has reached end-of-life.` +
`\nAs a result, deployments created on or after ${d} will fail to build.` +
'\nPlease use one of the following supported `engines` in `package.json`: ' +
JSON.stringify(validRanges) +
'\nThis change is the result of a decision made by an upstream infrastructure provider (AWS).' +
@@ -83,6 +88,7 @@ export async function getSupportedNodeVersion(
{ padding: 1 }
)
);
process.env.TERM = prevTerm;
}
return selection;

View File

@@ -39,6 +39,66 @@ export function spawnAsync(
});
}
export function execAsync(
command: string,
args: string[],
opts: SpawnOptions = {}
) {
return new Promise<{ stdout: string; stderr: string; code: number }>(
(resolve, reject) => {
opts.stdio = 'pipe';
const stdoutList: Buffer[] = [];
const stderrList: Buffer[] = [];
const child = spawn(command, args, opts);
child.stderr!.on('data', data => {
stderrList.push(data);
});
child.stdout!.on('data', data => {
stdoutList.push(data);
});
child.on('error', reject);
child.on('close', (code, signal) => {
if (code !== 0) {
return reject(
new Error(
`Program "${command}" exited with non-zero exit code ${code} ${signal}.`
)
);
}
return resolve({
code,
stdout: Buffer.concat(stdoutList).toString(),
stderr: Buffer.concat(stderrList).toString(),
});
});
}
);
}
export function spawnCommand(command: string, options: SpawnOptions = {}) {
if (process.platform === 'win32') {
return spawn('cmd.exe', ['/C', command], options);
}
return spawn('sh', ['-c', command], options);
}
export async function execCommand(command: string, options: SpawnOptions = {}) {
if (process.platform === 'win32') {
await spawnAsync('cmd.exe', ['/C', command], options);
} else {
await spawnAsync('sh', ['-c', command], options);
}
return true;
}
async function chmodPlusX(fsPath: string) {
const s = await fs.stat(fsPath);
const newMode = s.mode | 64 | 8 | 1; // eslint-disable-line no-bitwise

View File

@@ -8,7 +8,10 @@ import getWriteableDirectory from './fs/get-writable-directory';
import glob from './fs/glob';
import rename from './fs/rename';
import {
execAsync,
spawnAsync,
execCommand,
spawnCommand,
installDependencies,
runPackageJsonScript,
runNpmInstall,
@@ -38,9 +41,12 @@ export {
getWriteableDirectory,
glob,
rename,
execAsync,
spawnAsync,
installDependencies,
runPackageJsonScript,
execCommand,
spawnCommand,
runNpmInstall,
runBundleInstall,
runPipInstall,
@@ -56,5 +62,9 @@ export {
getLambdaOptionsFromFunction,
};
export { detectBuildersLegacy } from './detect-builders-legacy';
export { detectRoutesLegacy } from './detect-routes-legacy';
export { detectDefaults } from './detectors';
export * from './schemas';
export * from './types';

View File

@@ -0,0 +1,61 @@
export const functionsSchema = {
type: 'object',
minProperties: 1,
maxProperties: 50,
additionalProperties: false,
patternProperties: {
'^.{1,256}$': {
type: 'object',
additionalProperties: false,
properties: {
runtime: {
type: 'string',
maxLength: 256,
},
memory: {
// Number between 128 and 3008 in steps of 64
enum: Object.keys(Array.from({ length: 50 }))
.slice(2, 48)
.map(x => Number(x) * 64),
},
maxDuration: {
type: 'number',
minimum: 1,
maximum: 900,
},
includeFiles: {
type: 'string',
maxLength: 256,
},
excludeFiles: {
type: 'string',
maxLength: 256,
},
},
},
},
};
export const buildsSchema = {
type: 'array',
minItems: 0,
maxItems: 128,
items: {
type: 'object',
additionalProperties: false,
required: ['use'],
properties: {
src: {
type: 'string',
minLength: 1,
maxLength: 4096,
},
use: {
type: 'string',
minLength: 3,
maxLength: 256,
},
config: { type: 'object' },
},
},
};

View File

@@ -52,6 +52,13 @@ export interface Config {
zeroConfig?: boolean;
import?: { [key: string]: string };
functions?: BuilderFunctions;
outputDirectory?: string;
buildCommand?: string;
devCommand?: string;
framework?: {
slug: string;
version: string;
};
}
export interface Meta {
@@ -354,7 +361,7 @@ export interface DetectorParameters {
export interface DetectorOutput {
buildCommand: string;
buildDirectory: string;
outputDirectory: string;
buildVariables?: Env;
devCommand?: string;
devVariables?: Env;
@@ -366,6 +373,10 @@ export interface DetectorOutput {
redirects?: NowRedirect[];
headers?: NowHeader[];
trailingSlash?: boolean;
framework?: {
slug: string;
version: string;
};
}
export type DetectorResult = DetectorOutput | false;

View File

@@ -4,13 +4,34 @@ const {
packAndDeploy,
testDeployment,
} = require('../../../test/lib/deployment/test-deployment');
const { glob, detectBuilders, detectRoutes } = require('../');
const {
glob,
detectBuilders,
detectRoutes,
DetectorFilesystem,
detectDefaults,
} = require('../');
jest.setTimeout(4 * 60 * 1000);
const builderUrl = '@canary';
let buildUtilsUrl;
class LocalFilesystem extends DetectorFilesystem {
constructor(dir) {
super();
this.dir = dir;
}
_exists(name) {
return fs.pathExists(path.join(this.dir, name));
}
_readFile(name) {
return fs.readFile(path.join(this.dir, name));
}
}
beforeAll(async () => {
const buildUtilsPath = path.resolve(__dirname, '..');
buildUtilsUrl = await packAndDeploy(buildUtilsPath);
@@ -67,7 +88,9 @@ for (const builder of buildersToTestWith) {
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 detectorResult = await detectDefaults({
fs: new LocalFilesystem(fixture),
});
const fileList = await glob('**', fixture);
const files = Object.keys(fileList);
@@ -110,7 +133,7 @@ it('Test `detectBuilders` and `detectRoutes`', async () => {
},
];
const { builders } = await detectBuilders(files, pkg);
const { builders } = await detectBuilders(files, detectorResult);
const { defaultRoutes } = await detectRoutes(files, builders);
const nowConfig = { builds: builders, routes: defaultRoutes, probes };
@@ -128,7 +151,9 @@ it('Test `detectBuilders` and `detectRoutes`', async () => {
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 detectorResult = await detectDefaults({
fs: new LocalFilesystem(fixture),
});
const fileList = await glob('**', fixture);
const files = Object.keys(fileList);
@@ -192,7 +217,7 @@ it('Test `detectBuilders` and `detectRoutes` with `index` files', async () => {
},
];
const { builders } = await detectBuilders(files, pkg);
const { builders } = await detectBuilders(files, detectorResult);
const { defaultRoutes } = await detectRoutes(files, builders);
const nowConfig = { builds: builders, routes: defaultRoutes, probes };

View File

@@ -67,8 +67,8 @@ test('detectDefaults() - angular', async () => {
const fs = new LocalFilesystem(dir);
const result = await detectDefaults({ fs });
if (!result) throw new Error('Expected result');
assert.equal(result.buildDirectory, 'dist');
assert.deepEqual(result.buildCommand, 'ng build');
assert.equal(result.outputDirectory, 'dist');
assert.deepEqual(result.buildCommand, 'yarn run build');
});
test('detectDefaults() - brunch', async () => {
@@ -76,8 +76,8 @@ test('detectDefaults() - brunch', async () => {
const fs = new LocalFilesystem(dir);
const result = await detectDefaults({ fs });
if (!result) throw new Error('Expected result');
assert.equal(result.buildDirectory, 'public');
assert.deepEqual(result.buildCommand, 'brunch build --production');
assert.equal(result.outputDirectory, 'public');
assert.deepEqual(result.buildCommand, 'yarn run build');
});
test('detectDefaults() - hugo', async () => {
@@ -85,7 +85,7 @@ test('detectDefaults() - hugo', async () => {
const fs = new LocalFilesystem(dir);
const result = await detectDefaults({ fs });
if (!result) throw new Error('Expected result');
assert.equal(result.buildDirectory, 'public');
assert.equal(result.outputDirectory, 'public');
assert.deepEqual(result.buildCommand, 'hugo');
});
@@ -94,7 +94,7 @@ test('detectDefaults() - jekyll', async () => {
const fs = new LocalFilesystem(dir);
const result = await detectDefaults({ fs });
if (!result) throw new Error('Expected result');
assert.equal(result.buildDirectory, '_site');
assert.equal(result.outputDirectory, '_site');
assert.deepEqual(result.buildCommand, 'jekyll build');
});
@@ -103,6 +103,6 @@ test('detectDefaults() - middleman', async () => {
const fs = new LocalFilesystem(dir);
const result = await detectDefaults({ fs });
if (!result) throw new Error('Expected result');
assert.equal(result.buildDirectory, 'build');
assert.equal(result.outputDirectory, 'build');
assert.deepEqual(result.buildCommand, 'bundle exec middleman build');
});

View File

@@ -1,9 +1,14 @@
const path = require('path');
const fs = require('fs-extra');
const execa = require('execa');
const assert = require('assert');
const { createZip } = require('../dist/lambda');
const { glob, download, detectBuilders, detectRoutes } = require('../');
const {
glob,
download,
detectBuilders,
detectRoutes,
spawnAsync,
} = require('../');
const {
getSupportedNodeVersion,
defaultSelection,
@@ -39,7 +44,7 @@ it('should create zip files with symlinks properly', async () => {
await fs.mkdirp(outDir);
await fs.writeFile(outFile, await createZip(files));
await execa('unzip', [outFile], { cwd: outDir });
await spawnAsync('unzip', [outFile], { cwd: outDir });
const [linkStat, aStat] = await Promise.all([
fs.lstat(path.join(outDir, 'link.txt')),
@@ -110,42 +115,34 @@ it('should support require by path for legacy builders', () => {
});
describe('Test `detectBuilders`', () => {
it('package.json + no build', async () => {
const pkg = { dependencies: { next: '9.0.0' } };
it('package.json + no build command', async () => {
const detected = { framework: { slug: 'next', version: '9.0.0' } };
const files = ['package.json', 'pages/index.js', 'public/index.html'];
const { builders, errors } = await detectBuilders(files, pkg);
expect(builders).toBe(null);
expect(errors.length).toBe(1);
const { builders } = await detectBuilders(files, detected);
expect(builders.length).toBe(1);
expect(builders[0].src).toBe('public/**/*');
expect(builders[0].use).toBe('@now/static');
});
it('package.json + no build + next', async () => {
const pkg = {
scripts: { build: 'next build' },
dependencies: { next: '9.0.0' },
it('package.json + build command + next', async () => {
const detected = {
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
};
const files = ['package.json', 'pages/index.js'];
const { builders, errors } = await detectBuilders(files, pkg);
const { builders, errors } = await detectBuilders(files, detected);
expect(builders[0].use).toBe('@now/next');
expect(errors).toBe(null);
});
it('package.json + no build + next', async () => {
const pkg = {
scripts: { build: 'next build' },
devDependencies: { next: '9.0.0' },
};
const files = ['package.json', 'pages/index.js'];
const { builders, errors } = await detectBuilders(files, pkg);
expect(builders[0].use).toBe('@now/next');
expect(errors).toBe(null);
});
it('package.json + no build', async () => {
const pkg = {};
it('no detectors + no build command', async () => {
const files = ['package.json'];
const { builders, errors } = await detectBuilders(files, pkg);
const { builders, errors } = await detectBuilders(files, {});
expect(builders).toBe(null);
expect(errors.length).toBe(1);
expect(errors).toBe(null);
});
it('static file', async () => {
@@ -155,7 +152,7 @@ describe('Test `detectBuilders`', () => {
expect(errors).toBe(null);
});
it('no package.json + public', async () => {
it('no package.json + public + api', async () => {
const files = ['api/users.js', 'public/index.html'];
const { builders, errors } = await detectBuilders(files);
expect(builders[1].use).toBe('@now/static');
@@ -173,7 +170,7 @@ describe('Test `detectBuilders`', () => {
expect(errors).toBe(null);
});
it('package.json + no build + root + api', async () => {
it('no package.json + no build command + root + api', async () => {
const files = ['index.html', 'api/[endpoint].js', 'static/image.png'];
const { builders, errors } = await detectBuilders(files);
expect(builders[0].use).toBe('@now/node');
@@ -198,13 +195,17 @@ describe('Test `detectBuilders`', () => {
});
it('api + next + public', async () => {
const pkg = {
scripts: { build: 'next build' },
devDependencies: { next: '9.0.0' },
const detected = {
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
};
const files = ['package.json', 'api/endpoint.js', 'public/index.html'];
const { builders } = await detectBuilders(files, pkg);
const { builders } = await detectBuilders(files, detected);
expect(builders[0].use).toBe('@now/node');
expect(builders[0].src).toBe('api/endpoint.js');
expect(builders[1].use).toBe('@now/next');
@@ -213,13 +214,17 @@ describe('Test `detectBuilders`', () => {
});
it('api + next + raw static', async () => {
const pkg = {
scripts: { build: 'next build' },
devDependencies: { next: '9.0.0' },
const detected = {
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
};
const files = ['package.json', 'api/endpoint.js', 'index.html'];
const { builders } = await detectBuilders(files, pkg);
const { builders } = await detectBuilders(files, detected);
expect(builders[0].use).toBe('@now/node');
expect(builders[0].src).toBe('api/endpoint.js');
expect(builders[1].use).toBe('@now/next');
@@ -263,61 +268,85 @@ describe('Test `detectBuilders`', () => {
});
it('next + public', async () => {
const pkg = {
scripts: { build: 'next build' },
devDependencies: { next: '9.0.0' },
const detected = {
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
};
const files = ['package.json', 'public/index.html', 'README.md'];
const { builders } = await detectBuilders(files, pkg);
const { builders } = await detectBuilders(files, detected);
expect(builders[0].use).toBe('@now/next');
expect(builders[0].src).toBe('package.json');
expect(builders.length).toBe(1);
});
it('nuxt', async () => {
const pkg = {
scripts: { build: 'nuxt build' },
dependencies: { nuxt: '2.8.1' },
const detected = {
buildCommand: 'yarn build',
framework: {
slug: '@vue/cli-service',
version: '2.8.1',
},
};
const files = ['package.json', 'pages/index.js'];
const { builders } = await detectBuilders(files, pkg);
const { builders } = await detectBuilders(files, detected);
expect(builders[0].use).toBe('@now/static-build');
expect(builders[0].src).toBe('package.json');
expect(builders.length).toBe(1);
});
it('nuxt + tag canary', async () => {
const pkg = {
scripts: { build: 'nuxt build' },
dependencies: { nuxt: '2.8.1' },
const detected = {
buildCommand: 'yarn build',
framework: {
slug: '@vue/cli-service',
version: '2.8.1',
},
};
const files = ['package.json', 'pages/index.js'];
const { builders } = await detectBuilders(files, pkg, { tag: 'canary' });
const { builders } = await detectBuilders(files, detected, {
tag: 'canary',
});
expect(builders[0].use).toBe('@now/static-build@canary');
expect(builders[0].src).toBe('package.json');
expect(builders.length).toBe(1);
});
it('package.json with no build + api', async () => {
const pkg = { dependencies: { next: '9.0.0' } };
it('no build command + api', async () => {
const detected = {
framework: {
slug: 'next',
version: '9.0.0',
},
};
const files = ['package.json', 'api/[endpoint].js'];
const { builders } = await detectBuilders(files, pkg);
const { builders } = await detectBuilders(files, detected);
expect(builders[0].use).toBe('@now/node');
expect(builders[0].src).toBe('api/[endpoint].js');
expect(builders.length).toBe(1);
});
it('package.json with no build + public directory', async () => {
const pkg = { dependencies: { next: '9.0.0' } };
it('no build command + public directory', async () => {
const detected = {
framework: {
slug: 'next',
version: '9.0.0',
},
};
const files = ['package.json', 'public/index.html'];
const { builders, errors } = await detectBuilders(files, pkg);
expect(builders).toBe(null);
expect(errors.length).toBe(1);
const { builders, errors } = await detectBuilders(files, detected);
expect(builders.length).toBe(1);
expect(errors).toBe(null);
});
it('no package.json + api', async () => {
@@ -336,17 +365,23 @@ describe('Test `detectBuilders`', () => {
});
it('package.json + api + canary', async () => {
const pkg = {
scripts: { build: 'next build' },
dependencies: { next: '9.0.0' },
const detected = {
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
};
const files = [
'pages/index.js',
'api/[endpoint].js',
'api/[endpoint]/[id].js',
];
const { builders } = await detectBuilders(files, pkg, { tag: 'canary' });
const { builders } = await detectBuilders(files, detected, {
tag: 'canary',
});
expect(builders[0].use).toBe('@now/node@canary');
expect(builders[1].use).toBe('@now/node@canary');
expect(builders[2].use).toBe('@now/next@canary');
@@ -354,17 +389,23 @@ describe('Test `detectBuilders`', () => {
});
it('package.json + api + latest', async () => {
const pkg = {
scripts: { build: 'next build' },
dependencies: { next: '9.0.0' },
const detected = {
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
};
const files = [
'pages/index.js',
'api/[endpoint].js',
'api/[endpoint]/[id].js',
];
const { builders } = await detectBuilders(files, pkg, { tag: 'latest' });
const { builders } = await detectBuilders(files, detected, {
tag: 'latest',
});
expect(builders[0].use).toBe('@now/node@latest');
expect(builders[1].use).toBe('@now/node@latest');
expect(builders[2].use).toBe('@now/next@latest');
@@ -372,17 +413,21 @@ describe('Test `detectBuilders`', () => {
});
it('package.json + api + random tag', async () => {
const pkg = {
scripts: { build: 'next build' },
dependencies: { next: '9.0.0' },
const detected = {
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
};
const files = [
'pages/index.js',
'api/[endpoint].js',
'api/[endpoint]/[id].js',
];
const { builders } = await detectBuilders(files, pkg, { tag: 'haha' });
const { builders } = await detectBuilders(files, detected, { tag: 'haha' });
expect(builders[0].use).toBe('@now/node@haha');
expect(builders[1].use).toBe('@now/node@haha');
expect(builders[2].use).toBe('@now/next@haha');
@@ -390,13 +435,20 @@ describe('Test `detectBuilders`', () => {
});
it('next.js pages/api + api', async () => {
const pkg = {
scripts: { build: 'next build' },
dependencies: { next: '9.0.0' },
const detected = {
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
};
const files = ['api/user.js', 'pages/api/user.js'];
const { warnings, errors, builders } = await detectBuilders(files, pkg);
const { warnings, errors, builders } = await detectBuilders(
files,
detected
);
expect(errors).toBe(null);
expect(warnings[0]).toBeDefined();
@@ -420,9 +472,12 @@ describe('Test `detectBuilders`', () => {
});
it('functions with nextjs', async () => {
const pkg = {
scripts: { build: 'next build' },
dependencies: { next: '9.0.0' },
const detected = {
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
};
const functions = {
'pages/api/teams/**': {
@@ -435,7 +490,7 @@ describe('Test `detectBuilders`', () => {
'pages/index.js',
'pages/api/teams/members.ts',
];
const { builders, errors } = await detectBuilders(files, pkg, {
const { builders, errors } = await detectBuilders(files, detected, {
functions,
});
@@ -446,6 +501,11 @@ describe('Test `detectBuilders`', () => {
use: '@now/next',
config: {
zeroConfig: true,
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
functions: {
'pages/api/teams/**': {
memory: 128,
@@ -457,9 +517,12 @@ describe('Test `detectBuilders`', () => {
});
it('extend with functions', async () => {
const pkg = {
scripts: { build: 'next build' },
dependencies: { next: '9.0.0' },
const detected = {
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
};
const functions = {
'api/users/*.ts': {
@@ -476,7 +539,7 @@ describe('Test `detectBuilders`', () => {
'api/users/[id].ts',
'api/teams/members.ts',
];
const { builders } = await detectBuilders(files, pkg, { functions });
const { builders } = await detectBuilders(files, detected, { functions });
expect(builders.length).toBe(3);
expect(builders[0]).toEqual({
@@ -509,6 +572,11 @@ describe('Test `detectBuilders`', () => {
use: '@now/next',
config: {
zeroConfig: true,
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
},
});
});
@@ -607,31 +675,23 @@ describe('Test `detectBuilders`', () => {
});
it('Do not allow functions that are not used by @now/next', async () => {
const pkg = {
scripts: { build: 'next build' },
dependencies: { next: '9.0.0' },
const detected = {
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
};
const functions = { 'test.js': { memory: 1024 } };
const files = ['pages/index.js', 'test.js'];
const { errors } = await detectBuilders(files, pkg, { functions });
const { errors } = await detectBuilders(files, detected, { functions });
expect(errors).toBeDefined();
expect(errors[0].code).toBe('unused_function');
});
it('Do not allow function non Community Runtimes', async () => {
const functions = {
'api/test.js': { memory: 128, runtime: '@now/node@1.0.0' },
};
const files = ['api/test.js'];
const { errors } = await detectBuilders(files, null, { functions });
expect(errors).toBeDefined();
expect(errors[0].code).toBe('invalid_function_runtime');
});
it('Must include includeFiles config property', async () => {
const functions = {
'api/test.js': { includeFiles: 'text/include.txt' },
@@ -731,6 +791,149 @@ describe('Test `detectBuilders`', () => {
expect(errors).not.toBe(null);
expect(errors[0].code).toBe('invalid_function_source');
});
it('Custom static output directory', async () => {
const detected = {
outputDirectory: 'dist',
};
const files = ['dist/index.html', 'dist/style.css'];
const { builders } = await detectBuilders(files, detected);
expect(builders.length).toBe(1);
expect(builders[0].src).toBe('dist/**/*');
expect(builders[0].use).toBe('@now/static');
const { defaultRoutes } = await detectRoutes(files, builders);
expect(defaultRoutes.length).toBe(1);
expect(defaultRoutes[0].src).toBe('/(.*)');
expect(defaultRoutes[0].dest).toBe('/dist/$1');
});
it('Custom static output directory with api', async () => {
const detected = {
outputDirectory: 'output',
};
const files = ['api/user.ts', 'output/index.html', 'output/style.css'];
const { builders } = await detectBuilders(files, detected);
expect(builders.length).toBe(2);
expect(builders[1].src).toBe('output/**/*');
expect(builders[1].use).toBe('@now/static');
const { defaultRoutes } = await detectRoutes(files, builders);
expect(defaultRoutes.length).toBe(3);
expect(defaultRoutes[1].status).toBe(404);
expect(defaultRoutes[2].src).toBe('/(.*)');
expect(defaultRoutes[2].dest).toBe('/output/$1');
});
it('Custom directory for Serverless Functions', async () => {
const files = ['server/_lib/db.ts', 'server/user.ts', 'server/team.ts'];
const functions = {
'server/**/*.ts': {
memory: 128,
runtime: '@now/node@1.2.1',
},
};
const { builders } = await detectBuilders(files, null, { functions });
expect(builders.length).toBe(3);
expect(builders[0]).toEqual({
use: '@now/node@1.2.1',
src: 'server/team.ts',
config: {
zeroConfig: true,
functions: {
'server/**/*.ts': {
memory: 128,
runtime: '@now/node@1.2.1',
},
},
},
});
expect(builders[1]).toEqual({
use: '@now/node@1.2.1',
src: 'server/user.ts',
config: {
zeroConfig: true,
functions: {
'server/**/*.ts': {
memory: 128,
runtime: '@now/node@1.2.1',
},
},
},
});
// This is expected, since only "api + full static" is supported
// no other directory, so everything else will be deployed
expect(builders[2].use).toBe('@now/static');
const { defaultRoutes } = await detectRoutes(files, builders);
expect(defaultRoutes.length).toBe(3);
expect(defaultRoutes[0].dest).toBe('/server/team.ts');
expect(defaultRoutes[0].src).toBe('^/server/(team\\/|team|team\\.ts)$');
expect(defaultRoutes[1].dest).toBe('/server/user.ts');
expect(defaultRoutes[1].src).toBe('^/server/(user\\/|user|user\\.ts)$');
expect(defaultRoutes[2].status).toBe(404);
});
it('Custom directory for Serverless Functions + Next.js', async () => {
const detected = {
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
};
const functions = {
'server/**/*.ts': {
runtime: '@now/node@1.2.1',
},
};
const files = ['package.json', 'pages/index.ts', 'server/user.ts'];
const { builders } = await detectBuilders(files, detected, { functions });
expect(builders.length).toBe(2);
expect(builders[0]).toEqual({
use: '@now/node@1.2.1',
src: 'server/user.ts',
config: {
zeroConfig: true,
functions,
},
});
expect(builders[1]).toEqual({
use: '@now/next',
src: 'package.json',
config: {
zeroConfig: true,
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
},
});
const { defaultRoutes } = await detectRoutes(files, builders);
expect(defaultRoutes.length).toBe(2);
expect(defaultRoutes[0].dest).toBe('/server/user.ts');
expect(defaultRoutes[0].src).toBe('^/server/(user\\/|user|user\\.ts)$');
expect(defaultRoutes[1].status).toBe(404);
});
});
it('Test `detectRoutes`', async () => {
@@ -804,13 +1007,17 @@ it('Test `detectRoutes`', async () => {
}
{
const pkg = {
scripts: { build: 'next build' },
devDependencies: { next: '9.0.0' },
const detected = {
buildCommand: 'yarn build',
framework: {
slug: 'next',
version: '9.0.0',
},
};
const files = ['public/index.html', 'api/[endpoint].js'];
const { builders } = await detectBuilders(files, pkg);
const { builders } = await detectBuilders(files, detected);
const { defaultRoutes } = await detectRoutes(files, builders);
expect(defaultRoutes[1].status).toBe(404);
expect(defaultRoutes[1].src).toBe('/api(\\/.*)?$');

View File

@@ -1,6 +1,6 @@
{
"name": "@now/cgi",
"version": "1.0.1-canary.1",
"version": "1.0.1",
"license": "MIT",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "now",
"version": "16.6.4-canary.0",
"version": "16.7.1-canary.0",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Now",

View File

@@ -38,7 +38,6 @@ import {
} from '../../util/errors-ts';
import { SchemaValidationFailed } from '../../util/errors';
import purchaseDomainIfAvailable from '../../util/domains/purchase-domain-if-available';
import handleCertError from '../../util/certs/handle-cert-error';
import isWildcardAlias from '../../util/alias/is-wildcard-alias';
import shouldDeployDir from '../../util/deploy/should-deploy-dir';
@@ -382,15 +381,13 @@ export default async function main(
return 1;
}
const deploymentResponse = handleCertError(
output,
await getDeploymentByIdOrHost(now, contextName, deployment.id, 'v10')
const deploymentResponse = await getDeploymentByIdOrHost(
now,
contextName,
deployment.id,
'v10'
);
if (deploymentResponse === 1) {
return deploymentResponse;
}
if (
deploymentResponse instanceof DeploymentNotFound ||
deploymentResponse instanceof DeploymentPermissionDenied ||
@@ -400,10 +397,6 @@ export default async function main(
return 1;
}
if (handleCertError(output, deployment) === 1) {
return 1;
}
if (deployment === null) {
error('Uploading failed. Please try again.');
return 1;

View File

@@ -70,7 +70,6 @@ import {
InvalidRegionOrDCForScale,
} from '../../util/errors';
import { SchemaValidationFailed } from '../../util/errors';
import handleCertError from '../../util/certs/handle-cert-error';
import readPackage from '../../util/read-package';
interface Env {
@@ -801,11 +800,6 @@ async function sync({
createArgs
);
const handledResult = handleCertError(output, deployment);
if (handledResult === 1) {
return handledResult;
}
if (
deployment instanceof DomainNotFound ||
deployment instanceof NotDomainOwner ||

View File

@@ -10,6 +10,7 @@ import formatDate from '../../util/format-date';
import formatNSTable from '../../util/format-ns-table';
import getDomainByName from '../../util/domains/get-domain-by-name';
import getScope from '../../util/get-scope';
import getDomainPrice from '../../util/domains/get-domain-price';
type Options = {
'--debug': boolean;
@@ -23,7 +24,7 @@ export default async function inspect(
) {
const {
authConfig: { token },
config
config,
} = ctx;
const { currentTeam } = config;
const { apiUrl } = ctx;
@@ -61,7 +62,12 @@ export default async function inspect(
}
output.debug(`Fetching domain info`);
const domain = await getDomainByName(client, contextName, domainName);
const [domain, renewalPrice] = await Promise.all([
getDomainByName(client, contextName, domainName),
getDomainPrice(client, domainName, 'renewal')
.then(res => (res instanceof Error ? null : res.price))
.catch(() => null),
]);
if (domain instanceof DomainNotFound) {
output.error(
`Domain not found by "${domainName}" under ${chalk.bold(contextName)}`
@@ -104,24 +110,35 @@ export default async function inspect(
` ${chalk.cyan('Bought At')}\t\t\t${formatDate(domain.boughtAt)}\n`
);
output.print(
` ${chalk.cyan('Transferred At')}\t\t${formatDate(domain.transferredAt)}\n`
` ${chalk.cyan('Transferred At')}\t\t${formatDate(
domain.transferredAt
)}\n`
);
output.print(
` ${chalk.cyan('Expires At')}\t\t\t${formatDate(domain.expiresAt)}\n`
);
output.print(
` ${chalk.cyan('NS Verified At')}\t\t${formatDate(domain.nsVerifiedAt)}\n`
` ${chalk.cyan('NS Verified At')}\t\t${formatDate(
domain.nsVerifiedAt
)}\n`
);
output.print(
` ${chalk.cyan('TXT Verified At')}\t\t${formatDate(domain.txtVerifiedAt)}\n`
` ${chalk.cyan('TXT Verified At')}\t\t${formatDate(
domain.txtVerifiedAt
)}\n`
);
output.print(` ${chalk.cyan('CDN Enabled')}\t\t${true}\n`);
if (renewalPrice && domain.boughtAt) {
output.print(
` ${chalk.cyan('Renewal Price')}\t\t$${renewalPrice} USD\n`
);
}
output.print(` ${chalk.cyan('CDN Enabled')}\t\t\t${true}\n`);
output.print('\n');
output.print(chalk.bold(' Nameservers\n\n'));
output.print(
`${formatNSTable(domain.intendedNameservers, domain.nameservers, {
extraSpace: ' '
extraSpace: ' ',
})}\n`
);
output.print('\n');
@@ -129,7 +146,7 @@ export default async function inspect(
output.print(chalk.bold(' Verification Record\n\n'));
output.print(
`${dnsTable([['_now', 'TXT', domain.verificationRecord]], {
extraSpace: ' '
extraSpace: ' ',
})}\n`
);
output.print('\n');

View File

@@ -239,7 +239,7 @@ export default async function main(ctx) {
return 1;
}
if (deployment.version === 2) {
output.error('Cannot scale a deployment containing builds');
output.error('Cannot scale a Now 2.0 deployment');
now.close();
return 1;
}

View File

@@ -1,4 +1,3 @@
import chalk from 'chalk';
import execa from 'execa';
import semver from 'semver';
import pipe from 'promisepipe';
@@ -8,7 +7,6 @@ import { extract } from 'tar-fs';
import { createHash } from 'crypto';
import { createGunzip } from 'zlib';
import { join, resolve } from 'path';
import { funCacheDir } from '@zeit/fun';
import { PackageJson } from '@now/build-utils';
import XDGAppPaths from 'xdg-app-paths';
import {
@@ -17,7 +15,6 @@ import {
readFile,
readJSON,
writeFile,
remove,
} from 'fs-extra';
import pkg from '../../../package.json';
@@ -104,11 +101,7 @@ export async function prepareBuilderDir() {
if (!hasBundledBuilders(dependencies)) {
const extractor = extract(builderDir);
await pipe(
createReadStream(bundledTarballPath),
createGunzip(),
extractor
);
await pipe(createReadStream(bundledTarballPath), createGunzip(), extractor);
}
return builderDir;

View File

@@ -22,6 +22,8 @@ import {
PackageJson,
detectBuilders,
detectRoutes,
detectDefaults,
DetectorFilesystem,
} from '@now/build-utils';
import { once } from '../once';
@@ -100,6 +102,25 @@ function sortBuilders(buildA: Builder, buildB: Builder) {
return 0;
}
class DevDetectorFilesystem extends DetectorFilesystem {
private dir: string;
private files: string[];
constructor(dir: string, files: string[]) {
super();
this.dir = dir;
this.files = files;
}
_exists(name: string): Promise<boolean> {
return Promise.resolve(this.files.includes(name));
}
_readFile(name: string): Promise<Buffer> {
return fs.readFile(join(this.dir, name));
}
}
export default class DevServer {
public cwd: string;
public debug: boolean;
@@ -477,8 +498,6 @@ export default class DevServer {
return this.cachedNowConfig;
}
const pkg = await this.getPackageJson();
// The default empty `now.json` is used to serve all files as static
// when no `now.json` is present
let config: NowConfig = this.cachedNowConfig || { version: 2 };
@@ -526,11 +545,19 @@ export default class DevServer {
// no builds -> zero config
if (!config.builds || config.builds.length === 0) {
const { builders, warnings, errors } = await detectBuilders(files, pkg, {
tag: getDistTag(cliVersion) === 'canary' ? 'canary' : 'latest',
functions: config.functions,
const detectorResult = await detectDefaults({
fs: new DevDetectorFilesystem(this.cwd, files),
});
const { builders, warnings, errors } = await detectBuilders(
files,
detectorResult,
{
tag: getDistTag(cliVersion) === 'canary' ? 'canary' : 'latest',
functions: config.functions,
}
);
if (errors) {
this.output.error(errors[0].message);
await this.exit();
@@ -575,29 +602,6 @@ export default class DevServer {
return config;
}
async getPackageJson(): Promise<PackageJson | null> {
const pkgPath = join(this.cwd, 'package.json');
let pkg: PackageJson | null = null;
this.output.debug('Reading `package.json` file');
try {
pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
} catch (err) {
if (err.code === 'ENOENT') {
this.output.debug('No `package.json` file present');
} else if (err.name === 'SyntaxError') {
this.output.warn(
`There is a syntax error in the \`package.json\` file: ${err.message}`
);
} else {
throw err;
}
}
return pkg;
}
async tryValidateOrExit(
config: NowConfig,
validate: (c: NowConfig) => string | null

View File

@@ -8,62 +8,10 @@ import {
trailingSlashSchema,
} from '@now/routing-utils';
import { NowConfig } from './types';
import { functionsSchema, buildsSchema } from '@now/build-utils';
const ajv = new Ajv();
const buildsSchema = {
type: 'array',
minItems: 0,
maxItems: 128,
items: {
type: 'object',
additionalProperties: false,
required: ['use'],
properties: {
src: {
type: 'string',
minLength: 1,
maxLength: 4096,
},
use: {
type: 'string',
minLength: 3,
maxLength: 256,
},
config: { type: 'object' },
},
},
};
const functionsSchema = {
type: 'object',
minProperties: 1,
maxProperties: 50,
additionalProperties: false,
patternProperties: {
'^.{1,256}$': {
type: 'object',
additionalProperties: false,
properties: {
runtime: {
type: 'string',
maxLength: 256,
},
memory: {
enum: Object.keys(Array.from({ length: 50 }))
.slice(2, 48)
.map(x => Number(x) * 64),
},
maxDuration: {
type: 'number',
minimum: 1,
maximum: 900,
},
},
},
},
};
const validateBuilds = ajv.compile(buildsSchema);
const validateRoutes = ajv.compile(routesSchema);
const validateCleanUrls = ajv.compile(cleanUrlsSchema);

View File

@@ -1097,7 +1097,7 @@ export class LambdaSizeExceededError extends NowError<
size
).toLowerCase()}) exceeds the maximum size limit (${bytes(
maxLambdaSize
).toLowerCase()}). Learn more: https://zeit.co/docs/v2/deployments/concepts/lambdas/#maximum-bundle-size`,
).toLowerCase()}).`,
meta: { size, maxLambdaSize },
});
}

View File

@@ -1,4 +1,5 @@
import chalk from 'chalk';
import boxen from 'boxen';
import { format } from 'util';
import { Console } from 'console';
@@ -18,10 +19,32 @@ export default function createOutput({ debug: debugEnabled = false } = {}) {
}
function warn(str: string, slug: string | null = null) {
log(chalk`{yellow.bold WARN!} ${str}`);
if (slug !== null) {
log(`More details: https://err.sh/now/${slug}`);
const prevTerm = process.env.TERM;
if (!prevTerm) {
// workaround for https://github.com/sindresorhus/term-size/issues/13
process.env.TERM = 'xterm';
}
print(
boxen(
chalk.bold.yellow('WARN! ') +
str +
(slug ? `\nMore details: https://err.sh/now/${slug}` : ''),
{
padding: {
top: 0,
bottom: 0,
left: 1,
right: 1,
},
borderColor: 'yellow',
}
)
);
print('\n');
process.env.TERM = prevTerm;
}
function note(str: string) {
@@ -59,7 +82,7 @@ export default function createOutput({ debug: debugEnabled = false } = {}) {
_times: new Map(),
log(a: string, ...args: string[]) {
debug(format(a, ...args));
}
},
};
async function time(label: string, fn: Promise<any> | (() => Promise<any>)) {
@@ -85,6 +108,6 @@ export default function createOutput({ debug: debugEnabled = false } = {}) {
debug,
dim,
time,
note
note,
};
}

View File

@@ -58,9 +58,7 @@ export default async function preferV2Deployment({
)} is missing a ${cmd('start')} script. ${INFO}`;
}
} else if (!pkg && !hasDockerfile) {
return `Deploying to Now 2.0, because no ${highlight(
'Dockerfile'
)} was found. ${INFO}`;
return `Deploying to Now 2.0 automatically. ${INFO}`;
}
if (client && projectName) {

View File

@@ -1,5 +0,0 @@
{
"scripts": {
"build": "./hugo"
}
}

View File

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

View File

@@ -1238,7 +1238,7 @@ deep-extend@^0.6.0:
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
define-properties@^1.1.2:
define-properties@^1.1.2, define-properties@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
@@ -1660,7 +1660,7 @@ fsevents@^1.2.7:
nan "^2.12.1"
node-pre-gyp "^0.12.0"
function-bind@^1.0.2, function-bind@^1.1.0, function-bind@^1.1.1:
function-bind@^1.1.0, function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
@@ -3305,21 +3305,21 @@ string-width@^1.0.1:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^4.0.0"
string.prototype.trimleft@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.0.0.tgz#68b6aa8e162c6a80e76e3a8a0c2e747186e271ff"
integrity sha1-aLaqjhYsaoDnbjqKDC50cYbicf8=
string.prototype.trimleft@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634"
integrity sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==
dependencies:
define-properties "^1.1.2"
function-bind "^1.0.2"
define-properties "^1.1.3"
function-bind "^1.1.1"
string.prototype.trimright@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.0.0.tgz#ab4a56d802a01fbe7293e11e84f24dc8164661dd"
integrity sha1-q0pW2AKgH75yk+EehPJNyBZGYd0=
string.prototype.trimright@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz#669d164be9df9b6f7559fa8e89945b168a5a6c58"
integrity sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==
dependencies:
define-properties "^1.1.2"
function-bind "^1.0.2"
define-properties "^1.1.3"
function-bind "^1.1.1"
string_decoder@~0.10.31, string_decoder@~0.10.x:
version "0.10.31"

View File

@@ -0,0 +1 @@
!public

View File

@@ -0,0 +1,7 @@
{
"functions": {
"server/**/*.js": {
"runtime": "@now/node@1.2.1"
}
}
}

View File

@@ -0,0 +1 @@
This is content.

View File

@@ -0,0 +1,3 @@
export default (req, res) => {
res.end(`current hour: ${Math.floor(Date.now() / 10000)}`);
};

View File

@@ -15,6 +15,11 @@ let port = 3000;
const binaryPath = path.resolve(__dirname, `../../scripts/start.js`);
const fixture = name => path.join('test', 'dev', 'fixtures', name);
// Adds Hugo to the PATH
process.env.PATH = `${path.resolve(fixture('08-hugo'))}${path.delimiter}${
process.env.PATH
}`;
function fetchWithRetry(url, retries = 3, opts = {}) {
return new Promise(async (resolve, reject) => {
try {
@@ -128,17 +133,15 @@ function testFixtureStdio(directory, fn) {
readyResolve = resolve;
});
console.log(`> testing ${directory}`);
dev = execa(binaryPath, ['dev', dir, '-l', port]);
dev.stderr.on('data', async data => {
output += data.toString();
if (data.toString().includes('Ready! Available at')) {
if (output.includes('Ready! Available at')) {
readyResolve();
}
if (
data.toString().includes('Command failed') ||
data.toString().includes('Error!')
) {
if (output.includes('Command failed') || output.includes('Error!')) {
dev.kill('SIGTERM');
console.log(output);
process.exit(1);
@@ -511,13 +514,12 @@ 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;
const response = await fetch(`http://localhost:${port}`);
validateResponseHeaders(t, response);
const body = await response.text();
t.regex(body, /Hello, world!/gm);
t.regex(body, /<div id="app"><\/div>/gm);
})
);
} else {
@@ -529,8 +531,7 @@ if (satisfies(process.version, '>= 6.9.0 <7.0.0 || >= 8.9.0')) {
test(
'[now dev] 07-hexo-node',
testFixtureStdio('07-hexo-node', async (t, port) => {
const result = await fetchWithRetry(`http://localhost:${port}`, 180);
const response = await result;
const response = await fetchWithRetry(`http://localhost:${port}`, 180);
validateResponseHeaders(t, response);
@@ -542,8 +543,7 @@ test(
test(
'[now dev] 08-hugo',
testFixtureStdio('08-hugo', async (t, port) => {
const result = fetch(`http://localhost:${port}`);
const response = await result;
const response = await fetch(`http://localhost:${port}`);
validateResponseHeaders(t, response);
@@ -840,8 +840,7 @@ if (satisfies(process.version, '>= 8.10.0')) {
test(
'[now dev] 22-brunch',
testFixtureStdio('22-brunch', async (t, port) => {
const result = fetch(`http://localhost:${port}`);
const response = await result;
const response = await fetchWithRetry(`http://localhost:${port}`, 50);
validateResponseHeaders(t, response);
@@ -1114,8 +1113,7 @@ if (satisfies(process.version, '>= 8.9.0')) {
// start `now dev` detached in child_process
dev.unref();
const result = await fetchWithRetry(`http://localhost:${port}`, 80);
const response = await result;
const response = await fetchWithRetry(`http://localhost:${port}`, 80);
validateResponseHeaders(t, response);
@@ -1134,8 +1132,10 @@ if (satisfies(process.version, '>= 8.9.0')) {
test(
'[now dev] Use runtime from the functions property',
testFixtureStdio('custom-runtime', async (t, port) => {
const result = await fetchWithRetry(`http://localhost:${port}/api/user`, 3);
const response = await result;
const response = await fetchWithRetry(
`http://localhost:${port}/api/user`,
3
);
validateResponseHeaders(t, response);
@@ -1143,3 +1143,20 @@ test(
t.regex(body, /Hello, from Bash!/gm);
})
);
test(
'[now dev] Use public with a custom Serverless Function in `server/date.js',
testFixtureStdio('public-and-server-as-api', async (t, port) => {
const response = await fetchWithRetry(
`http://localhost:${port}/server/date`
);
validateResponseHeaders(t, response);
t.is(response.status, 200);
t.is(
await response.text(),
`current hour: ${Math.floor(Date.now() / 10000)}`
);
})
);

View File

@@ -493,6 +493,35 @@ test('list the payment methods', async t => {
t.true(stdout.startsWith(`> 0 cards found under ${contextName}`));
});
test('domains inspect', async t => {
const domainName = `inspect-${contextName}.org`;
const addRes = await execa(
binaryPath,
[`domains`, `add`, domainName, ...defaultArgs],
{ reject: false }
);
t.is(addRes.exitCode, 0);
const { stderr, exitCode } = await execa(
binaryPath,
['domains', 'inspect', domainName, ...defaultArgs],
{
reject: false,
}
);
const rmRes = await execa(
binaryPath,
[`domains`, `rm`, domainName, ...defaultArgs],
{ reject: false, input: 'y' }
);
t.is(rmRes.exitCode, 0);
t.is(exitCode, 0);
t.true(!stderr.includes(`Renewal Price`));
});
test('try to purchase a domain', async t => {
const { stderr, stdout, exitCode } = await execa(
binaryPath,
@@ -767,7 +796,19 @@ test('create wildcard alias for deployment', async t => {
t.true(stdout.startsWith(goal));
// Send a test request to the alias
const response = await fetch(`https://test.${contextName}.now.sh`);
// Retries to make sure we consider the time it takes to update
const response = await retry(
async () => {
const response = await fetch(`https://test.${contextName}.now.sh`);
if (response.ok) {
return response;
}
throw new Error(`Error: Returned code ${response.status}`);
},
{ retries: 3 }
);
const content = await response.text();
t.true(response.ok);
@@ -874,7 +915,7 @@ test('ensure we render a warning for deployments with no files', async t => {
// Ensure the warning is printed
t.true(
stderr.includes(
'> WARN! There are no files (or only files starting with a dot) inside your deployment.'
'WARN! There are no files (or only files starting with a dot) inside your deployment.'
)
);

View File

@@ -17,7 +17,7 @@ import getURL from './helpers/get-url';
import {
npm as getNpmFiles_,
docker as getDockerFiles_,
staticFiles as getStaticFiles_
staticFiles as getStaticFiles_,
} from '../src/util/get-files';
import didYouMean from '../src/util/init/did-you-mean';
import { isValidName } from '../src/util/is-valid-name';
@@ -32,7 +32,7 @@ const fixture = name => join(prefix, name);
const getNpmFiles = async dir => {
const { pkg, nowConfig, hasNowJson } = await readMetadata(dir, {
quiet: true,
strict: false
strict: false,
});
return getNpmFiles_(dir, pkg, nowConfig, { hasNowJson, output });
@@ -41,7 +41,7 @@ const getNpmFiles = async dir => {
const getDockerFiles = async dir => {
const { nowConfig, hasNowJson } = await readMetadata(dir, {
quiet: true,
strict: false
strict: false,
});
return getDockerFiles_(dir, nowConfig, { hasNowJson, output });
@@ -51,7 +51,7 @@ const getStaticFiles = async (dir, isBuilds = false) => {
const { nowConfig, hasNowJson } = await readMetadata(dir, {
deploymentType: 'static',
quiet: true,
strict: false
strict: false,
});
return getStaticFiles_(dir, nowConfig, { hasNowJson, output, isBuilds });
@@ -169,11 +169,9 @@ test('`now.files` overrides `.gitignore` in Static with custom config path', asy
const path = 'now-json-static-gitignore-override';
// Simulate custom args passed by the user
process.argv = [...process.argv, '--local-config', './now.json']
process.argv = [...process.argv, '--local-config', './now.json'];
let files = await getStaticFiles(
fixture(path)
);
let files = await getStaticFiles(fixture(path));
files = files.sort(alpha);
@@ -185,9 +183,7 @@ test('`now.files` overrides `.gitignore` in Static with custom config path', asy
test('`now.files` overrides `.gitignore` in Static', async t => {
const path = 'now-json-static-gitignore-override';
let files = await getStaticFiles(
fixture(path)
);
let files = await getStaticFiles(fixture(path));
files = files.sort(alpha);
t.is(files.length, 3);
@@ -337,7 +333,7 @@ test('support docker', async t => {
test('gets correct name of docker deployment', async t => {
const { name, deploymentType } = await readMetadata(fixture('dockerfile'), {
quiet: true,
strict: false
strict: false,
});
t.is(deploymentType, 'docker');
@@ -375,7 +371,7 @@ test('throw for unsupported `now.json` type property', async t => {
try {
await readMetadata(f, {
quiet: true,
strict: false
strict: false,
});
} catch (err) {
t.is(err.code, 'unsupported_deployment_type');
@@ -387,7 +383,7 @@ test('support `now.json` files with package.json non quiet', async t => {
const f = fixture('now-json-no-name');
const { deploymentType } = await readMetadata(f, {
quiet: false,
strict: false
strict: false,
});
t.is(deploymentType, 'npm');
@@ -404,7 +400,7 @@ test('support `now.json` files with package.json non quiet', async t => {
test('support `now.json` files with package.json non quiet not specified', async t => {
const f = fixture('now-json-no-name');
const { deploymentType } = await readMetadata(f, {
strict: false
strict: false,
});
t.is(deploymentType, 'npm');
@@ -423,7 +419,7 @@ test('No commands in Dockerfile with automatic strictness', async t => {
try {
await readMetadata(f, {
quiet: true
quiet: true,
});
} catch (err) {
t.is(err.code, 'no_dockerfile_commands');
@@ -437,7 +433,7 @@ test('No commands in Dockerfile', async t => {
try {
await readMetadata(f, {
quiet: true,
strict: true
strict: true,
});
} catch (err) {
t.is(err.code, 'no_dockerfile_commands');
@@ -451,7 +447,7 @@ test('Missing Dockerfile for `docker` type', async t => {
try {
await readMetadata(f, {
quiet: true,
strict: true
strict: true,
});
} catch (err) {
t.is(err.code, 'dockerfile_missing');
@@ -463,7 +459,7 @@ test('support `now.json` files with Dockerfile', async t => {
const f = fixture('now-json-docker');
const { deploymentType, nowConfig, hasNowJson } = await readMetadata(f, {
quiet: true,
strict: false
strict: false,
});
t.is(deploymentType, 'docker');
@@ -479,7 +475,7 @@ test('load name from Dockerfile', async t => {
const f = fixture('now-json-docker-name');
const { deploymentType, name } = await readMetadata(f, {
quiet: true,
strict: false
strict: false,
});
t.is(deploymentType, 'docker');
@@ -490,7 +486,7 @@ test('support `now.json` files with Dockerfile non quiet', async t => {
const f = fixture('now-json-docker');
const { deploymentType, nowConfig, hasNowJson } = await readMetadata(f, {
quiet: false,
strict: false
strict: false,
});
t.is(deploymentType, 'docker');
@@ -507,7 +503,7 @@ test('throws when both `now.json` and `package.json:now` exist', async t => {
try {
await readMetadata(fixture('now-json-throws'), {
quiet: true,
strict: false
strict: false,
});
} catch (err) {
e = err;
@@ -523,7 +519,7 @@ test('throws when `package.json` and `Dockerfile` exist', async t => {
try {
await readMetadata(fixture('multiple-manifests-throws'), {
quiet: true,
strict: false
strict: false,
});
} catch (err) {
e = err;
@@ -536,7 +532,7 @@ test('support `package.json:now.type` to bypass multiple manifests error', async
const f = fixture('type-in-package-now-with-dockerfile');
const { type, nowConfig, hasNowJson } = await readMetadata(f, {
quiet: true,
strict: false
strict: false,
});
t.is(type, 'npm');
t.is(nowConfig.type, 'npm');
@@ -544,16 +540,16 @@ test('support `package.json:now.type` to bypass multiple manifests error', async
});
test('friendly error for malformed JSON', async t => {
const err = await t.throwsAsync(
() => readMetadata(fixture('json-syntax-error'), {
const err = await t.throwsAsync(() =>
readMetadata(fixture('json-syntax-error'), {
quiet: true,
strict: false
strict: false,
})
);
t.is(err.name, 'JSONError');
t.is(
err.message,
'Unexpected token \'o\' at 2:5 in test/fixtures/unit/json-syntax-error/package.json\n oops\n ^'
"Unexpected token 'o' at 2:5 in test/fixtures/unit/json-syntax-error/package.json\n oops\n ^"
);
});
@@ -597,7 +593,7 @@ test('`wait` utility does not invoke spinner before n miliseconds', async t => {
const oraStub = sinon.stub().returns({
color: '',
start: () => {},
stop: () => {}
stop: () => {},
});
const timeOut = 200;
@@ -612,7 +608,7 @@ test('`wait` utility invokes spinner after n miliseconds', async t => {
const oraStub = sinon.stub().returns({
color: '',
start: () => {},
stop: () => {}
stop: () => {},
});
const timeOut = 200;
@@ -635,7 +631,7 @@ test('`wait` utility does not invoke spinner when stopped before delay', async t
const oraStub = sinon.stub().returns({
color: '',
start: () => {},
stop: () => {}
stop: () => {},
});
const timeOut = 200;
@@ -694,8 +690,8 @@ test('4xx response error as correct JSON', async t => {
const fn = async (req, res) => {
send(res, 400, {
error: {
message: 'The request is not correct'
}
message: 'The request is not correct',
},
});
};
@@ -721,7 +717,7 @@ test('5xx response error as HTML', async t => {
test('5xx response error with random JSON', async t => {
const fn = async (req, res) => {
send(res, 500, {
wrong: 'property'
wrong: 'property',
});
};
@@ -733,23 +729,27 @@ test('5xx response error with random JSON', async t => {
});
test('getProjectName with argv - option 1', t => {
const project = getProjectName({argv: {
name: 'abc'
}});
const project = getProjectName({
argv: {
name: 'abc',
},
});
t.is(project, 'abc');
});
test('getProjectName with argv - option 2', t => {
const project = getProjectName({argv: {
'--name': 'abc'
}});
const project = getProjectName({
argv: {
'--name': 'abc',
},
});
t.is(project, 'abc');
});
test('getProjectName with now.json', t => {
const project = getProjectName({
argv: {},
nowConfig: {name: 'abc'}
nowConfig: { name: 'abc' },
});
t.is(project, 'abc');
});
@@ -758,7 +758,7 @@ test('getProjectName with a file', t => {
const project = getProjectName({
argv: {},
nowConfig: {},
isFile: true
isFile: true,
});
t.is(project, 'files');
});
@@ -767,7 +767,7 @@ test('getProjectName with a multiple files', t => {
const project = getProjectName({
argv: {},
nowConfig: {},
paths: ['/tmp/aa/abc.png', '/tmp/aa/bbc.png']
paths: ['/tmp/aa/abc.png', '/tmp/aa/bbc.png'],
});
t.is(project, 'files');
});
@@ -776,7 +776,7 @@ test('getProjectName with a directory', t => {
const project = getProjectName({
argv: {},
nowConfig: {},
paths: ['/tmp/aa']
paths: ['/tmp/aa'],
});
t.is(project, 'aa');
});
@@ -797,8 +797,8 @@ test('4xx error message with proper message', async t => {
const fn = async (req, res) => {
send(res, 403, {
error: {
message: 'This is a test'
}
message: 'This is a test',
},
});
};
@@ -813,8 +813,8 @@ test('5xx error message with proper message', async t => {
const fn = async (req, res) => {
send(res, 500, {
error: {
message: 'This is a test'
}
message: 'This is a test',
},
});
};
@@ -842,8 +842,8 @@ test('4xx response error as correct JSON with more properties', async t => {
send(res, 403, {
error: {
message: 'The request is not correct',
additionalProperty: 'test'
}
additionalProperty: 'test',
},
});
};
@@ -861,8 +861,8 @@ test('429 response error with retry header', async t => {
send(res, 429, {
error: {
message: 'You were rate limited'
}
message: 'You were rate limited',
},
});
};
@@ -878,8 +878,8 @@ test('429 response error without retry header', async t => {
const fn = async (req, res) => {
send(res, 429, {
error: {
message: 'You were rate limited'
}
message: 'You were rate limited',
},
});
};
@@ -891,8 +891,41 @@ test('429 response error without retry header', async t => {
t.is(formatted.retryAfter, undefined);
});
test('guess user\'s intention with custom didYouMean', async t => {
const examples = ['apollo','create-react-app','docz','gatsby','go','gridsome','html-minifier','mdx-deck','monorepo','nextjs','nextjs-news','nextjs-static','node-server','nodejs','nodejs-canvas-partyparrot','nodejs-coffee','nodejs-express','nodejs-hapi','nodejs-koa','nodejs-koa-ts','nodejs-pdfkit','nuxt-static','optipng','php-7','puppeteer-screenshot','python','redirect','serverless-ssr-reddit','static','vue','vue-ssr','vuepress'];
test("guess user's intention with custom didYouMean", async t => {
const examples = [
'apollo',
'create-react-app',
'docz',
'gatsby',
'go',
'gridsome',
'html-minifier',
'mdx-deck',
'monorepo',
'nextjs',
'nextjs-news',
'nextjs-static',
'node-server',
'nodejs',
'nodejs-canvas-partyparrot',
'nodejs-coffee',
'nodejs-express',
'nodejs-hapi',
'nodejs-koa',
'nodejs-koa-ts',
'nodejs-pdfkit',
'nuxt-static',
'optipng',
'php-7',
'puppeteer-screenshot',
'python',
'redirect',
'serverless-ssr-reddit',
'static',
'vue',
'vue-ssr',
'vuepress',
];
t.is(didYouMean('md', examples, 0.7), 'mdx-deck');
t.is(didYouMean('koa', examples, 0.7), 'nodejs-koa');
@@ -906,16 +939,26 @@ test('check platform version chanage with `preferV2Deployment`', async t => {
const pkg = null;
const hasDockerfile = false;
const hasServerfile = false;
const reason = await preferV2Deployment({ localConfig, pkg, hasDockerfile, hasServerfile });
t.regex(reason, /Dockerfile/gm);
const reason = await preferV2Deployment({
localConfig,
pkg,
hasDockerfile,
hasServerfile,
});
t.regex(reason, /Deploying to Now 2\.0 automatically/gm);
}
{
const localConfig = undefined;
const pkg = { scripts: { 'start': 'echo hi' } };
const pkg = { scripts: { start: 'echo hi' } };
const hasDockerfile = false;
const hasServerfile = false;
const reason = await preferV2Deployment({ localConfig, pkg, hasDockerfile, hasServerfile });
const reason = await preferV2Deployment({
localConfig,
pkg,
hasDockerfile,
hasServerfile,
});
t.is(reason, null);
}
@@ -924,16 +967,26 @@ test('check platform version chanage with `preferV2Deployment`', async t => {
const pkg = { scripts: { 'now-start': 'echo hi' } };
const hasDockerfile = false;
const hasServerfile = false;
const reason = await preferV2Deployment({ localConfig, pkg, hasDockerfile, hasServerfile });
const reason = await preferV2Deployment({
localConfig,
pkg,
hasDockerfile,
hasServerfile,
});
t.is(reason, null);
}
{
const localConfig = { 'version': 1 };
const localConfig = { version: 1 };
const pkg = null;
const hasDockerfile = false;
const hasServerfile = false;
const reason = await preferV2Deployment({ localConfig, pkg, hasDockerfile, hasServerfile });
const reason = await preferV2Deployment({
localConfig,
pkg,
hasDockerfile,
hasServerfile,
});
t.is(reason, null);
}
@@ -942,16 +995,26 @@ test('check platform version chanage with `preferV2Deployment`', async t => {
const pkg = null;
const hasDockerfile = true;
const hasServerfile = false;
const reason = await preferV2Deployment({ localConfig, pkg, hasDockerfile, hasServerfile });
const reason = await preferV2Deployment({
localConfig,
pkg,
hasDockerfile,
hasServerfile,
});
t.is(reason, null);
}
{
const localConfig = undefined;
const pkg = { scripts: { 'build': 'echo hi' } };
const pkg = { scripts: { build: 'echo hi' } };
const hasDockerfile = false;
const hasServerfile = false;
const reason = await preferV2Deployment({ localConfig, pkg, hasDockerfile, hasServerfile });
const reason = await preferV2Deployment({
localConfig,
pkg,
hasDockerfile,
hasServerfile,
});
t.regex(reason, /package\.json/gm);
}
@@ -960,7 +1023,12 @@ test('check platform version chanage with `preferV2Deployment`', async t => {
const pkg = null;
const hasDockerfile = false;
const hasServerfile = true;
const reason = await preferV2Deployment({ localConfig, pkg, hasDockerfile, hasServerfile });
const reason = await preferV2Deployment({
localConfig,
pkg,
hasDockerfile,
hasServerfile,
});
t.is(reason, null);
}
});

View File

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

View File

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

View File

@@ -1,16 +1,3 @@
import { ChildProcess, fork } from 'child_process';
import {
pathExists,
readFile,
unlink as unlinkFile,
writeFile,
lstatSync,
} from 'fs-extra';
import os from 'os';
import path from 'path';
import resolveFrom from 'resolve-from';
import semver from 'semver';
import {
BuildOptions,
Config,
@@ -20,6 +7,7 @@ import {
FileBlob,
FileFsRef,
Files,
getLambdaOptionsFromFunction,
getNodeVersion,
getSpawnOptions,
glob,
@@ -30,10 +18,24 @@ import {
Route,
runNpmInstall,
runPackageJsonScript,
getLambdaOptionsFromFunction,
} from '@now/build-utils';
import {
convertRedirects,
convertRewrites,
} from '@now/routing-utils/dist/superstatic';
import nodeFileTrace, { NodeFileTraceReasons } from '@zeit/node-file-trace';
import { ChildProcess, fork } from 'child_process';
import {
lstatSync,
pathExists,
readFile,
unlink as unlinkFile,
writeFile,
} from 'fs-extra';
import os from 'os';
import path from 'path';
import resolveFrom from 'resolve-from';
import semver from 'semver';
import createServerlessConfig from './create-serverless-config';
import nextLegacyVersions from './legacy-versions';
import {
@@ -43,10 +45,14 @@ import {
excludeFiles,
ExperimentalTraceVersion,
getDynamicRoutes,
getExportIntent,
getExportStatus,
getNextConfig,
getPathsInside,
getPrerenderManifest,
getRoutes,
getRoutesManifest,
getSourceFilePathFromPage,
isDynamicRoute,
normalizePackageJson,
normalizePage,
@@ -54,15 +60,8 @@ import {
stringMap,
syncEnvVars,
validateEntrypoint,
getSourceFilePathFromPage,
getRoutesManifest,
} from './utils';
import {
convertRedirects,
convertRewrites,
} from '@now/routing-utils/dist/superstatic';
interface BuildParamsMeta {
isDev: boolean | undefined;
env?: EnvConfig;
@@ -334,6 +333,122 @@ export const build = async ({
env.NODE_OPTIONS = `--max_old_space_size=${memoryToConsume}`;
await runPackageJsonScript(entryPath, shouldRunScript, { ...spawnOpts, env });
const routesManifest = await getRoutesManifest(entryPath, realNextVersion);
const rewrites: Route[] = [];
const redirects: Route[] = [];
if (routesManifest) {
switch (routesManifest.version) {
case 1: {
redirects.push(...convertRedirects(routesManifest.redirects));
rewrites.push(...convertRewrites(routesManifest.rewrites));
break;
}
default: {
// update MIN_ROUTES_MANIFEST_VERSION in ./utils.ts
throw new Error(
'This version of `@now/next` does not support the version of Next.js you are trying to deploy.\n' +
'Please upgrade your `@now/next` builder and try again. Contact support if this continues to happen.'
);
}
}
}
const exportIntent = await getExportIntent(entryPath);
const userExport = await getExportStatus(entryPath);
if (exportIntent || userExport) {
const { trailingSlash = false } = exportIntent || {};
if (!userExport) {
await writePackageJson(entryPath, {
...pkg,
scripts: {
...pkg.scripts,
'now-automatic-next-export': `next export --outdir "${path.resolve(
entryPath,
'out'
)}"`,
},
});
await runPackageJsonScript(entryPath, 'now-automatic-next-export', {
...spawnOpts,
env,
});
}
const resultingExport = await getExportStatus(entryPath);
if (!resultingExport) {
throw new Error(
'Exporting Next.js app failed. Please check your build logs and contact us if this continues.'
);
}
if (resultingExport.success !== true) {
throw new Error(
'Export of Next.js app failed. Please check your build logs.'
);
}
const outDirectory = resultingExport.outDirectory;
debug(`next export should use trailing slash: ${trailingSlash}`);
// This handles pages, `public/`, and `static/`.
const filesAfterBuild = await glob('**', outDirectory);
const output: Files = { ...filesAfterBuild };
// Strip `.html` extensions from build output
Object.entries(output)
.filter(([name]) => name.endsWith('.html'))
.forEach(([name, value]) => {
const cleanName = name.slice(0, -5);
delete output[name];
output[cleanName] = value;
if (value.type === 'FileBlob' || value.type === 'FileFsRef') {
value.contentType = value.contentType || 'text/html; charset=utf-8';
}
});
return {
output,
routes: [
// TODO: low priority: handle trailingSlash
// redirects take the highest priority
...redirects,
// Before we handle static files we need to set proper caching headers
{
// This ensures we only match known emitted-by-Next.js files and not
// user-emitted files which may be missing a hash in their filename.
src: path.join(
'/',
entryDirectory,
'_next/static/(?:[^/]+/pages|chunks|runtime|css|media)/.+'
),
// Next.js assets contain a hash or entropy in their filenames, so they
// are guaranteed to be unique and cacheable indefinitely.
headers: { 'cache-control': 'public,max-age=31536000,immutable' },
continue: true,
},
{
src: path.join('/', entryDirectory, '_next(?!/data(?:/|$))(?:/.*)?'),
},
// Next.js pages, `static/` folder, reserved assets, and `public/`
// folder
{ handle: 'filesystem' },
...rewrites,
// Dynamic routes
// TODO: do we want to do this?: ...dynamicRoutes,
],
watch: [],
childProcesses: [],
};
}
if (isLegacy) {
debug('Running npm install --production...');
await runNpmInstall(
@@ -819,8 +934,6 @@ export const build = async ({
let dynamicPrefix = path.join('/', entryDirectory);
dynamicPrefix = dynamicPrefix === '/' ? '' : dynamicPrefix;
const routesManifest = await getRoutesManifest(entryPath, realNextVersion);
const dynamicRoutes = await getDynamicRoutes(
entryPath,
entryDirectory,
@@ -834,26 +947,6 @@ export const build = async ({
})
);
const rewrites: Route[] = [];
const redirects: Route[] = [];
if (routesManifest) {
switch (routesManifest.version) {
case 1: {
redirects.push(...convertRedirects(routesManifest.redirects));
rewrites.push(...convertRewrites(routesManifest.rewrites));
break;
}
default: {
// update MIN_ROUTES_MANIFEST_VERSION in ./utils.ts
throw new Error(
'This version of `@now/next` does not support the version of Next.js you are trying to deploy.\n' +
'Please upgrade your `@now/next` builder and try again. Contact support if this continues to happen.'
);
}
}
}
return {
output: {
...publicDirectoryFiles,

View File

@@ -624,6 +624,73 @@ export type NextPrerenderedRoutes = {
};
};
export async function getExportIntent(
entryPath: string
): Promise<false | { trailingSlash: boolean }> {
const pathExportMarker = path.join(entryPath, '.next', 'export-marker.json');
const hasExportMarker: boolean = await fs
.access(pathExportMarker, fs.constants.F_OK)
.then(() => true)
.catch(() => false);
if (!hasExportMarker) {
return false;
}
const manifest: {
version: 1;
exportTrailingSlash: boolean;
hasExportPathMap: boolean;
} = JSON.parse(await fs.readFile(pathExportMarker, 'utf8'));
switch (manifest.version) {
case 1: {
if (manifest.hasExportPathMap !== true) {
return false;
}
return { trailingSlash: manifest.exportTrailingSlash };
}
default: {
return false;
}
}
}
export async function getExportStatus(
entryPath: string
): Promise<false | { success: boolean; outDirectory: string }> {
const pathExportDetail = path.join(entryPath, '.next', 'export-detail.json');
const hasExportDetail: boolean = await fs
.access(pathExportDetail, fs.constants.F_OK)
.then(() => true)
.catch(() => false);
if (!hasExportDetail) {
return false;
}
const manifest: {
version: 1;
success: boolean;
outDirectory: string;
} = JSON.parse(await fs.readFile(pathExportDetail, 'utf8'));
switch (manifest.version) {
case 1: {
return {
success: !!manifest.success,
outDirectory: manifest.outDirectory,
};
}
default: {
return false;
}
}
}
export async function getPrerenderManifest(
entryPath: string
): Promise<NextPrerenderedRoutes> {

View File

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

View File

@@ -0,0 +1,16 @@
{
"version": 2,
"builds": [{ "src": "package.json", "use": "@now/next" }],
"probes": [
{
"path": "/_next/static/testing-build-id/pages/index.js",
"responseHeaders": {
"cache-control": "public,max-age=31536000,immutable"
}
},
{
"path": "/",
"mustContain": "nextExport\":true"
}
]
}

View File

@@ -0,0 +1,10 @@
{
"scripts": {
"build": "next build && next export"
},
"dependencies": {
"next": "canary",
"react": "^16.8.6",
"react-dom": "^16.8.6"
}
}

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
{
"version": 2,
"builds": [{ "src": "package.json", "use": "@now/next" }],
"probes": [
{
"path": "/",
"mustContain": "Hi There"
},
{
"path": "/about",
"mustContain": "Hi on About"
},
{
"path": "/",
"mustContain": "nextExport\":true"
}
]
}

View File

@@ -0,0 +1,10 @@
{
"scripts": {
"build": "next build && next export"
},
"dependencies": {
"next": "canary",
"react": "^16.8.6",
"react-dom": "^16.8.6"
}
}

View File

@@ -0,0 +1,7 @@
function About() {
return <div>Hi on About</div>;
}
About.getInitialProps = () => ({});
export default About;

View File

@@ -0,0 +1,7 @@
function Home() {
return <div>Hi There</div>;
}
Home.getInitialProps = () => ({});
export default Home;

View File

@@ -0,0 +1,6 @@
module.exports = {
generateBuildId() {
return 'testing-build-id';
},
exportPathMap: d => d,
};

View File

@@ -0,0 +1,18 @@
{
"version": 2,
"builds": [{ "src": "package.json", "use": "@now/next" }],
"probes": [
{
"path": "/",
"mustContain": "Hi There"
},
{
"path": "/about",
"mustContain": "Hi on About"
},
{
"path": "/",
"mustContain": "nextExport\":true"
}
]
}

View File

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

View File

@@ -0,0 +1,7 @@
function About() {
return <div>Hi on About</div>;
}
About.getInitialProps = () => ({});
export default About;

View File

@@ -0,0 +1,7 @@
function Home() {
return <div>Hi There</div>;
}
Home.getInitialProps = () => ({});
export default Home;

View File

@@ -0,0 +1,24 @@
module.exports = {
generateBuildId() {
return 'testing-build-id';
},
exportPathMap: d => d,
experimental: {
async rewrites() {
return [
{
source: '/first',
destination: '/',
},
];
},
async redirects() {
return [
{
source: '/second',
destination: '/about',
},
];
},
},
};

View File

@@ -0,0 +1,18 @@
{
"version": 2,
"builds": [{ "src": "package.json", "use": "@now/next" }],
"probes": [
{
"path": "/first",
"mustContain": "Hi There"
},
{
"path": "/second",
"mustContain": "Hi on About"
},
{
"path": "/",
"mustContain": "nextExport\":true"
}
]
}

View File

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

View File

@@ -0,0 +1,7 @@
function About() {
return <div>Hi on About</div>;
}
About.getInitialProps = () => ({});
export default About;

View File

@@ -0,0 +1,7 @@
function Home() {
return <div>Hi There</div>;
}
Home.getInitialProps = () => ({});
export default Home;

View File

@@ -3,7 +3,7 @@ const path = require('path');
const {
packAndDeploy,
testDeployment
testDeployment,
} = require('../../../test/lib/deployment/test-deployment.js');
jest.setTimeout(4 * 60 * 1000);

View File

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

View File

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

View File

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

View File

@@ -19,44 +19,53 @@ import {
export { getCleanUrls } from './superstatic';
export { mergeRoutes } from './merge';
const VALID_HANDLE_VALUES = ['filesystem', 'hit', 'miss'] as const;
const validHandleValues = new Set<string>(VALID_HANDLE_VALUES);
export type HandleValue = typeof VALID_HANDLE_VALUES[number];
export function isHandler(route: Route): route is Handler {
return typeof (route as Handler).handle !== 'undefined';
}
export function isValidHandleValue(handle: string): handle is HandleValue {
return validHandleValues.has(handle);
}
export function normalizeRoutes(inputRoutes: Route[] | null): NormalizedRoutes {
if (!inputRoutes || inputRoutes.length === 0) {
return { routes: inputRoutes, error: null };
}
const routes: Route[] = [];
const handling: string[] = [];
const errors = [];
const handling: HandleValue[] = [];
const errors: NowErrorNested[] = [];
// We don't want to treat the input routes as references
inputRoutes.forEach(r => routes.push(Object.assign({}, r)));
for (const route of routes) {
if (isHandler(route)) {
// typeof { handle: string }
if (Object.keys(route).length !== 1) {
errors.push({
message: `Cannot have any other keys when handle is used (handle: ${route.handle})`,
handle: route.handle,
});
}
if (!['filesystem'].includes(route.handle)) {
const { handle } = route;
if (!isValidHandleValue(handle)) {
errors.push({
message: `This is not a valid handler (handle: ${route.handle})`,
handle: route.handle,
message: `This is not a valid handler (handle: ${handle})`,
handle: handle,
});
continue;
}
if (handling.includes(route.handle)) {
if (handling.includes(handle)) {
errors.push({
message: `You can only handle something once (handle: ${route.handle})`,
handle: route.handle,
message: `You can only handle something once (handle: ${handle})`,
handle: handle,
});
} else {
handling.push(route.handle);
handling.push(handle);
}
} else if (route.src) {
// Route src should always start with a '^'
@@ -76,6 +85,35 @@ export function normalizeRoutes(inputRoutes: Route[] | null): NormalizedRoutes {
if (regError) {
errors.push(regError);
}
// The last seen handling is the current handler
const handleValue = handling[handling.length - 1];
if (handleValue === 'hit') {
if (route.dest) {
errors.push({
message: `You cannot assign "dest" after "handle: hit"`,
src: route.src,
});
}
if (!route.continue) {
errors.push({
message: `You must assign "continue: true" after "handle: hit"`,
src: route.src,
});
}
} else if (handleValue === 'miss') {
if (route.dest && !route.check) {
errors.push({
message: `You must assign "check: true" after "handle: miss"`,
src: route.src,
});
} else if (!route.dest && !route.continue) {
errors.push({
message: `You must assign "continue: true" after "handle: miss"`,
src: route.src,
});
}
}
} else {
errors.push({
message: 'A route must set either handle or src',

View File

@@ -54,6 +54,19 @@ describe('normalizeRoutes', () => {
},
{ handle: 'filesystem' },
{ src: '^/(?<slug>[^/]+)$', dest: 'blog?slug=$slug' },
{ handle: 'hit' },
{
src: '^/hit-me$',
headers: { 'Cache-Control': 'max-age=20' },
continue: true,
},
{ handle: 'miss' },
{ src: '^/missed-me$', dest: '/api/missed-me', check: true },
{
src: '^/missed-me$',
headers: { 'Cache-Control': 'max-age=10' },
continue: true,
},
];
assertValid(routes);
@@ -442,6 +455,82 @@ describe('normalizeRoutes', () => {
]
);
});
test('fails if routes after `handle: hit` use `dest`', () => {
const input = [
{
handle: 'hit',
},
{
src: '^/user$',
dest: '^/api/user$',
},
];
const { error } = normalizeRoutes(input);
assert.deepEqual(error.code, 'invalid_routes');
assert.deepEqual(
error.errors[0].message,
'You cannot assign "dest" after "handle: hit"'
);
});
test('fails if routes after `handle: hit` do not use `continue: true`', () => {
const input = [
{
handle: 'hit',
},
{
src: '^/user$',
headers: { 'Cache-Control': 'no-cache' },
},
];
const { error } = normalizeRoutes(input);
assert.deepEqual(error.code, 'invalid_routes');
assert.deepEqual(
error.errors[0].message,
'You must assign "continue: true" after "handle: hit"'
);
});
test('fails if routes after `handle: miss` do not use `check: true`', () => {
const input = [
{
handle: 'miss',
},
{
src: '^/user$',
dest: '^/api/user$',
},
];
const { error } = normalizeRoutes(input);
assert.deepEqual(error.code, 'invalid_routes');
assert.deepEqual(
error.errors[0].message,
'You must assign "check: true" after "handle: miss"'
);
});
test('fails if routes after `handle: miss` do not use `continue: true`', () => {
const input = [
{
handle: 'miss',
},
{
src: '^/user$',
headers: { 'Cache-Control': 'no-cache' },
},
];
const { error } = normalizeRoutes(input);
assert.deepEqual(error.code, 'invalid_routes');
assert.deepEqual(
error.errors[0].message,
'You must assign "continue: true" after "handle: miss"'
);
});
});
describe('getTransformedRoutes', () => {

View File

@@ -1,7 +1,7 @@
{
"name": "@now/ruby",
"author": "Nathan Cahill <nathan@nathancahill.com>",
"version": "1.0.1-canary.1",
"version": "1.0.1",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://zeit.co/docs/runtimes#official-runtimes/ruby",

View File

@@ -1,6 +1,6 @@
{
"name": "@now/static-build",
"version": "0.13.2-canary.0",
"version": "0.14.1-canary.0",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://zeit.co/docs/runtimes#official-runtimes/static-builds",
@@ -22,7 +22,6 @@
"@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",
"ms": "2.1.2",

View File

@@ -1,6 +1,5 @@
import ms from 'ms';
import path from 'path';
import spawn from 'cross-spawn';
import getPort from 'get-port';
import isPortReachable from 'is-port-reachable';
import { ChildProcess, SpawnOptions } from 'child_process';
@@ -9,7 +8,10 @@ import { frameworks, Framework } from './frameworks';
import {
glob,
download,
execAsync,
spawnAsync,
execCommand,
spawnCommand,
runNpmInstall,
runBundleInstall,
runPipInstall,
@@ -160,7 +162,33 @@ function getPkg(entrypoint: string, workPath: string) {
return pkg;
}
function getFramework(pkg: PackageJson) {
function getFramework(config: Config | null, pkg?: PackageJson | null) {
const { framework: configFramework = null, outputDirectory = null } =
config || {};
if (configFramework && configFramework.slug) {
const framework = frameworks.find(
({ dependency }) => dependency === configFramework.slug
);
if (framework) {
if (!framework.getOutputDirName && outputDirectory) {
return {
...framework,
getOutputDirName(prefix: string) {
return Promise.resolve(path.join(prefix, outputDirectory));
},
};
}
return framework;
}
}
if (!pkg) {
return;
}
const dependencies = Object.assign({}, pkg.dependencies, pkg.devDependencies);
const framework = frameworks.find(
({ dependency }) => dependencies[dependency || '']
@@ -183,12 +211,14 @@ export async function build({
let distPath = path.join(
workPath,
path.dirname(entrypoint),
(config && (config.distDir as string)) || 'dist'
(config && (config.distDir as string)) ||
(config.outputDirectory as string) ||
'dist'
);
const pkg = getPkg(entrypoint, workPath);
if (pkg) {
if (pkg || config.buildCommand) {
const gemfilePath = path.join(workPath, 'Gemfile');
const requirementsPath = path.join(workPath, 'requirements.txt');
@@ -197,7 +227,7 @@ export async function build({
let minNodeRange: string | undefined = undefined;
const routes: Route[] = [];
const devScript = getCommand(pkg, 'dev', config as Config);
const devScript = pkg ? getCommand(pkg, 'dev', config) : null;
if (config.zeroConfig) {
if (existsSync(gemfilePath) && !meta.isDev) {
@@ -244,9 +274,13 @@ export async function build({
}
// `public` is the default for zero config
distPath = path.join(workPath, path.dirname(entrypoint), 'public');
distPath = path.join(
workPath,
path.dirname(entrypoint),
(config.outputDirectory as string) || 'public'
);
framework = getFramework(pkg);
framework = getFramework(config, pkg);
}
if (framework) {
@@ -276,11 +310,37 @@ export async function build({
console.log('Installing dependencies...');
await runNpmInstall(entrypointDir, ['--prefer-offline'], spawnOpts, meta);
if (meta.isDev && pkg.scripts && pkg.scripts[devScript]) {
if (pkg && (config.buildCommand || config.devCommand)) {
// We want to add `node_modules/.bin` after `npm install`
const { stdout } = await execAsync('yarn', ['bin'], {
cwd: entrypointDir,
});
spawnOpts.env = {
...spawnOpts.env,
PATH: `${stdout.trim()}${path.delimiter}${
spawnOpts.env ? spawnOpts.env.PATH : ''
}`,
};
debug(
`Added "${stdout.trim()}" to PATH env because a package.json file was found.`
);
}
if (
meta.isDev &&
(config.devCommand ||
(pkg && devScript && pkg.scripts && pkg.scripts[devScript]))
) {
let devPort: number | undefined = nowDevScriptPorts.get(entrypoint);
if (typeof devPort === 'number') {
debug('`%s` server already running for %j', devScript, entrypoint);
debug(
'`%s` server already running for %j',
config.devCommand || devScript,
entrypoint
);
} else {
// Run the `now-dev` or `dev` script out-of-bounds, since it is assumed that
// it will launch a dev server that never "completes"
@@ -290,10 +350,12 @@ export async function build({
const opts: SpawnOptions = {
cwd: entrypointDir,
stdio: 'inherit',
env: { ...process.env, PORT: String(devPort) },
env: { ...spawnOpts.env, PORT: String(devPort) },
};
const child: ChildProcess = spawn('yarn', ['run', devScript], opts);
const cmd = config.devCommand || `yarn run ${devScript}`;
const child: ChildProcess = spawnCommand(cmd, opts);
child.on('exit', () => nowDevScriptPorts.delete(entrypoint));
nowDevChildProcesses.add(child);
@@ -328,24 +390,30 @@ export async function build({
);
} else {
if (meta.isDev) {
debug(`WARN: "${devScript}" script is missing from package.json`);
debug(`WARN: A dev script is missing.`);
debug(
'See the local development docs: https://zeit.co/docs/v2/deployments/official-builders/static-build-now-static-build/#local-development'
);
}
const buildScript = getCommand(pkg, 'build', config as Config);
debug(`Running "${buildScript}" script in "${entrypoint}"`);
const found = await runPackageJsonScript(
entrypointDir,
buildScript,
spawnOpts
const buildScript = pkg ? getCommand(pkg, 'build', config) : null;
debug(
`Running "${config.buildCommand ||
buildScript}" script in "${entrypoint}"`
);
const found =
typeof config.buildCommand === 'string'
? await execCommand(config.buildCommand, {
...spawnOpts,
cwd: entrypointDir,
})
: await runPackageJsonScript(entrypointDir, buildScript!, spawnOpts);
if (!found) {
throw new Error(
`Missing required "${buildScript}" script in "${entrypoint}"`
`Missing required "${config.buildCommand ||
buildScript}" script in "${entrypoint}"`
);
}
@@ -414,6 +482,7 @@ export async function build({
export async function prepareCache({
entrypoint,
workPath,
config,
}: PrepareCacheOptions): Promise<Files> {
// default cache paths
const defaultCacheFiles = await glob('node_modules/**', workPath);
@@ -423,7 +492,7 @@ export async function prepareCache({
const pkg = getPkg(entrypoint, workPath);
if (pkg) {
const framework = getFramework(pkg);
const framework = getFramework(config, pkg);
if (framework && framework.cachePattern) {
frameworkCacheFiles = await glob(framework.cachePattern, workPath);

View File

@@ -0,0 +1 @@
public

View File

@@ -0,0 +1,23 @@
const path = require('path');
const { promises: fs } = require('fs');
async function main() {
console.log('Starting to build...');
await fs.mkdir(path.join(__dirname, 'public'));
await fs.writeFile(
path.join(__dirname, 'public', 'index.txt'),
`Time of Creation: ${Date.now()}`
);
console.log('Finished building...');
}
main()
.then(() => {
process.exit(0);
})
.catch(error => {
console.error(error);
process.exit(1);
});

View File

@@ -0,0 +1,13 @@
{
"builds": [
{
"src": "build.js",
"use": "@now/static-build",
"config": {
"zeroConfig": true,
"buildCommand": "node build.js"
}
}
],
"probes": [{ "path": "/", "mustContain": "Time of Creation:" }]
}

View File

@@ -0,0 +1 @@
custom-output

View File

@@ -0,0 +1,23 @@
const path = require('path');
const { promises: fs } = require('fs');
async function main() {
console.log('Starting to build...');
await fs.mkdir(path.join(__dirname, 'custom-output'));
await fs.writeFile(
path.join(__dirname, 'custom-output', 'index.txt'),
`Time of Creation: ${Date.now()}`
);
console.log('Finished building...');
}
main()
.then(() => {
process.exit(0);
})
.catch(error => {
console.error(error);
process.exit(1);
});

View File

@@ -0,0 +1,30 @@
const http = require('http');
async function main() {
console.log('Starting to build...');
const server = http.createServer((req, res) => {
console.log(`> Request ${req.url}`);
res.end(`Time of Creation: ${Date.now()}`);
});
const port = process.env.PORT || 3000;
server.listen(port, () => {
console.log(`Started server on port ${port}`);
});
await new Promise((resolve, reject) => {
server.on('exit', () => resolve());
server.on('error', error => reject(error));
});
}
main()
.then(() => {
process.exit(0);
})
.catch(error => {
console.error(error);
process.exit(1);
});

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