Compare commits

..

43 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
Steven
bdd25ac727 Publish Canary
- @now/build-utils@1.1.2-canary.0
 - @now/cgi@1.0.1-canary.1
 - now@16.6.4-canary.0
 - @now/go@1.0.1-canary.1
 - @now/next@2.1.2-canary.0
 - @now/node@1.2.2-canary.0
 - @now/python@1.0.1-canary.1
 - @now/ruby@1.0.1-canary.1
 - @now/static-build@0.13.2-canary.0
2019-12-11 17:30:11 -05:00
Steven
3a27328828 [now-build-utils] Discontinue Node 8 (#3406)
This PR adds a `discontinueDate` to Node 8 and prints a warning if the current deployment is using it.

```
    ┌──────────────────────────────────────────────────────────────────────────────────────────────┐
    │                                                                                              │
    │   WARNING                                                                                    │
    │                                                                                              │
    │   Node.js 8.10.x will be discontinued on 2020-01-06.                                         │
    │   Deployments created on or after 2020-01-06 will fail to build.                             │
    │   Please use one of the following supported `engines` in `package.json`: ["12.x","10.x"]     │
    │   This change is a result of a decision made by an upstream infrastructure provider (AWS).   │
    │   Read more: https://docs.aws.amazon.com/lambda/latest/dg/runtime-support-policy.html        │
    │                                                                                              │
    └──────────────────────────────────────────────────────────────────────────────────────────────┘
```

Starting January 2020, Node 8 deployments will fail to build and print an error.

```
Found `engines` in `package.json` with an unsupported Node.js version range: 8.10.x
Please use one of the following supported ranges: ["12.x","10.x"]
```

[PRODUCT-796]

[PRODUCT-796]: https://zeit.atlassian.net/browse/PRODUCT-796
2019-12-11 22:27:16 +00:00
Nathan Rajlich
c076a5620f [all] Move "Downloading deployment source files" message to download() (#3413)
Before, the debug log message "Downloading user files..." was copy+pasted to all the builders.

This change centralizes that log message to be inside the `download()` function for consistency and DRY purposes.

Additionally, the wording has changed as per [INFRA-289], and a resulting timestamp message is also printed.

[INFRA-289]: https://zeit.atlassian.net/browse/INFRA-289
2019-12-11 20:59:14 +00:00
Nathan Rajlich
2bd8ef9eed [now-next] Do not pass runtime env vars via argv in now dev (#3340)
Before this change, in `@now/next` when running via `now dev`,
the runtime env vars get passed to the child `dev-server.js`
process via argv.

This is problematic because it causes the env vars to be visible in
the process listing, and also causes the command itself to be very
large.

In some cases, with a lot of env vars, we've seen the command be too
large and it will fail to spawn (https://serverfault.com/a/163390/294389).

This changes the start-up process such that the env vars are passed
in via Node.js `fork()` IPC, rather than via `argv`.
2019-12-11 19:18:08 +00:00
JJ Kasper
500014f2fc [now-next] Handle symlinks in createPseudoLayer (#3404)
This makes sure to handle symlinks created in mono repos while creating the zip for a lambda based page in Next.js

Fixes #3400
2019-12-11 15:43:14 +00:00
JJ Kasper
17687e9bcd Fix unused pre-commit package overriding husky (#3405)
Saw my `pre-commit` hook wasn't being triggered after running `yarn` and noticed it was due to an un-used `pre-commit` dependency in `now-cli` overriding the changes to my `.git/hooks/pre-commit` file done by `husky`

**Note**: you will probably need to run `yarn install --force` after this is applied before the correct `pre-commit` changes are applied by `husky`
2019-12-11 14:35:30 +00:00
Andy
90354e9fe7 [now-cgi][now-next][now-ruby] Unify logging about downloading user files (#3397)
* [now-cgi][now-next][now-ruby] Unify logging about downloading user files

* Update next tests

* Bump Next.js Version
2019-12-10 21:15:32 +01:00
Leo Lamprecht
6236631beb [now-dev] Improved feedback link (#3399)
We've shut down our Typeform for feedback, so we can simply the feedback link. The old one will continue working, but we should start using the new one.

Pending on https://github.com/zeit/front/pull/5874.

Fixes #3377
2019-12-10 18:18:10 +00:00
137 changed files with 13663 additions and 793 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.1",
"version": "1.2.1-canary.0",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.js",
@@ -32,9 +32,9 @@
"aggregate-error": "3.0.1",
"async-retry": "1.2.3",
"async-sema": "2.1.4",
"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

@@ -1,4 +1,5 @@
import path from 'path';
import debug from '../debug';
import FileFsRef from '../file-fs-ref';
import { File, Files, Meta } from '../types';
import { remove, mkdirp, readlink, symlink } from 'fs-extra';
@@ -39,8 +40,12 @@ export default async function download(
basePath: string,
meta?: Meta
): Promise<DownloadedFiles> {
const { isDev = false, skipDownload = false, filesChanged = null, filesRemoved = null } =
meta || {};
const {
isDev = false,
skipDownload = false,
filesChanged = null,
filesRemoved = null,
} = meta || {};
if (isDev || skipDownload) {
// In `now dev`, the `download()` function is a no-op because
@@ -48,11 +53,14 @@ export default async function download(
// source files are already available.
return files as DownloadedFiles;
}
debug('Downloading deployment source files...');
const start = Date.now();
const files2: DownloadedFiles = {};
const filenames = Object.keys(files);
await Promise.all(
Object.keys(files).map(async name => {
filenames.map(async name => {
// If the file does not exist anymore, remove it.
if (Array.isArray(filesRemoved) && filesRemoved.includes(name)) {
await removeFile(basePath, name);
@@ -71,5 +79,8 @@ export default async function download(
})
);
const duration = Date.now() - start;
debug(`Downloaded ${filenames.length} source files: ${duration}ms`);
return files2;
}

View File

@@ -1,13 +1,21 @@
import { intersects } from 'semver';
import boxen from 'boxen';
import { NodeVersion } from '../types';
import debug from '../debug';
const supportedOptions: NodeVersion[] = [
const allOptions: NodeVersion[] = [
{ major: 12, range: '12.x', runtime: 'nodejs12.x' },
{ major: 10, range: '10.x', runtime: 'nodejs10.x' },
{ major: 8, range: '8.10.x', runtime: 'nodejs8.10' },
{
major: 8,
range: '8.10.x',
runtime: 'nodejs8.10',
discontinueDate: new Date('2020-01-06'),
},
];
const supportedOptions = allOptions.filter(o => !isDiscontinued(o));
// This version should match Fargate's default in the PATH
// Today that is Node 8
export const defaultSelection = supportedOptions.find(
@@ -28,13 +36,14 @@ export async function getSupportedNodeVersion(
);
}
} else {
const found = supportedOptions.some(o => {
const found = allOptions.some(o => {
// the array is already in order so return the first
// match which will be the newest version of node
selection = o;
return intersects(o.range, engineRange);
});
if (found) {
const discontinued = isDiscontinued(selection);
if (found && !discontinued) {
if (!silent) {
debug(
'Found `engines` in `package.json`, selecting range: ' +
@@ -42,15 +51,50 @@ export async function getSupportedNodeVersion(
);
}
} else {
if (!silent) {
throw new Error(
'Found `engines` in `package.json` with an unsupported node range: ' +
engineRange +
'\nPlease use one of the following supported ranges: ' +
JSON.stringify(supportedOptions.map(o => o.range))
);
}
throw new Error(
'Found `engines` in `package.json` with an unsupported Node.js version range: ' +
engineRange +
'\nPlease use one of the following supported ranges: ' +
JSON.stringify(supportedOptions.map(o => o.range)) +
(discontinued
? '\nThis change is the result of a decision made by an upstream infrastructure provider (AWS).' +
'\nRead more: https://docs.aws.amazon.com/lambda/latest/dg/runtime-support-policy.html'
: '')
);
}
}
const { range, discontinueDate } = selection;
if (discontinueDate && !isDiscontinued(selection)) {
const d = discontinueDate.toISOString().split('T')[0];
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(
'NOTICE' +
'\n' +
`\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).' +
'\nRead more: https://docs.aws.amazon.com/lambda/latest/dg/runtime-support-policy.html',
{ padding: 1 }
)
);
process.env.TERM = prevTerm;
}
return selection;
}
function isDiscontinued({ discontinueDate }: NodeVersion): boolean {
const today = new Date();
return discontinueDate !== undefined && discontinueDate <= today;
}

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

@@ -3,12 +3,15 @@ import FileFsRef from './file-fs-ref';
import FileRef from './file-ref';
import { Lambda, createLambda, getLambdaOptionsFromFunction } from './lambda';
import { Prerender } from './prerender';
import download, { DownloadedFiles } from './fs/download';
import download, { DownloadedFiles, isSymbolicLink } from './fs/download';
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,
@@ -52,8 +58,13 @@ export {
detectBuilders,
detectRoutes,
debug,
isSymbolicLink,
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 {
@@ -304,6 +311,7 @@ export interface NodeVersion {
major: number;
range: string;
runtime: string;
discontinueDate?: Date;
}
export interface Builder {
@@ -353,7 +361,7 @@ export interface DetectorParameters {
export interface DetectorOutput {
buildCommand: string;
buildDirectory: string;
outputDirectory: string;
buildVariables?: Env;
devCommand?: string;
devVariables?: Env;
@@ -365,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

@@ -3,6 +3,7 @@ const { mkdirp, copyFile } = require('fs-extra');
const {
glob,
debug,
download,
shouldServe,
createLambda,
@@ -14,7 +15,6 @@ exports.analyze = ({ files, entrypoint }) => files[entrypoint].digest;
exports.version = 3;
exports.build = async ({ workPath, files, entrypoint, meta, config }) => {
console.log('downloading files...');
const outDir = await getWritableDirectory();
await download(files, workPath, meta);

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "now",
"version": "16.6.3",
"version": "16.7.1-canary.0",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Now",
@@ -146,7 +146,6 @@
"ora": "3.4.0",
"pcre-to-regexp": "1.0.0",
"pluralize": "7.0.0",
"pre-commit": "1.2.2",
"printf": "0.2.5",
"progress": "2.0.3",
"promisepipe": "3.0.0",

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

@@ -18,7 +18,7 @@ export default async function dev(
output: Output
) {
output.dim(
`Now CLI ${pkg.version} dev (beta) — https://zeit.co/feedback/dev`
`Now CLI ${pkg.version} dev (beta) — https://zeit.co/feedback`
);
const [dir = '.'] = args;

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

@@ -466,10 +466,10 @@ CMD ["node", "index.js"]`,
autoAlias: true,
autoJobCancelation: true,
enabled: true,
silent: true
}
})
}
silent: true,
},
}),
},
};
for (const typeName of Object.keys(spec)) {

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.'
)
);
@@ -2040,10 +2081,10 @@ test('fail to deploy a Lambda with a specific runtime but without a locked versi
});
test('ensure `github` and `scope` are not sent to the API', async t => {
const directory = fixture('github-and-scope-config');
const output = await execute([directory]);
const directory = fixture('github-and-scope-config');
const output = await execute([directory]);
t.is(output.exitCode, 0, formatOutput(output));
t.is(output.exitCode, 0, formatOutput(output));
});
test.after.always(async () => {

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

@@ -70,7 +70,6 @@ Learn more: https://github.com/golang/go/wiki/Modules
`);
}
debug('Downloading user files...');
const entrypointArr = entrypoint.split(sep);
// eslint-disable-next-line prefer-const

View File

@@ -1,6 +1,6 @@
{
"name": "@now/go",
"version": "1.0.1-canary.0",
"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.1",
"version": "2.3.0",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://zeit.co/docs/runtimes#official-runtimes/next-js",

View File

@@ -10,25 +10,15 @@ process.on('unhandledRejection', err => {
process.exit(1);
});
async function main(cwd: string) {
const next = require(resolveFrom(cwd, 'next'));
const app = next({ dev: true, dir: cwd });
process.once('message', async ({ dir, runtimeEnv }) => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const next = require(resolveFrom(dir, 'next'));
const app = next({ dev: true, dir });
const handler = app.getRequestHandler();
const openPort = await getPort({
port: [5000, 4000],
});
const [openPort] = await Promise.all([getPort(), app.prepare()]);
const url = `http://localhost:${openPort}`;
// Prepare for incoming requests
await app.prepare();
// The runtime env vars are passed in to `argv[2]`
// as a base64-encoded JSON string
const runtimeEnv = JSON.parse(
Buffer.from(process.argv[2], 'base64').toString()
);
syncEnvVars(process.env, process.env, runtimeEnv);
createServer((req, res) => {
@@ -39,6 +29,4 @@ async function main(cwd: string) {
process.send(url);
}
});
}
main(process.cwd());
});

View File

@@ -1,15 +1,3 @@
import { ChildProcess, fork } from 'child_process';
import {
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 {
BuildOptions,
Config,
@@ -19,6 +7,7 @@ import {
FileBlob,
FileFsRef,
Files,
getLambdaOptionsFromFunction,
getNodeVersion,
getSpawnOptions,
glob,
@@ -29,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 {
@@ -42,10 +45,14 @@ import {
excludeFiles,
ExperimentalTraceVersion,
getDynamicRoutes,
getExportIntent,
getExportStatus,
getNextConfig,
getPathsInside,
getPrerenderManifest,
getRoutes,
getRoutesManifest,
getSourceFilePathFromPage,
isDynamicRoute,
normalizePackageJson,
normalizePage,
@@ -53,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;
@@ -162,24 +162,20 @@ const name = '[@now/next]';
const urls: stringMap = {};
function startDevServer(entryPath: string, runtimeEnv: EnvConfig) {
// The runtime env vars are encoded and passed in as `argv[2]`, so that the
// dev-server process can replace them onto `process.env` after the Next.js
// "prepare" step
const encodedEnv = Buffer.from(JSON.stringify(runtimeEnv)).toString('base64');
// `env` is omitted since that
// makes it default to `process.env`
const forked = fork(path.join(__dirname, 'dev-server.js'), [encodedEnv], {
// `env` is omitted since that makes it default to `process.env`
const forked = fork(path.join(__dirname, 'dev-server.js'), [], {
cwd: entryPath,
execArgv: [],
});
const getUrl = () =>
new Promise<string>((resolve, reject) => {
forked.on('message', resolve);
forked.on('error', reject);
forked.once('message', resolve);
forked.once('error', reject);
});
forked.send({ dir: entryPath, runtimeEnv });
return { forked, getUrl };
}
@@ -201,7 +197,6 @@ export const build = async ({
const entryPath = path.join(workPath, entryDirectory);
const dotNextStatic = path.join(entryPath, '.next/static');
debug(`${name} Downloading user files...`);
await download(files, workPath, meta);
const pkg = await readPackageJson(entryPath);
@@ -338,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(
@@ -583,9 +694,11 @@ export const build = async ({
// Initial files are manually added to the lambda later
return;
}
const { mode } = lstatSync(path.join(workPath, file));
files[file] = new FileFsRef({
fsPath: path.join(workPath, file),
mode,
});
};
@@ -821,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,
@@ -836,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

@@ -12,6 +12,7 @@ import {
streamToBuffer,
Lambda,
Route,
isSymbolicLink,
} from '@now/build-utils';
type stringMap = { [key: string]: string };
@@ -340,6 +341,7 @@ export async function getRoutesManifest(
);
}
// eslint-disable-next-line @typescript-eslint/no-var-requires
const routesManifest: RoutesManifest = require(pathRoutesManifest);
return routesManifest;
@@ -454,11 +456,20 @@ function syncEnvVars(base: EnvConfig, removeEnv: EnvConfig, addEnv: EnvConfig) {
export const ExperimentalTraceVersion = `9.0.4-canary.1`;
export type PseudoLayer = {
[fileName: string]: {
crc32: number;
compBuffer: Buffer;
uncompressedSize: number;
};
[fileName: string]: PseudoFile | PseudoSymbolicLink;
};
export type PseudoFile = {
isSymlink: false;
crc32: number;
compBuffer: Buffer;
uncompressedSize: number;
};
export type PseudoSymbolicLink = {
isSymlink: true;
file: FileFsRef;
symlinkTarget: string;
};
const compressBuffer = (buf: Buffer): Promise<Buffer> => {
@@ -481,13 +492,22 @@ export async function createPseudoLayer(files: {
for (const fileName of Object.keys(files)) {
const file = files[fileName];
const origBuffer = await streamToBuffer(file.toStream());
const compBuffer = await compressBuffer(origBuffer);
pseudoLayer[fileName] = {
compBuffer,
crc32: crc32.unsigned(origBuffer),
uncompressedSize: origBuffer.byteLength,
};
if (isSymbolicLink(file.mode)) {
pseudoLayer[fileName] = {
file,
isSymlink: true,
symlinkTarget: await fs.readlink(file.fsPath),
} as PseudoSymbolicLink;
} else {
const origBuffer = await streamToBuffer(file.toStream());
const compBuffer = await compressBuffer(origBuffer);
pseudoLayer[fileName] = {
compBuffer,
crc32: crc32.unsigned(origBuffer),
uncompressedSize: origBuffer.byteLength,
} as PseudoFile;
}
}
return pseudoLayer;
@@ -520,10 +540,31 @@ export async function createLambdaFromPseudoLayers({
const zipFile = new ZipFile();
const addedFiles = new Set();
const names = Object.keys(files).sort();
const symlinkTargets = new Map<string, string>();
for (const name of names) {
const file = files[name];
if (file.mode && isSymbolicLink(file.mode) && file.type === 'FileFsRef') {
const symlinkTarget = await fs.readlink((file as FileFsRef).fsPath);
symlinkTargets.set(name, symlinkTarget);
}
}
// apply pseudo layers (already compressed objects)
for (const layer of layers) {
for (const seedKey of Object.keys(layer)) {
const { compBuffer, crc32, uncompressedSize } = layer[seedKey];
const item = layer[seedKey];
if (item.isSymlink) {
const { symlinkTarget, file } = item;
zipFile.addBuffer(Buffer.from(symlinkTarget, 'utf8'), seedKey, {
mode: file.mode,
});
continue;
}
const { compBuffer, crc32, uncompressedSize } = item;
// @ts-ignore: `addDeflatedBuffer` is a valid function, but missing on the type
zipFile.addDeflatedBuffer(compBuffer, seedKey, {
@@ -539,8 +580,16 @@ export async function createLambdaFromPseudoLayers({
// was already added in a pseudo layer
if (addedFiles.has(fileName)) continue;
const file = files[fileName];
const fileBuffer = await streamToBuffer(file.toStream());
zipFile.addBuffer(fileBuffer, fileName);
const symlinkTarget = symlinkTargets.get(fileName);
if (typeof symlinkTarget === 'string') {
zipFile.addBuffer(Buffer.from(symlinkTarget, 'utf8'), fileName, {
mode: file.mode,
});
} else {
const fileBuffer = await streamToBuffer(file.toStream());
zipFile.addBuffer(fileBuffer, fileName);
}
}
zipFile.end();
@@ -575,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

@@ -1,6 +1,6 @@
{
"dependencies": {
"next": "^9.1.2-canary.8",
"next": "^9.1.6-canary.1",
"react": "^16.8.6",
"react-dom": "^16.8.6"
}

View File

@@ -1,16 +1,16 @@
import React from 'react';
// eslint-disable-next-line camelcase
export async function unstable_getStaticParams() {
export async function unstable_getStaticPaths () {
return [
'/blog/post-1/comment-1',
{ post: 'post-2', comment: 'comment-2' },
{ params: { post: 'post-2', comment: 'comment-2' } },
'/blog/post-1337/comment-1337',
];
}
// eslint-disable-next-line camelcase
export async function unstable_getStaticProps({ params }) {
export async function unstable_getStaticProps ({ params }) {
return {
props: {
post: params.post,

View File

@@ -1,13 +1,14 @@
import React from 'react'
// eslint-disable-next-line camelcase
export async function unstable_getStaticParams () {
export async function unstable_getStaticPaths () {
return [
'/blog/post-1',
{ post: 'post-2' },
{ params: { post: 'post-2' } },
]
}
// eslint-disable-next-line camelcase
export async function unstable_getStaticProps ({ params }) {
if (params.post === 'post-10') {

View File

@@ -0,0 +1,6 @@
node_modules
.next
.env
tsconfig.tsbuildinfo
.DS_Store
*.log

View File

@@ -0,0 +1,5 @@
{
"packages": ["packages/*"],
"npmClient": "yarn",
"version": "0.0.0"
}

View File

@@ -0,0 +1,11 @@
{
"version": 2,
"builds": [{ "src": "packages/web/next.config.js", "use": "@now/next" }],
"routes": [{ "src": "/(.*)", "dest": "/packages/web/$1", "continue": true }],
"probes": [
{
"path": "/",
"mustContain": "hello world <!-- -->6"
}
]
}

View File

@@ -0,0 +1,14 @@
{
"private": true,
"workspaces": {
"packages": [
"packages/*"
]
},
"scripts": {
"postinstall": "lerna run build --scope=@jimmy/common"
},
"devDependencies": {
"lerna": "^3.19.0"
}
}

View File

@@ -0,0 +1 @@
export declare const add: (a: number, b: number) => number;

View File

@@ -0,0 +1,6 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
exports.add = (a, b) => {
return a + b;
};
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;AAAa,QAAA,GAAG,GAAG,CAAC,CAAS,EAAE,CAAS,EAAE,EAAE;IAC1C,OAAO,CAAC,GAAG,CAAC,CAAC;AACf,CAAC,CAAC"}

View File

@@ -0,0 +1,14 @@
{
"name": "@jimmy/common",
"version": "1.0.64",
"types": "dist/index.d.ts",
"main": "dist/index.js",
"license": "MIT",
"scripts": {
"watch": "tsc -w",
"build": "tsc"
},
"devDependencies": {
"typescript": "^3.7.3"
}
}

View File

@@ -0,0 +1,3 @@
export const add = (a: number, b: number) => {
return a + b;
};

View File

@@ -0,0 +1,32 @@
{
"compilerOptions": {
"sourceMap": true,
"removeComments": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"composite": true,
"rootDir": "src",
"outDir": "dist",
"target": "es6",
"module": "commonjs",
"lib": ["dom", "es2017", "esnext.asynciterable", "es2017.object"],
"declaration": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.json"],
"exclude": ["dist"]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
/// <reference types="next" />
/// <reference types="next/types/global" />

View File

@@ -0,0 +1 @@
module.exports = {};

View File

@@ -0,0 +1,24 @@
{
"name": "@jimmy/web",
"version": "1.0.67",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@jimmy/common": "^1.0.64",
"next": "^9.1.4",
"react": "^16.12.0",
"react-dom": "^16.12.0"
},
"devDependencies": {
"@babel/plugin-syntax-class-properties": "^7.7.4",
"@types/next": "^9.0.0",
"@types/node": "^12.12.14",
"@types/react": "^16.9.15",
"@types/react-dom": "16.9.4",
"typescript": "3.7.3"
},
"license": "ISC"
}

View File

@@ -0,0 +1,58 @@
import App from "next/app";
import React from "react";
class MyApp extends App<any> {
static async getInitialProps() {
console.log("i am props");
return { q: 5, pageProps: {} };
}
render() {
const { Component, pageProps } = this.props;
return (
<>
<div>yo</div>
<Component {...pageProps} />
<style jsx global>
{`
html {
min-height: 100%;
display: flex;
flex-direction: column;
position: relative;
}
h1 {
font-size: 40px;
}
body,
body > div {
flex: 1;
display: flex;
flex-direction: column;
}
div,
input,
form,
li,
ul {
box-sizing: border-box;
}
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
`}
</style>
</>
);
}
}
export default MyApp;

View File

@@ -0,0 +1,5 @@
import { add } from "@jimmy/common";
export default () => {
return <div>hello world {add(1, 5)}</div>;
};

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"jsx": "preserve",
"lib": ["dom", "es2017"],
"baseUrl": ".",
"moduleResolution": "node",
"strict": true,
"allowJs": true,
"noEmit": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"isolatedModules": true,
"removeComments": false,
"preserveConstEnums": true,
"sourceMap": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"exclude": ["dist", ".next", "out", "next.config.js"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}

File diff suppressed because it is too large Load Diff

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"
}
}

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