mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-23 09:59:12 +00:00
Compare commits
11 Commits
@now/pytho
...
@now/stati
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42ce9aca86 | ||
|
|
156f596189 | ||
|
|
8acfd5bf71 | ||
|
|
76c99ebb28 | ||
|
|
5fb119b99c | ||
|
|
99b766d9cb | ||
|
|
c207cf9b40 | ||
|
|
dd00ac4621 | ||
|
|
3d18a067a0 | ||
|
|
c48571a799 | ||
|
|
6eeb6983d9 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@now/build-utils",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.1-canary.0",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.js",
|
||||
|
||||
432
packages/now-build-utils/src/detect-builders-legacy.ts
Normal file
432
packages/now-build-utils/src/detect-builders-legacy.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -80,7 +80,12 @@ function detectFrontBuilder(
|
||||
const { tag } = options;
|
||||
const withTag = tag ? `@${tag}` : '';
|
||||
|
||||
const { framework, buildCommand, outputDirectory } = detectorResult;
|
||||
const {
|
||||
framework,
|
||||
buildCommand,
|
||||
outputDirectory,
|
||||
devCommand,
|
||||
} = detectorResult;
|
||||
|
||||
const frameworkSlug = framework ? framework.slug : null;
|
||||
|
||||
@@ -88,6 +93,14 @@ function detectFrontBuilder(
|
||||
zeroConfig: true,
|
||||
};
|
||||
|
||||
if (framework) {
|
||||
config.framework = framework;
|
||||
}
|
||||
|
||||
if (devCommand) {
|
||||
config.devCommand = devCommand;
|
||||
}
|
||||
|
||||
if (buildCommand) {
|
||||
config.buildCommand = buildCommand;
|
||||
}
|
||||
|
||||
298
packages/now-build-utils/src/detect-routes-legacy.ts
Normal file
298
packages/now-build-utils/src/detect-routes-legacy.ts
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -8,7 +8,10 @@ import getWriteableDirectory from './fs/get-writable-directory';
|
||||
import glob from './fs/glob';
|
||||
import rename from './fs/rename';
|
||||
import {
|
||||
execAsync,
|
||||
spawnAsync,
|
||||
execCommand,
|
||||
spawnCommand,
|
||||
installDependencies,
|
||||
runPackageJsonScript,
|
||||
runNpmInstall,
|
||||
@@ -38,9 +41,12 @@ export {
|
||||
getWriteableDirectory,
|
||||
glob,
|
||||
rename,
|
||||
execAsync,
|
||||
spawnAsync,
|
||||
installDependencies,
|
||||
runPackageJsonScript,
|
||||
execCommand,
|
||||
spawnCommand,
|
||||
runNpmInstall,
|
||||
runBundleInstall,
|
||||
runPipInstall,
|
||||
@@ -56,6 +62,9 @@ export {
|
||||
getLambdaOptionsFromFunction,
|
||||
};
|
||||
|
||||
export { detectBuildersLegacy } from './detect-builders-legacy';
|
||||
export { detectRoutesLegacy } from './detect-routes-legacy';
|
||||
|
||||
export { detectDefaults } from './detectors';
|
||||
export * from './schemas';
|
||||
export * from './types';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
14
packages/now-build-utils/test/unit.test.js
vendored
14
packages/now-build-utils/test/unit.test.js
vendored
@@ -502,6 +502,10 @@ describe('Test `detectBuilders`', () => {
|
||||
config: {
|
||||
zeroConfig: true,
|
||||
buildCommand: 'yarn build',
|
||||
framework: {
|
||||
slug: 'next',
|
||||
version: '9.0.0',
|
||||
},
|
||||
functions: {
|
||||
'pages/api/teams/**': {
|
||||
memory: 128,
|
||||
@@ -569,6 +573,10 @@ describe('Test `detectBuilders`', () => {
|
||||
config: {
|
||||
zeroConfig: true,
|
||||
buildCommand: 'yarn build',
|
||||
framework: {
|
||||
slug: 'next',
|
||||
version: '9.0.0',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -910,8 +918,12 @@ describe('Test `detectBuilders`', () => {
|
||||
use: '@now/next',
|
||||
src: 'package.json',
|
||||
config: {
|
||||
buildCommand: 'yarn build',
|
||||
zeroConfig: true,
|
||||
buildCommand: 'yarn build',
|
||||
framework: {
|
||||
slug: 'next',
|
||||
version: '9.0.0',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "now",
|
||||
"version": "16.7.0",
|
||||
"version": "16.7.1-canary.0",
|
||||
"preferGlobal": true,
|
||||
"license": "Apache-2.0",
|
||||
"description": "The command-line interface for Now",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
2
packages/now-cli/test/integration.js
vendored
2
packages/now-cli/test/integration.js
vendored
@@ -915,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.'
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
202
packages/now-cli/test/unit.js
vendored
202
packages/now-cli/test/unit.js
vendored
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@now/next",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.1-canary.1",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://zeit.co/docs/runtimes#official-runtimes/next-js",
|
||||
|
||||
@@ -1,16 +1,3 @@
|
||||
import { ChildProcess, fork } from 'child_process';
|
||||
import {
|
||||
pathExists,
|
||||
readFile,
|
||||
unlink as unlinkFile,
|
||||
writeFile,
|
||||
lstatSync,
|
||||
} from 'fs-extra';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import semver from 'semver';
|
||||
|
||||
import {
|
||||
BuildOptions,
|
||||
Config,
|
||||
@@ -20,6 +7,7 @@ import {
|
||||
FileBlob,
|
||||
FileFsRef,
|
||||
Files,
|
||||
getLambdaOptionsFromFunction,
|
||||
getNodeVersion,
|
||||
getSpawnOptions,
|
||||
glob,
|
||||
@@ -30,10 +18,24 @@ import {
|
||||
Route,
|
||||
runNpmInstall,
|
||||
runPackageJsonScript,
|
||||
getLambdaOptionsFromFunction,
|
||||
} from '@now/build-utils';
|
||||
import {
|
||||
convertRedirects,
|
||||
convertRewrites,
|
||||
} from '@now/routing-utils/dist/superstatic';
|
||||
import nodeFileTrace, { NodeFileTraceReasons } from '@zeit/node-file-trace';
|
||||
|
||||
import { ChildProcess, fork } from 'child_process';
|
||||
import {
|
||||
lstatSync,
|
||||
pathExists,
|
||||
readFile,
|
||||
unlink as unlinkFile,
|
||||
writeFile,
|
||||
} from 'fs-extra';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import semver from 'semver';
|
||||
import createServerlessConfig from './create-serverless-config';
|
||||
import nextLegacyVersions from './legacy-versions';
|
||||
import {
|
||||
@@ -43,10 +45,14 @@ import {
|
||||
excludeFiles,
|
||||
ExperimentalTraceVersion,
|
||||
getDynamicRoutes,
|
||||
getExportIntent,
|
||||
getExportStatus,
|
||||
getNextConfig,
|
||||
getPathsInside,
|
||||
getPrerenderManifest,
|
||||
getRoutes,
|
||||
getRoutesManifest,
|
||||
getSourceFilePathFromPage,
|
||||
isDynamicRoute,
|
||||
normalizePackageJson,
|
||||
normalizePage,
|
||||
@@ -54,15 +60,8 @@ import {
|
||||
stringMap,
|
||||
syncEnvVars,
|
||||
validateEntrypoint,
|
||||
getSourceFilePathFromPage,
|
||||
getRoutesManifest,
|
||||
} from './utils';
|
||||
|
||||
import {
|
||||
convertRedirects,
|
||||
convertRewrites,
|
||||
} from '@now/routing-utils/dist/superstatic';
|
||||
|
||||
interface BuildParamsMeta {
|
||||
isDev: boolean | undefined;
|
||||
env?: EnvConfig;
|
||||
@@ -355,6 +354,101 @@ export const build = async ({
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
@@ -624,6 +624,73 @@ export type NextPrerenderedRoutes = {
|
||||
};
|
||||
};
|
||||
|
||||
export async function getExportIntent(
|
||||
entryPath: string
|
||||
): Promise<false | { trailingSlash: boolean }> {
|
||||
const pathExportMarker = path.join(entryPath, '.next', 'export-marker.json');
|
||||
const hasExportMarker: boolean = await fs
|
||||
.access(pathExportMarker, fs.constants.F_OK)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
if (!hasExportMarker) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const manifest: {
|
||||
version: 1;
|
||||
exportTrailingSlash: boolean;
|
||||
hasExportPathMap: boolean;
|
||||
} = JSON.parse(await fs.readFile(pathExportMarker, 'utf8'));
|
||||
|
||||
switch (manifest.version) {
|
||||
case 1: {
|
||||
if (manifest.hasExportPathMap !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return { trailingSlash: manifest.exportTrailingSlash };
|
||||
}
|
||||
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getExportStatus(
|
||||
entryPath: string
|
||||
): Promise<false | { success: boolean; outDirectory: string }> {
|
||||
const pathExportDetail = path.join(entryPath, '.next', 'export-detail.json');
|
||||
const hasExportDetail: boolean = await fs
|
||||
.access(pathExportDetail, fs.constants.F_OK)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
if (!hasExportDetail) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const manifest: {
|
||||
version: 1;
|
||||
success: boolean;
|
||||
outDirectory: string;
|
||||
} = JSON.parse(await fs.readFile(pathExportDetail, 'utf8'));
|
||||
|
||||
switch (manifest.version) {
|
||||
case 1: {
|
||||
return {
|
||||
success: !!manifest.success,
|
||||
outDirectory: manifest.outDirectory,
|
||||
};
|
||||
}
|
||||
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPrerenderManifest(
|
||||
entryPath: string
|
||||
): Promise<NextPrerenderedRoutes> {
|
||||
|
||||
5
packages/now-next/test/fixtures/10-export-cache-headers/next.config.js
vendored
Normal file
5
packages/now-next/test/fixtures/10-export-cache-headers/next.config.js
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
generateBuildId() {
|
||||
return 'testing-build-id';
|
||||
},
|
||||
};
|
||||
16
packages/now-next/test/fixtures/10-export-cache-headers/now.json
vendored
Normal file
16
packages/now-next/test/fixtures/10-export-cache-headers/now.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
packages/now-next/test/fixtures/10-export-cache-headers/package.json
vendored
Normal file
10
packages/now-next/test/fixtures/10-export-cache-headers/package.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"scripts": {
|
||||
"build": "next build && next export"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "canary",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
}
|
||||
}
|
||||
5
packages/now-next/test/fixtures/10-export-cache-headers/pages/index.js
vendored
Normal file
5
packages/now-next/test/fixtures/10-export-cache-headers/pages/index.js
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
const Page = () => 'Hi';
|
||||
|
||||
Page.getInitialProps = () => ({ hello: 'world' });
|
||||
|
||||
export default Page;
|
||||
5
packages/now-next/test/fixtures/11-export-clean-urls/next.config.js
vendored
Normal file
5
packages/now-next/test/fixtures/11-export-clean-urls/next.config.js
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
generateBuildId() {
|
||||
return 'testing-build-id';
|
||||
},
|
||||
};
|
||||
18
packages/now-next/test/fixtures/11-export-clean-urls/now.json
vendored
Normal file
18
packages/now-next/test/fixtures/11-export-clean-urls/now.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
packages/now-next/test/fixtures/11-export-clean-urls/package.json
vendored
Normal file
10
packages/now-next/test/fixtures/11-export-clean-urls/package.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"scripts": {
|
||||
"build": "next build && next export"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "canary",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
}
|
||||
}
|
||||
7
packages/now-next/test/fixtures/11-export-clean-urls/pages/about.js
vendored
Normal file
7
packages/now-next/test/fixtures/11-export-clean-urls/pages/about.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
function About() {
|
||||
return <div>Hi on About</div>;
|
||||
}
|
||||
|
||||
About.getInitialProps = () => ({});
|
||||
|
||||
export default About;
|
||||
7
packages/now-next/test/fixtures/11-export-clean-urls/pages/index.js
vendored
Normal file
7
packages/now-next/test/fixtures/11-export-clean-urls/pages/index.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
function Home() {
|
||||
return <div>Hi There</div>;
|
||||
}
|
||||
|
||||
Home.getInitialProps = () => ({});
|
||||
|
||||
export default Home;
|
||||
6
packages/now-next/test/fixtures/12-export-auto/next.config.js
vendored
Normal file
6
packages/now-next/test/fixtures/12-export-auto/next.config.js
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
generateBuildId() {
|
||||
return 'testing-build-id';
|
||||
},
|
||||
exportPathMap: d => d,
|
||||
};
|
||||
18
packages/now-next/test/fixtures/12-export-auto/now.json
vendored
Normal file
18
packages/now-next/test/fixtures/12-export-auto/now.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
7
packages/now-next/test/fixtures/12-export-auto/package.json
vendored
Normal file
7
packages/now-next/test/fixtures/12-export-auto/package.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"next": "canary",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
}
|
||||
}
|
||||
7
packages/now-next/test/fixtures/12-export-auto/pages/about.js
vendored
Normal file
7
packages/now-next/test/fixtures/12-export-auto/pages/about.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
function About() {
|
||||
return <div>Hi on About</div>;
|
||||
}
|
||||
|
||||
About.getInitialProps = () => ({});
|
||||
|
||||
export default About;
|
||||
7
packages/now-next/test/fixtures/12-export-auto/pages/index.js
vendored
Normal file
7
packages/now-next/test/fixtures/12-export-auto/pages/index.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
function Home() {
|
||||
return <div>Hi There</div>;
|
||||
}
|
||||
|
||||
Home.getInitialProps = () => ({});
|
||||
|
||||
export default Home;
|
||||
24
packages/now-next/test/fixtures/13-export-custom-routes/next.config.js
vendored
Normal file
24
packages/now-next/test/fixtures/13-export-custom-routes/next.config.js
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
module.exports = {
|
||||
generateBuildId() {
|
||||
return 'testing-build-id';
|
||||
},
|
||||
exportPathMap: d => d,
|
||||
experimental: {
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/first',
|
||||
destination: '/',
|
||||
},
|
||||
];
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: '/second',
|
||||
destination: '/about',
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
};
|
||||
18
packages/now-next/test/fixtures/13-export-custom-routes/now.json
vendored
Normal file
18
packages/now-next/test/fixtures/13-export-custom-routes/now.json
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [{ "src": "package.json", "use": "@now/next" }],
|
||||
"probes": [
|
||||
{
|
||||
"path": "/first",
|
||||
"mustContain": "Hi There"
|
||||
},
|
||||
{
|
||||
"path": "/second",
|
||||
"mustContain": "Hi on About"
|
||||
},
|
||||
{
|
||||
"path": "/",
|
||||
"mustContain": "nextExport\":true"
|
||||
}
|
||||
]
|
||||
}
|
||||
7
packages/now-next/test/fixtures/13-export-custom-routes/package.json
vendored
Normal file
7
packages/now-next/test/fixtures/13-export-custom-routes/package.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"next": "canary",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
}
|
||||
}
|
||||
7
packages/now-next/test/fixtures/13-export-custom-routes/pages/about.js
vendored
Normal file
7
packages/now-next/test/fixtures/13-export-custom-routes/pages/about.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
function About() {
|
||||
return <div>Hi on About</div>;
|
||||
}
|
||||
|
||||
About.getInitialProps = () => ({});
|
||||
|
||||
export default About;
|
||||
7
packages/now-next/test/fixtures/13-export-custom-routes/pages/index.js
vendored
Normal file
7
packages/now-next/test/fixtures/13-export-custom-routes/pages/index.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
function Home() {
|
||||
return <div>Hi There</div>;
|
||||
}
|
||||
|
||||
Home.getInitialProps = () => ({});
|
||||
|
||||
export default Home;
|
||||
2
packages/now-next/test/test.js
vendored
2
packages/now-next/test/test.js
vendored
@@ -3,7 +3,7 @@ const path = require('path');
|
||||
|
||||
const {
|
||||
packAndDeploy,
|
||||
testDeployment
|
||||
testDeployment,
|
||||
} = require('../../../test/lib/deployment/test-deployment.js');
|
||||
|
||||
jest.setTimeout(4 * 60 * 1000);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@now/routing-utils",
|
||||
"version": "1.4.0",
|
||||
"version": "1.4.1-canary.0",
|
||||
"description": "ZEIT Now routing utilities",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
|
||||
@@ -19,44 +19,53 @@ import {
|
||||
export { getCleanUrls } from './superstatic';
|
||||
export { mergeRoutes } from './merge';
|
||||
|
||||
const VALID_HANDLE_VALUES = ['filesystem', 'hit', 'miss'] as const;
|
||||
const validHandleValues = new Set<string>(VALID_HANDLE_VALUES);
|
||||
export type HandleValue = typeof VALID_HANDLE_VALUES[number];
|
||||
|
||||
export function isHandler(route: Route): route is Handler {
|
||||
return typeof (route as Handler).handle !== 'undefined';
|
||||
}
|
||||
|
||||
export function isValidHandleValue(handle: string): handle is HandleValue {
|
||||
return validHandleValues.has(handle);
|
||||
}
|
||||
|
||||
export function normalizeRoutes(inputRoutes: Route[] | null): NormalizedRoutes {
|
||||
if (!inputRoutes || inputRoutes.length === 0) {
|
||||
return { routes: inputRoutes, error: null };
|
||||
}
|
||||
|
||||
const routes: Route[] = [];
|
||||
const handling: string[] = [];
|
||||
const errors = [];
|
||||
const handling: HandleValue[] = [];
|
||||
const errors: NowErrorNested[] = [];
|
||||
|
||||
// We don't want to treat the input routes as references
|
||||
inputRoutes.forEach(r => routes.push(Object.assign({}, r)));
|
||||
|
||||
for (const route of routes) {
|
||||
if (isHandler(route)) {
|
||||
// typeof { handle: string }
|
||||
if (Object.keys(route).length !== 1) {
|
||||
errors.push({
|
||||
message: `Cannot have any other keys when handle is used (handle: ${route.handle})`,
|
||||
handle: route.handle,
|
||||
});
|
||||
}
|
||||
if (!['filesystem'].includes(route.handle)) {
|
||||
const { handle } = route;
|
||||
if (!isValidHandleValue(handle)) {
|
||||
errors.push({
|
||||
message: `This is not a valid handler (handle: ${route.handle})`,
|
||||
handle: route.handle,
|
||||
message: `This is not a valid handler (handle: ${handle})`,
|
||||
handle: handle,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (handling.includes(route.handle)) {
|
||||
if (handling.includes(handle)) {
|
||||
errors.push({
|
||||
message: `You can only handle something once (handle: ${route.handle})`,
|
||||
handle: route.handle,
|
||||
message: `You can only handle something once (handle: ${handle})`,
|
||||
handle: handle,
|
||||
});
|
||||
} else {
|
||||
handling.push(route.handle);
|
||||
handling.push(handle);
|
||||
}
|
||||
} else if (route.src) {
|
||||
// Route src should always start with a '^'
|
||||
@@ -76,6 +85,35 @@ export function normalizeRoutes(inputRoutes: Route[] | null): NormalizedRoutes {
|
||||
if (regError) {
|
||||
errors.push(regError);
|
||||
}
|
||||
|
||||
// The last seen handling is the current handler
|
||||
const handleValue = handling[handling.length - 1];
|
||||
if (handleValue === 'hit') {
|
||||
if (route.dest) {
|
||||
errors.push({
|
||||
message: `You cannot assign "dest" after "handle: hit"`,
|
||||
src: route.src,
|
||||
});
|
||||
}
|
||||
if (!route.continue) {
|
||||
errors.push({
|
||||
message: `You must assign "continue: true" after "handle: hit"`,
|
||||
src: route.src,
|
||||
});
|
||||
}
|
||||
} else if (handleValue === 'miss') {
|
||||
if (route.dest && !route.check) {
|
||||
errors.push({
|
||||
message: `You must assign "check: true" after "handle: miss"`,
|
||||
src: route.src,
|
||||
});
|
||||
} else if (!route.dest && !route.continue) {
|
||||
errors.push({
|
||||
message: `You must assign "continue: true" after "handle: miss"`,
|
||||
src: route.src,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errors.push({
|
||||
message: 'A route must set either handle or src',
|
||||
|
||||
89
packages/now-routing-utils/test/index.spec.js
vendored
89
packages/now-routing-utils/test/index.spec.js
vendored
@@ -54,6 +54,19 @@ describe('normalizeRoutes', () => {
|
||||
},
|
||||
{ handle: 'filesystem' },
|
||||
{ src: '^/(?<slug>[^/]+)$', dest: 'blog?slug=$slug' },
|
||||
{ handle: 'hit' },
|
||||
{
|
||||
src: '^/hit-me$',
|
||||
headers: { 'Cache-Control': 'max-age=20' },
|
||||
continue: true,
|
||||
},
|
||||
{ handle: 'miss' },
|
||||
{ src: '^/missed-me$', dest: '/api/missed-me', check: true },
|
||||
{
|
||||
src: '^/missed-me$',
|
||||
headers: { 'Cache-Control': 'max-age=10' },
|
||||
continue: true,
|
||||
},
|
||||
];
|
||||
|
||||
assertValid(routes);
|
||||
@@ -442,6 +455,82 @@ describe('normalizeRoutes', () => {
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test('fails if routes after `handle: hit` use `dest`', () => {
|
||||
const input = [
|
||||
{
|
||||
handle: 'hit',
|
||||
},
|
||||
{
|
||||
src: '^/user$',
|
||||
dest: '^/api/user$',
|
||||
},
|
||||
];
|
||||
const { error } = normalizeRoutes(input);
|
||||
|
||||
assert.deepEqual(error.code, 'invalid_routes');
|
||||
assert.deepEqual(
|
||||
error.errors[0].message,
|
||||
'You cannot assign "dest" after "handle: hit"'
|
||||
);
|
||||
});
|
||||
|
||||
test('fails if routes after `handle: hit` do not use `continue: true`', () => {
|
||||
const input = [
|
||||
{
|
||||
handle: 'hit',
|
||||
},
|
||||
{
|
||||
src: '^/user$',
|
||||
headers: { 'Cache-Control': 'no-cache' },
|
||||
},
|
||||
];
|
||||
const { error } = normalizeRoutes(input);
|
||||
|
||||
assert.deepEqual(error.code, 'invalid_routes');
|
||||
assert.deepEqual(
|
||||
error.errors[0].message,
|
||||
'You must assign "continue: true" after "handle: hit"'
|
||||
);
|
||||
});
|
||||
|
||||
test('fails if routes after `handle: miss` do not use `check: true`', () => {
|
||||
const input = [
|
||||
{
|
||||
handle: 'miss',
|
||||
},
|
||||
{
|
||||
src: '^/user$',
|
||||
dest: '^/api/user$',
|
||||
},
|
||||
];
|
||||
const { error } = normalizeRoutes(input);
|
||||
|
||||
assert.deepEqual(error.code, 'invalid_routes');
|
||||
assert.deepEqual(
|
||||
error.errors[0].message,
|
||||
'You must assign "check: true" after "handle: miss"'
|
||||
);
|
||||
});
|
||||
|
||||
test('fails if routes after `handle: miss` do not use `continue: true`', () => {
|
||||
const input = [
|
||||
{
|
||||
handle: 'miss',
|
||||
},
|
||||
{
|
||||
src: '^/user$',
|
||||
headers: { 'Cache-Control': 'no-cache' },
|
||||
},
|
||||
];
|
||||
const { error } = normalizeRoutes(input);
|
||||
|
||||
assert.deepEqual(error.code, 'invalid_routes');
|
||||
assert.deepEqual(
|
||||
error.errors[0].message,
|
||||
'You must assign "continue: true" after "handle: miss"'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTransformedRoutes', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@now/static-build",
|
||||
"version": "0.14.0",
|
||||
"version": "0.14.1-canary.0",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index",
|
||||
"homepage": "https://zeit.co/docs/runtimes#official-runtimes/static-builds",
|
||||
@@ -22,7 +22,6 @@
|
||||
"@types/cross-spawn": "6.0.0",
|
||||
"@types/ms": "0.7.31",
|
||||
"@types/promise-timeout": "1.3.0",
|
||||
"cross-spawn": "6.0.5",
|
||||
"get-port": "5.0.0",
|
||||
"is-port-reachable": "2.0.1",
|
||||
"ms": "2.1.2",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import ms from 'ms';
|
||||
import path from 'path';
|
||||
import spawn from 'cross-spawn';
|
||||
import getPort from 'get-port';
|
||||
import isPortReachable from 'is-port-reachable';
|
||||
import { ChildProcess, SpawnOptions } from 'child_process';
|
||||
@@ -9,7 +8,10 @@ import { frameworks, Framework } from './frameworks';
|
||||
import {
|
||||
glob,
|
||||
download,
|
||||
execAsync,
|
||||
spawnAsync,
|
||||
execCommand,
|
||||
spawnCommand,
|
||||
runNpmInstall,
|
||||
runBundleInstall,
|
||||
runPipInstall,
|
||||
@@ -160,7 +162,33 @@ function getPkg(entrypoint: string, workPath: string) {
|
||||
return pkg;
|
||||
}
|
||||
|
||||
function getFramework(pkg: PackageJson) {
|
||||
function getFramework(config: Config | null, pkg?: PackageJson | null) {
|
||||
const { framework: configFramework = null, outputDirectory = null } =
|
||||
config || {};
|
||||
|
||||
if (configFramework && configFramework.slug) {
|
||||
const framework = frameworks.find(
|
||||
({ dependency }) => dependency === configFramework.slug
|
||||
);
|
||||
|
||||
if (framework) {
|
||||
if (!framework.getOutputDirName && outputDirectory) {
|
||||
return {
|
||||
...framework,
|
||||
getOutputDirName(prefix: string) {
|
||||
return Promise.resolve(path.join(prefix, outputDirectory));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return framework;
|
||||
}
|
||||
}
|
||||
|
||||
if (!pkg) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dependencies = Object.assign({}, pkg.dependencies, pkg.devDependencies);
|
||||
const framework = frameworks.find(
|
||||
({ dependency }) => dependencies[dependency || '']
|
||||
@@ -183,12 +211,14 @@ export async function build({
|
||||
let distPath = path.join(
|
||||
workPath,
|
||||
path.dirname(entrypoint),
|
||||
(config && (config.distDir as string)) || 'dist'
|
||||
(config && (config.distDir as string)) ||
|
||||
(config.outputDirectory as string) ||
|
||||
'dist'
|
||||
);
|
||||
|
||||
const pkg = getPkg(entrypoint, workPath);
|
||||
|
||||
if (pkg) {
|
||||
if (pkg || config.buildCommand) {
|
||||
const gemfilePath = path.join(workPath, 'Gemfile');
|
||||
const requirementsPath = path.join(workPath, 'requirements.txt');
|
||||
|
||||
@@ -197,7 +227,7 @@ export async function build({
|
||||
let minNodeRange: string | undefined = undefined;
|
||||
|
||||
const routes: Route[] = [];
|
||||
const devScript = getCommand(pkg, 'dev', config as Config);
|
||||
const devScript = pkg ? getCommand(pkg, 'dev', config) : null;
|
||||
|
||||
if (config.zeroConfig) {
|
||||
if (existsSync(gemfilePath) && !meta.isDev) {
|
||||
@@ -244,9 +274,13 @@ export async function build({
|
||||
}
|
||||
|
||||
// `public` is the default for zero config
|
||||
distPath = path.join(workPath, path.dirname(entrypoint), 'public');
|
||||
distPath = path.join(
|
||||
workPath,
|
||||
path.dirname(entrypoint),
|
||||
(config.outputDirectory as string) || 'public'
|
||||
);
|
||||
|
||||
framework = getFramework(pkg);
|
||||
framework = getFramework(config, pkg);
|
||||
}
|
||||
|
||||
if (framework) {
|
||||
@@ -276,11 +310,37 @@ export async function build({
|
||||
console.log('Installing dependencies...');
|
||||
await runNpmInstall(entrypointDir, ['--prefer-offline'], spawnOpts, meta);
|
||||
|
||||
if (meta.isDev && pkg.scripts && pkg.scripts[devScript]) {
|
||||
if (pkg && (config.buildCommand || config.devCommand)) {
|
||||
// We want to add `node_modules/.bin` after `npm install`
|
||||
const { stdout } = await execAsync('yarn', ['bin'], {
|
||||
cwd: entrypointDir,
|
||||
});
|
||||
|
||||
spawnOpts.env = {
|
||||
...spawnOpts.env,
|
||||
PATH: `${stdout.trim()}${path.delimiter}${
|
||||
spawnOpts.env ? spawnOpts.env.PATH : ''
|
||||
}`,
|
||||
};
|
||||
|
||||
debug(
|
||||
`Added "${stdout.trim()}" to PATH env because a package.json file was found.`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
meta.isDev &&
|
||||
(config.devCommand ||
|
||||
(pkg && devScript && pkg.scripts && pkg.scripts[devScript]))
|
||||
) {
|
||||
let devPort: number | undefined = nowDevScriptPorts.get(entrypoint);
|
||||
|
||||
if (typeof devPort === 'number') {
|
||||
debug('`%s` server already running for %j', devScript, entrypoint);
|
||||
debug(
|
||||
'`%s` server already running for %j',
|
||||
config.devCommand || devScript,
|
||||
entrypoint
|
||||
);
|
||||
} else {
|
||||
// Run the `now-dev` or `dev` script out-of-bounds, since it is assumed that
|
||||
// it will launch a dev server that never "completes"
|
||||
@@ -290,10 +350,12 @@ export async function build({
|
||||
const opts: SpawnOptions = {
|
||||
cwd: entrypointDir,
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env, PORT: String(devPort) },
|
||||
env: { ...spawnOpts.env, PORT: String(devPort) },
|
||||
};
|
||||
|
||||
const child: ChildProcess = spawn('yarn', ['run', devScript], opts);
|
||||
const cmd = config.devCommand || `yarn run ${devScript}`;
|
||||
const child: ChildProcess = spawnCommand(cmd, opts);
|
||||
|
||||
child.on('exit', () => nowDevScriptPorts.delete(entrypoint));
|
||||
nowDevChildProcesses.add(child);
|
||||
|
||||
@@ -328,24 +390,30 @@ export async function build({
|
||||
);
|
||||
} else {
|
||||
if (meta.isDev) {
|
||||
debug(`WARN: "${devScript}" script is missing from package.json`);
|
||||
debug(`WARN: A dev script is missing.`);
|
||||
debug(
|
||||
'See the local development docs: https://zeit.co/docs/v2/deployments/official-builders/static-build-now-static-build/#local-development'
|
||||
);
|
||||
}
|
||||
|
||||
const buildScript = getCommand(pkg, 'build', config as Config);
|
||||
debug(`Running "${buildScript}" script in "${entrypoint}"`);
|
||||
|
||||
const found = await runPackageJsonScript(
|
||||
entrypointDir,
|
||||
buildScript,
|
||||
spawnOpts
|
||||
const buildScript = pkg ? getCommand(pkg, 'build', config) : null;
|
||||
debug(
|
||||
`Running "${config.buildCommand ||
|
||||
buildScript}" script in "${entrypoint}"`
|
||||
);
|
||||
|
||||
const found =
|
||||
typeof config.buildCommand === 'string'
|
||||
? await execCommand(config.buildCommand, {
|
||||
...spawnOpts,
|
||||
cwd: entrypointDir,
|
||||
})
|
||||
: await runPackageJsonScript(entrypointDir, buildScript!, spawnOpts);
|
||||
|
||||
if (!found) {
|
||||
throw new Error(
|
||||
`Missing required "${buildScript}" script in "${entrypoint}"`
|
||||
`Missing required "${config.buildCommand ||
|
||||
buildScript}" script in "${entrypoint}"`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -414,6 +482,7 @@ export async function build({
|
||||
export async function prepareCache({
|
||||
entrypoint,
|
||||
workPath,
|
||||
config,
|
||||
}: PrepareCacheOptions): Promise<Files> {
|
||||
// default cache paths
|
||||
const defaultCacheFiles = await glob('node_modules/**', workPath);
|
||||
@@ -423,7 +492,7 @@ export async function prepareCache({
|
||||
|
||||
const pkg = getPkg(entrypoint, workPath);
|
||||
if (pkg) {
|
||||
const framework = getFramework(pkg);
|
||||
const framework = getFramework(config, pkg);
|
||||
|
||||
if (framework && framework.cachePattern) {
|
||||
frameworkCacheFiles = await glob(framework.cachePattern, workPath);
|
||||
|
||||
1
packages/now-static-build/test/fixtures/43-build-command/.gitignore
vendored
Normal file
1
packages/now-static-build/test/fixtures/43-build-command/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
public
|
||||
23
packages/now-static-build/test/fixtures/43-build-command/build.js
vendored
Normal file
23
packages/now-static-build/test/fixtures/43-build-command/build.js
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
const path = require('path');
|
||||
const { promises: fs } = require('fs');
|
||||
|
||||
async function main() {
|
||||
console.log('Starting to build...');
|
||||
|
||||
await fs.mkdir(path.join(__dirname, 'public'));
|
||||
await fs.writeFile(
|
||||
path.join(__dirname, 'public', 'index.txt'),
|
||||
`Time of Creation: ${Date.now()}`
|
||||
);
|
||||
|
||||
console.log('Finished building...');
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => {
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
13
packages/now-static-build/test/fixtures/43-build-command/now.json
vendored
Normal file
13
packages/now-static-build/test/fixtures/43-build-command/now.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"builds": [
|
||||
{
|
||||
"src": "build.js",
|
||||
"use": "@now/static-build",
|
||||
"config": {
|
||||
"zeroConfig": true,
|
||||
"buildCommand": "node build.js"
|
||||
}
|
||||
}
|
||||
],
|
||||
"probes": [{ "path": "/", "mustContain": "Time of Creation:" }]
|
||||
}
|
||||
1
packages/now-static-build/test/fixtures/44-custom-output-dir/.gitignore
vendored
Normal file
1
packages/now-static-build/test/fixtures/44-custom-output-dir/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
custom-output
|
||||
23
packages/now-static-build/test/fixtures/44-custom-output-dir/build.js
vendored
Normal file
23
packages/now-static-build/test/fixtures/44-custom-output-dir/build.js
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
const path = require('path');
|
||||
const { promises: fs } = require('fs');
|
||||
|
||||
async function main() {
|
||||
console.log('Starting to build...');
|
||||
|
||||
await fs.mkdir(path.join(__dirname, 'custom-output'));
|
||||
await fs.writeFile(
|
||||
path.join(__dirname, 'custom-output', 'index.txt'),
|
||||
`Time of Creation: ${Date.now()}`
|
||||
);
|
||||
|
||||
console.log('Finished building...');
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => {
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
30
packages/now-static-build/test/fixtures/44-custom-output-dir/dev.js
vendored
Normal file
30
packages/now-static-build/test/fixtures/44-custom-output-dir/dev.js
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
const http = require('http');
|
||||
|
||||
async function main() {
|
||||
console.log('Starting to build...');
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
console.log(`> Request ${req.url}`);
|
||||
res.end(`Time of Creation: ${Date.now()}`);
|
||||
});
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
server.listen(port, () => {
|
||||
console.log(`Started server on port ${port}`);
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
server.on('exit', () => resolve());
|
||||
server.on('error', error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => {
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
15
packages/now-static-build/test/fixtures/44-custom-output-dir/now.json
vendored
Normal file
15
packages/now-static-build/test/fixtures/44-custom-output-dir/now.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"builds": [
|
||||
{
|
||||
"src": "build.js",
|
||||
"use": "@now/static-build",
|
||||
"config": {
|
||||
"zeroConfig": true,
|
||||
"buildCommand": "node build.js",
|
||||
"devCommand": "node dev.js",
|
||||
"outputDirectory": "custom-output"
|
||||
}
|
||||
}
|
||||
],
|
||||
"probes": [{ "path": "/", "mustContain": "Time of Creation:" }]
|
||||
}
|
||||
19
packages/now-static-build/test/fixtures/45-node-modules-bin-path/now.json
vendored
Normal file
19
packages/now-static-build/test/fixtures/45-node-modules-bin-path/now.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"builds": [
|
||||
{
|
||||
"src": "package.json",
|
||||
"use": "@now/static-build",
|
||||
"config": {
|
||||
"zeroConfig": true,
|
||||
"buildCommand": "next build && next export",
|
||||
"outputDirectory": "out"
|
||||
}
|
||||
}
|
||||
],
|
||||
"build": {
|
||||
"env": {
|
||||
"NOW_BUILDER_DEBUG": "1"
|
||||
}
|
||||
},
|
||||
"probes": [{ "path": "/", "mustContain": "hello world" }]
|
||||
}
|
||||
17
packages/now-static-build/test/fixtures/45-node-modules-bin-path/package.json
vendored
Normal file
17
packages/now-static-build/test/fixtures/45-node-modules-bin-path/package.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "45-node-modules-bin-path",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "pages/index.js",
|
||||
"scripts": {
|
||||
"dev": "next",
|
||||
"build": "next build"
|
||||
},
|
||||
"author": "Andy",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"next": "9.1.5",
|
||||
"react": "16.12.0",
|
||||
"react-dom": "16.12.0"
|
||||
}
|
||||
}
|
||||
1
packages/now-static-build/test/fixtures/45-node-modules-bin-path/pages/index.js
vendored
Normal file
1
packages/now-static-build/test/fixtures/45-node-modules-bin-path/pages/index.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export default () => <div>hello world</div>;
|
||||
Reference in New Issue
Block a user