Compare commits

..

15 Commits

Author SHA1 Message Date
Steven
8aba6f1ff8 Publish Stable
- now@17.0.3
2020-02-07 20:01:17 -05:00
Steven
35e5e328aa Publish Canary
- now@17.0.3-canary.1
2020-02-07 19:18:47 -05:00
Andy
bdd4953d62 [now-cli] Consider root directory for now.json (#3764)
* [now-cli] Consider root directory for now.json

* Adjust message check in test

* Fallback if config does not exist as well

* Assign localConfig later and add debug

* Prefere now.json from root directory

* Comment

* Adjust test and add warning
2020-02-08 01:04:34 +01:00
Steven
e938b298e2 [now-cli] Fix port assignment for now dev (#3769)
This fixes a regression from #3749 where the PORT env var was removed. This is necessary so frameworks like create-react-app and gatsby can proxy to `now dev`.

Fixes #3761
2020-02-07 19:03:56 -05:00
Andy
b67f324e10 [api] Add hasDetectors property (#3770) 2020-02-08 00:29:11 +01:00
Andy Bitz
1dfa286af5 Publish Canary
- now@17.0.3-canary.0
2020-02-07 21:14:50 +01:00
Andy
d4391bd4cc [now-cli] Fix chalk template for legacy (#3768)
* [now-cli] Fix chalk template for legacy

* Update packages/now-cli/src/commands/deploy/legacy.ts

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

Co-authored-by: Nathan Rajlich <n@n8.io>
2020-02-07 21:14:11 +01:00
luc
53eb71f26d Publish Stable
- now@17.0.2
 - @now/routing-utils@1.5.3
2020-02-06 19:18:35 +01:00
luc
92ffd654b5 Publish Canary
- now@17.0.2-canary.1
2020-02-06 18:53:36 +01:00
Luc
36b83f1606 [now-cli] Use npx or yarn to execute dev command (#3760)
1. Use bundled yarn to run `yarn bin`
2. Skip `yarn bin` if `npx` is used
2020-02-06 17:47:39 +00:00
Steven
7a79f620c0 [tests] Fix changelog script (#3751)
Previously, the changelog script was looking for the last "Publish Stable" commit, but it should really be looking for the last Stable release of Now CLI.

This PR updates the changelog script so that it fetches the latest GH Release (which should be Now CLI) and then compares that to the HEAD.
2020-02-06 14:35:15 +00:00
Steven
f3b286ecf3 Publish Canary
- now@17.0.2-canary.0
 - @now/routing-utils@1.5.3-canary.0
2020-02-06 09:12:19 -05:00
Steven
adf31c3fcc [now-routing-utils] Change behavior of trailingSlash: true redirects (#3745)
This PR changes the behavior of `trailingSlash: true` after we received feedback that files should not be redirected with a trailing slash. This matches the behavior of `serve` and `serve-handler`.

### Examples 
* `/index.html` => serve
* `/assets/style.css` => serve
* `/assets` => redirect to `/assets/`
* `/assets/style` => redirect to `/assets/style/` 

### Additional

In order to avoid duplicate content, this PR also adds redirects to files without a trailing slash.

* `/about.html/` => redirect to `/about.html`
* `/assets/style.css/` => redirect to `/assets/style.css`


Fixes #3731
2020-02-06 09:07:46 -05:00
luc
deacdfc47c Publish Stable
- now@17.0.1
2020-02-06 01:07:15 +01:00
Luc
ac9badbe9e [now-cli] Always output deployment url in stdout (#3753) 2020-02-06 00:58:24 +01:00
16 changed files with 296 additions and 208 deletions

View File

@@ -2,17 +2,19 @@ import { NowRequest, NowResponse } from '@now/node';
import { withApiHandler } from './_lib/util/with-api-handler';
import frameworkList, { Framework } from '../packages/frameworks';
const frameworks: Framework[] = (frameworkList as Framework[]).map(
framework => {
delete framework.detectors;
const frameworks = (frameworkList as Framework[]).map(frameworkItem => {
const framework = {
...frameworkItem,
hasDetectors: Boolean(frameworkItem.detectors),
detectors: undefined,
};
if (framework.logo) {
framework.logo = `https://res.cloudinary.com/zeit-inc/image/fetch/${framework.logo}`;
}
return framework;
if (framework.logo) {
framework.logo = `https://res.cloudinary.com/zeit-inc/image/fetch/${framework.logo}`;
}
);
return framework;
});
export default withApiHandler(async function(
req: NowRequest,

View File

@@ -1,6 +1,6 @@
{
"name": "now",
"version": "17.0.0",
"version": "17.0.3",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Now",

View File

@@ -186,7 +186,7 @@ export default async ctx => {
contextName,
output,
stats,
localConfig || {},
localConfig,
parts.latestArgs
);
}

View File

@@ -53,6 +53,7 @@ import { inputRootDirectory } from '../../util/input/input-root-directory';
import validatePaths, {
validateRootDirectory,
} from '../../util/validate-paths';
import { readLocalConfig } from '../../util/config/files';
const addProcessEnv = async (log, env) => {
let val;
@@ -94,7 +95,6 @@ const printDeploymentStatus = async (
},
deployStamp,
isClipboardEnabled,
quiet,
isFile
) => {
const isProdDeployment = target === 'production';
@@ -144,11 +144,6 @@ const printDeploymentStatus = async (
.catch(error => output.debug(`Error copying to clipboard: ${error}`));
}
// write to stdout
if (quiet) {
process.stdout.write(`https://${deploymentUrl}`);
}
output.print(
prependEmoji(
`${isProdDeployment ? 'Production' : 'Preview'}: ${chalk.bold(
@@ -244,13 +239,6 @@ export default async function main(
const { isFile, path } = pathValidation;
const autoConfirm = argv['--confirm'] || isFile;
// build `meta`
const meta = Object.assign(
{},
parseMeta(localConfig.meta),
parseMeta(argv['--meta'])
);
// --no-scale
if (argv['--no-scale']) {
warn(`The option --no-scale is only supported on Now 1.0 deployments`);
@@ -266,7 +254,125 @@ export default async function main(
);
}
if (localConfig && localConfig.name) {
const client = new Client({
apiUrl: ctx.apiUrl,
token: ctx.authConfig.token,
debug: debugEnabled,
});
// retrieve `project` and `org` from .now
const link = await getLinkedProject(output, client, path);
if (link.status === 'error') {
return link.exitCode;
}
let { org, project, status } = link;
let newProjectName = null;
let rootDirectory = project ? project.rootDirectory : null;
if (status === 'not_linked') {
const shouldStartSetup =
autoConfirm ||
(await confirm(
`Set up and deploy ${chalk.cyan(`${toHumanPath(path)}`)}?`,
true
));
if (!shouldStartSetup) {
output.print(`Aborted. Project not set up.\n`);
return 0;
}
org = await selectOrg(
output,
'Which scope do you want to deploy to?',
client,
ctx.config.currentTeam,
autoConfirm
);
// We use `localConfig` here to read the name
// even though the `now.json` file can change
// afterwards, this is fine since the property
// will be deprecated and can be replaced with
// user input.
const detectedProjectName = getProjectName({
argv,
nowConfig: localConfig || {},
isFile,
paths,
});
const projectOrNewProjectName = await inputProject(
output,
client,
org,
detectedProjectName,
autoConfirm
);
if (typeof projectOrNewProjectName === 'string') {
newProjectName = projectOrNewProjectName;
rootDirectory = await inputRootDirectory(path, output, autoConfirm);
} else {
project = projectOrNewProjectName;
rootDirectory = project.rootDirectory;
// we can already link the project
await linkFolderToProject(
output,
path,
{
projectId: project.id,
orgId: org.id,
},
project.name,
org.slug
);
status = 'linked';
}
}
const sourcePath = rootDirectory ? join(path, rootDirectory) : path;
if (
rootDirectory &&
(await validateRootDirectory(
output,
path,
sourcePath,
project
? `To change your project settings, go to https://zeit.co/${org.slug}/${project.name}/settings`
: ''
)) === false
) {
return 1;
}
// If Root Directory is used we'll try to read the config
// from there instead and use it if it exists.
if (rootDirectory) {
const rootDirectoryConfig = readLocalConfig(sourcePath);
if (rootDirectoryConfig) {
debug(`Read local config from root directory (${rootDirectory})`);
localConfig = rootDirectoryConfig;
} else if (localConfig) {
output.print(
`${prependEmoji(
`The ${highlight(
'now.json'
)} file should be inside of the provided root directory.`,
emoji('warning')
)}\n`
);
}
}
localConfig = localConfig || {};
if (localConfig.name) {
output.print(
`${prependEmoji(
`The ${code('name')} property in ${highlight(
@@ -316,6 +422,13 @@ export default async function main(
}
}
// build `meta`
const meta = Object.assign(
{},
parseMeta(localConfig.meta),
parseMeta(argv['--meta'])
);
// Merge dotenv config, `env` from now.json, and `--env` / `-e` arguments
const deploymentEnv = Object.assign(
{},
@@ -373,97 +486,6 @@ export default async function main(
target = 'production';
}
const client = new Client({
apiUrl: ctx.apiUrl,
token: ctx.authConfig.token,
debug: debugEnabled,
});
// retrieve `project` and `org` from .now
const link = await getLinkedProject(output, client, path);
if (link.status === 'error') {
return link.exitCode;
}
let { org, project, status } = link;
let newProjectName = null;
let rootDirectory = project ? project.rootDirectory : null;
if (status === 'not_linked') {
const shouldStartSetup =
autoConfirm ||
(await confirm(
`Set up and deploy ${chalk.cyan(`${toHumanPath(path)}`)}?`,
true
));
if (!shouldStartSetup) {
output.print(`Aborted. Project not set up.\n`);
return 0;
}
org = await selectOrg(
output,
'Which scope do you want to deploy to?',
client,
ctx.config.currentTeam,
autoConfirm
);
const detectedProjectName = getProjectName({
argv,
nowConfig: localConfig,
isFile,
paths,
});
const projectOrNewProjectName = await inputProject(
output,
client,
org,
detectedProjectName,
autoConfirm
);
if (typeof projectOrNewProjectName === 'string') {
newProjectName = projectOrNewProjectName;
rootDirectory = await inputRootDirectory(path, output, autoConfirm);
} else {
project = projectOrNewProjectName;
rootDirectory = project.rootDirectory;
// we can already link the project
await linkFolderToProject(
output,
path,
{
projectId: project.id,
orgId: org.id,
},
project.name,
org.slug
);
status = 'linked';
}
}
const sourcePath = rootDirectory ? join(path, rootDirectory) : path;
if (
rootDirectory &&
(await validateRootDirectory(
output,
path,
sourcePath,
project
? `To change your project settings, go to https://zeit.co/${org.slug}/${project.name}/settings`
: ''
)) === false
) {
return 1;
}
const currentTeam = org.type === 'team' ? org.id : undefined;
const now = new Now({ apiUrl, token, debug: debugEnabled, currentTeam });
let deployStamp = stamp();
@@ -659,7 +681,6 @@ export default async function main(
deployment,
deployStamp,
!argv['--no-clipboard'],
quiet,
isFile
);
}

View File

@@ -852,7 +852,7 @@ async function sync({
const { url } = now;
const dcs =
deploymentType !== 'static' && deployment.scale
? chalk` ({bold ${Object.keys(deployment.scale).join(', ')})`
? ` (${ chalk.bold(Object.keys(deployment.scale).join(', ')) })`
: '';
if (isTTY) {

View File

@@ -83,6 +83,7 @@ export default async function processDeployment({
deployStamp,
force,
nowConfig,
quiet,
} = args;
const { debug } = output;
@@ -179,6 +180,10 @@ export default async function processDeployment({
printInspectUrl(output, event.payload.url, deployStamp, org.slug);
if (quiet) {
process.stdout.write(`https://${event.payload.url}`);
}
if (queuedSpinner === null) {
queuedSpinner =
event.payload.readyState === 'QUEUED'

View File

@@ -11,7 +11,7 @@ import { randomBytes } from 'crypto';
import serveHandler from 'serve-handler';
import { watch, FSWatcher } from 'chokidar';
import { parse as parseDotenv } from 'dotenv';
import { basename, dirname, extname, join, delimiter } from 'path';
import { basename, dirname, extname, join } from 'path';
import { getTransformedRoutes, HandleValue } from '@now/routing-utils';
import directoryTemplate from 'serve-handler/src/directory';
import getPort from 'get-port';
@@ -27,7 +27,6 @@ import {
detectRoutes,
detectApiDirectory,
detectApiExtensions,
execAsync,
spawnCommand,
} from '@now/build-utils';
@@ -1633,18 +1632,14 @@ export default class DevServer {
}
async runDevCommand() {
if (!this.devCommand) return;
const { devCommand, cwd } = this;
const cwd = this.cwd;
const { stdout: yarnBinStdout } = await execAsync('yarn', ['bin'], {
cwd,
});
const yarnBinPath = yarnBinStdout.trim();
if (!devCommand) {
return;
}
this.output.log(
`Running Dev Command ${chalk.cyan.bold(`${this.devCommand}`)}`
`Running Dev Command ${chalk.cyan.bold(`${devCommand}`)}`
);
const port = await getPort();
@@ -1653,16 +1648,19 @@ export default class DevServer {
...process.env,
...this.buildEnv,
NOW_REGION: 'dev1',
PORT: `${port}`,
};
const devCommand = this.devCommand
// This is necesary so that the dev command in the Project
// will work cross-platform (especially Windows).
let command = devCommand
.replace(/\$PORT/g, `${port}`)
.replace(/%PORT%/g, `${port}`);
this.output.debug(
`Starting dev command with parameters : ${JSON.stringify({
cwd: this.cwd,
devCommand,
cwd,
command,
port,
})}`
);
@@ -1671,17 +1669,21 @@ export default class DevServer {
.then(() => true)
.catch(() => false);
if (!isNpxAvailable) {
env.PATH = `${yarnBinPath}${delimiter}${env.PATH}`;
if (isNpxAvailable) {
command = `npx --no-install ${command}`;
} else {
const isYarnAvailable = await which('yarn')
.then(() => true)
.catch(() => false);
if (isYarnAvailable) {
command = `yarn run --silent ${command}`;
}
}
this.output.debug('Spawning dev command');
this.output.debug(`PATH is ${env.PATH}`);
this.output.debug(`Spawning dev command: ${command}`);
const p = spawnCommand(
isNpxAvailable ? `npx --no-install ${devCommand}` : devCommand,
{ stdio: 'inherit', cwd, env }
);
const p = spawnCommand(command, { stdio: 'inherit', cwd, env });
p.on('exit', () => {
this.devProcessPort = undefined;

View File

@@ -0,0 +1 @@
SKIP_PREFLIGHT_CHECK=true

View File

@@ -22,3 +22,4 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
!yarn.lock
!.env

View File

@@ -638,7 +638,7 @@ test(
await testPath(200, '/about/', 'About Page');
await testPath(200, '/sub/', 'Sub Index Page');
await testPath(200, '/sub/another/', 'Sub Another Page');
await testPath(200, '/style.css/', 'body { color: green }');
await testPath(200, '/style.css', 'body { color: green }');
await testPath(308, '/index.html', '', { Location: '/' });
await testPath(308, '/about.html', '', { Location: '/about/' });
await testPath(308, '/sub/index.html', '', { Location: '/sub/' });
@@ -653,17 +653,15 @@ test(
'[now dev] test trailingSlash true serve correct content',
testFixtureStdio('test-trailing-slash', async (t, port, testPath) => {
await testPath(200, '/', 'Index Page');
await testPath(200, '/index.html/', 'Index Page');
await testPath(200, '/about.html/', 'About Page');
await testPath(200, '/index.html', 'Index Page');
await testPath(200, '/about.html', 'About Page');
await testPath(200, '/sub/', 'Sub Index Page');
await testPath(200, '/sub/index.html/', 'Sub Index Page');
await testPath(200, '/sub/another.html/', 'Sub Another Page');
await testPath(200, '/style.css/', 'body { color: green }');
await testPath(308, '/about.html', '', { Location: '/about.html/' });
await testPath(200, '/sub/index.html', 'Sub Index Page');
await testPath(200, '/sub/another.html', 'Sub Another Page');
await testPath(200, '/style.css', 'body { color: green }');
await testPath(308, '/about.html/', '', { Location: '/about.html' });
await testPath(308, '/style.css/', '', { Location: '/style.css' });
await testPath(308, '/sub', '', { Location: '/sub/' });
await testPath(308, '/sub/another.html', '', {
Location: '/sub/another.html/',
});
})
);
@@ -772,17 +770,17 @@ test('[now dev] 03-aurelia', async t => {
await tester(t);
});
// test(
// '[now dev] 04-create-react-app-node',
// testFixtureStdio('create-react-app', async(t, port) => {
// const response = await fetch(`http://localhost:${port}`);
test(
'[now dev] 04-create-react-app',
testFixtureStdio('04-create-react-app', async (t, port) => {
const response = await fetch(`http://localhost:${port}`);
// validateResponseHeaders(t, response);
validateResponseHeaders(t, response);
// const body = await response.text();
// t.regex(body, /React App/gm);
// })
// );
const body = await response.text();
t.regex(body, /React App/gm);
})
);
test('[now dev] 05-gatsby', async t => {
if (shouldSkip(t, '05-gatsby', '>^6.14.0 || ^8.10.0 || >=9.10.0')) return;

View File

@@ -487,6 +487,14 @@ CMD ["node", "index.js"]`,
},
'project-root-directory': {
'src/index.html': '<h1>I am a website.</h1>',
'src/now.json': JSON.stringify({
rewrites: [
{
source: '/i-do-exist',
destination: '/',
},
],
}),
},
};

View File

@@ -2279,17 +2279,19 @@ test('should prefill "project name" prompt with now.json `name`', async t => {
let isDeprecated = false;
await waitForPrompt(now, chunk => {
if (chunk.includes('The `name` property in now.json is deprecated')) {
now.stderr.on('data', data => {
if (
data.toString().includes('The `name` property in now.json is deprecated')
) {
isDeprecated = true;
}
});
await waitForPrompt(now, chunk => {
return /Set up and deploy [^?]+\?/.test(chunk);
});
now.stdin.write('yes\n');
t.is(isDeprecated, true);
await waitForPrompt(now, chunk =>
chunk.includes('Which scope do you want to deploy to?')
);
@@ -2318,6 +2320,8 @@ test('should prefill "project name" prompt with now.json `name`', async t => {
const output = await now;
t.is(output.exitCode, 0, formatOutput(output));
t.is(isDeprecated, true);
// clean up
await remove(path.join(directory, 'now.json'));
});
@@ -2448,9 +2452,14 @@ test('use `rootDirectory` from project when deploying', async t => {
const secondResult = await execute([directory, '--public']);
t.is(secondResult.exitCode, 0, formatOutput(secondResult));
const pageResponse = await fetch(secondResult.stdout);
t.is(pageResponse.status, 200);
t.regex(await pageResponse.text(), /I am a website/gm);
const pageResponse1 = await fetch(secondResult.stdout);
t.is(pageResponse1.status, 200);
t.regex(await pageResponse1.text(), /I am a website/gm);
// Ensures that the `now.json` file has been applied
const pageResponse2 = await fetch(`${secondResult.stdout}/i-do-exist`);
t.is(pageResponse2.status, 200);
t.regex(await pageResponse2.text(), /I am a website/gm);
await apiFetch(`/v2/projects/${projectId}`, {
method: 'DELETE',

View File

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

View File

@@ -105,10 +105,15 @@ export function convertTrailingSlash(enable: boolean, status = 308): Route[] {
const routes: Route[] = [];
if (enable) {
routes.push({
src: '^/(.*[^\\/])$',
src: '^/((?:[^/]+/)*[^/\\.]+)$',
headers: { Location: '/$1/' },
status,
});
routes.push({
src: '^/((?:[^/]+/)*[^/]+\\.\\w+)/$',
headers: { Location: '/$1' },
status,
});
} else {
routes.push({
src: '^/(.*)\\/$',

View File

@@ -503,16 +503,41 @@ test('convertTrailingSlash enabled', () => {
const actual = convertTrailingSlash(true);
const expected = [
{
src: '^/(.*[^\\/])$',
src: '^/((?:[^/]+/)*[^/\\.]+)$',
headers: { Location: '/$1/' },
status: 308,
},
{
src: '^/((?:[^/]+/)*[^/]+\\.\\w+)/$',
headers: { Location: '/$1' },
status: 308,
},
];
deepEqual(actual, expected);
const mustMatch = [['/index.html', '/dir', '/dir/index.html', '/foo/bar']];
const mustMatch = [
['/dir', '/dir/foo', '/dir/foo/bar'],
['/foo.html/', '/dir/foo.html/', '/dir/foo/bar.css/', '/dir/about.map.js/'],
];
const mustNotMatch = [['/', '/dir/', '/dir/foo/', '/next.php?page=/']];
const mustNotMatch = [
[
'/',
'/index.html',
'/asset/style.css',
'/asset/about.map.js',
'/dir/',
'/dir/foo/',
'/next.php?page=/',
],
[
'/',
'/foo.html',
'/dir/foo.html',
'/dir/foo/bar.css',
'/dir/about.map.js',
],
];
assertRegexMatches(actual, mustMatch, mustNotMatch);
});

View File

@@ -1,45 +1,56 @@
const { join } = require('path');
const { execSync } = require('child_process');
const fetch = require('node-fetch');
process.chdir(join(__dirname, '..'));
const commit = execSync('git log --pretty=format:"%s %H"')
.toString()
.trim()
.split('\n')
.find(line => line.startsWith('Publish Stable '))
.split(' ')
.pop();
async function main() {
const res = await fetch(
'https://api.github.com/repos/zeit/now/releases/latest'
);
const { tag_name } = await res.json();
if (!commit) {
throw new Error('Unable to find last publish commit');
// git log --pretty=format:"- %s [%an]" `git show-ref -s 'now@16.7.3'`...HEAD | grep -v '\- Publish '
if (!tag_name) {
throw new Error('Unable to find last GitHub Release tag.');
}
const log =
execSync(`git log --pretty=format:"- %s [%an]" ${tag_name}...HEAD`)
.toString()
.trim()
.split('\n')
.filter(line => !line.startsWith('- Publish '))
.join('\n') || 'NO CHANGES DETECTED';
console.log(`Changes since the last stable release (${tag_name}):`);
console.log(`\n${log}\n`);
const pkgs =
Array.from(
new Set(
execSync(`git diff --name-only ${tag_name}...HEAD`)
.toString()
.trim()
.split('\n')
.filter(line => line.startsWith('packages/'))
.map(line => line.split('/')[1])
.map(pkgName => {
try {
return require(`../packages/${pkgName}/package.json`).name;
} catch {
// Failed to read package.json (perhaps the pkg was deleted)
}
})
.filter(s => Boolean(s))
)
).join(',') || 'now';
console.log('To publish a stable release, execute the following:');
console.log(
`\nnpx lerna version --message 'Publish Stable' --exact --force-publish=${pkgs}\n`
);
}
const log =
execSync(`git log --pretty=format:"- %s [%an]" ${commit}...HEAD`)
.toString()
.trim()
.split('\n')
.filter(line => !line.startsWith('- Publish Canary '))
.join('\n') || 'NO CHANGES DETECTED';
console.log(`Changes since the last Stable release (${commit.slice(0, 7)}):`);
console.log(`\n${log}\n`);
const pkgs =
Array.from(
new Set(
execSync(`git diff --name-only ${commit}...HEAD`)
.toString()
.trim()
.split('\n')
.filter(line => line.startsWith('packages/'))
.map(line => line.split('/')[1])
.map(pkgName => require(`../packages/${pkgName}/package.json`).name)
)
).join(',') || 'now';
console.log('To publish a stable release, execute the following:');
console.log(
`\ngit pull && lerna version --message 'Publish Stable' --exact --force-publish=${pkgs}\n`
);
main().catch(console.error);