Compare commits

...

57 Commits

Author SHA1 Message Date
Sean Massa
0fa6ff1701 detect remix version 2023-01-09 14:06:14 -06:00
Sean Massa
06e123f494 Merge branch 'main' into endangeredmassa/detect-framework-versions 2023-01-06 14:53:10 -06:00
Sean Massa
c0d63d8017 fix framework version lookup 2023-01-06 14:52:56 -06:00
Sean Massa
6e9eef793b Merge remote-tracking branch 'origin' into endangeredmassa/detect-framework-versions 2023-01-06 13:39:38 -06:00
Sean Massa
48471f9a7f Merge branch 'main' into endangeredmassa/detect-framework-versions 2023-01-05 11:04:14 -06:00
Sean Massa
0f50da19a2 update schema 2023-01-05 10:44:49 -06:00
Sean Massa
057d0ed269 use ncc-safe package version lookup 2023-01-05 09:58:42 -06:00
Sean Massa
6ca6b397c9 use semver to verify package.json version is explicit 2023-01-05 09:54:10 -06:00
Sean Massa
6774f990b4 remove unused versionDependencies 2023-01-04 18:57:42 -06:00
Sean Massa
036989b977 Merge branch 'main' into endangeredmassa/detect-framework-versions 2023-01-04 18:56:30 -06:00
Sean Massa
b18c4dbd3a Merge branch 'endangeredmassa/detect-framework-versions' of https://github.com/vercel/vercel into endangeredmassa/detect-framework-versions 2023-01-04 18:56:07 -06:00
Sean Massa
433bf5480b implement matchPackage detector 2023-01-04 18:51:09 -06:00
Sean Massa
6c640c4f19 Merge branch 'main' into endangeredmassa/detect-framework-versions 2022-12-16 02:13:47 -06:00
Sean Massa
6cb571f480 Merge branch 'main' into endangeredmassa/detect-framework-versions 2022-12-14 09:51:41 -06:00
Sean Massa
a9d281f155 Update package.json
Co-authored-by: Nathan Rajlich <n@n8.io>
2022-12-14 09:51:34 -06:00
Sean Massa
dbb7dd2607 Update package.json
Co-authored-by: Nathan Rajlich <n@n8.io>
2022-12-14 09:51:27 -06:00
Sean Massa
1b62000f68 Merge branch 'main' into endangeredmassa/detect-framework-versions 2022-12-13 01:03:42 -06:00
Sean Massa
f78b1aaa87 Revert "do not import from dist"
This reverts commit fed8f99f19.
2022-12-12 16:40:40 -06:00
Sean Massa
dcb125dbba remove testing 2022-12-12 16:37:54 -06:00
Sean Massa
68f2c8acf9 Revert "fix flakey test"
This reverts commit 3f69e0110f.
2022-12-12 16:37:32 -06:00
Sean Massa
ae0f9b7353 Revert "update build-utils to latest in fs-detectors"
This reverts commit 0df400cf6b.
2022-12-12 16:37:18 -06:00
Sean Massa
aca134fe53 remove testing 2022-12-12 16:37:16 -06:00
Sean Massa
0df400cf6b update build-utils to latest in fs-detectors 2022-12-12 16:14:28 -06:00
Sean Massa
fed8f99f19 do not import from dist 2022-12-12 16:00:00 -06:00
Sean Massa
c415fd0fad force off turbo cache 2022-12-12 15:38:34 -06:00
Sean Massa
bf6efd2d02 bust turbo cache on frameworks 2022-12-12 15:30:12 -06:00
Sean Massa
23dae76bc4 test fs-detectors build timing 2022-12-12 15:22:46 -06:00
Sean Massa
3f69e0110f fix flakey test 2022-12-12 15:14:27 -06:00
Sean Massa
28efa86e20 try breaking turbo cache 2022-12-12 14:36:06 -06:00
Sean Massa
b96dead0d1 Merge branch 'main' into endangeredmassa/detect-framework-versions 2022-12-12 13:07:53 -06:00
Sean Massa
fbf562eb51 PR feedback 2022-12-12 13:07:35 -06:00
Sean Massa
6954ca19ef fix path 2022-12-09 15:12:29 -06:00
Sean Massa
87fa17b968 add node_module mock to fixture 2022-12-09 14:53:11 -06:00
Sean Massa
18a19c8928 extract VirtualFilesystem into a shared file 2022-12-09 14:43:17 -06:00
Sean Massa
46c1cef0f4 remove fail 2022-12-09 14:35:25 -06:00
Sean Massa
0072df8464 added tests 2022-12-09 14:24:27 -06:00
Sean Massa
f2cbf0e4c3 remove debugging logs 2022-12-09 13:38:17 -06:00
Sean Massa
9c56142c8a Merge branch 'main' into endangeredmassa/detect-framework-versions 2022-12-09 13:34:59 -06:00
Sean Massa
f3484f9801 use framework returned by builder 2022-12-09 12:40:40 -06:00
Sean Massa
9e6119a8a9 Merge branch 'main' into endangeredmassa/detect-framework-versions 2022-12-09 12:05:08 -06:00
Sean Massa
c5d2f1b470 pull framework from build output, when available 2022-12-09 12:04:53 -06:00
Sean Massa
9893e3af64 Merge branch 'main' into endangeredmassa/detect-framework-versions 2022-12-08 16:23:57 -06:00
Sean Massa
3cffb69b25 Merge branch 'main' into endangeredmassa/detect-framework-versions 2022-12-08 10:06:12 -06:00
Sean Massa
b76f28b331 Merge branch 'main' into endangeredmassa/detect-framework-versions 2022-12-07 16:45:41 -06:00
Sean Massa
3e92ec897b Merge branch 'main' into endangeredmassa/detect-framework-versions 2022-12-07 09:06:32 -06:00
Sean Massa
5147d0285e add logging 2022-12-07 09:06:05 -06:00
Sean Massa
e117cfb0e8 add next version in more places 2022-12-06 18:25:31 -06:00
Sean Massa
2d269bb995 add version detection to next builder 2022-12-06 01:29:40 -06:00
Sean Massa
668d7e537b remove unnecessary comments 2022-12-05 21:30:41 -06:00
Sean Massa
f0c85bbf8e Merge branch 'main' into endangeredmassa/detect-framework-versions 2022-12-05 17:03:27 -06:00
Sean Massa
da2a327f97 remove unused 2022-12-05 17:03:05 -06:00
Sean Massa
4abce84609 revert some detector changes 2022-12-05 17:02:31 -06:00
Sean Massa
f0f2dc9af6 use npm ls --json 2022-12-05 17:00:16 -06:00
Sean Massa
b7f1cffc16 switch to npm ls method of detecting version 2022-12-05 16:36:58 -06:00
Sean Massa
d1f0997d0a add version detection to most frameworks 2022-12-05 14:38:56 -06:00
Sean Massa
e054d52bd6 await 2022-12-05 14:07:15 -06:00
Sean Massa
e04c0decf0 detect some versions 2022-12-05 13:58:07 -06:00
15 changed files with 642 additions and 274 deletions

View File

@@ -17,7 +17,11 @@ import {
BuildResultV3,
NowBuildError,
} from '@vercel/build-utils';
import { detectBuilders } from '@vercel/fs-detectors';
import {
detectBuilders,
detectFrameworkRecord,
LocalFileSystemDetector,
} from '@vercel/fs-detectors';
import minimatch from 'minimatch';
import {
appendRoutesToPhase,
@@ -59,6 +63,9 @@ import { toEnumerableError } from '../util/error';
import { validateConfig } from '../util/validate-config';
import { setMonorepoDefaultSettings } from '../util/build/monorepo';
import frameworks from '@vercel/frameworks';
import { detectFrameworkVersion } from '@vercel/fs-detectors';
import semver from 'semver';
type BuildResult = BuildResultV2 | BuildResultV3;
@@ -69,6 +76,20 @@ interface SerializedBuilder extends Builder {
apiVersion: number;
}
/**
* Build Output API `config.json` file interface.
*/
interface BuildOutputConfig {
version?: 3;
wildcard?: BuildResultV2Typical['wildcard'];
images?: BuildResultV2Typical['images'];
routes?: BuildResultV2Typical['routes'];
overrides?: Record<string, PathOverride>;
framework?: {
version: string;
};
}
/**
* Contents of the `builds.json` file.
*/
@@ -434,7 +455,7 @@ async function doBuild(
// Execute Builders for detected entrypoints
// TODO: parallelize builds (except for frontend)
const sortedBuilders = sortBuilders(builds);
const buildResults: Map<Builder, BuildResult> = new Map();
const buildResults: Map<Builder, BuildResult | BuildOutputConfig> = new Map();
const overrides: PathOverride[] = [];
const repoRootPath = cwd;
const corepackShimDir = await initCorepack({ repoRootPath });
@@ -538,8 +559,7 @@ async function doBuild(
// Merge existing `config.json` file into the one that will be produced
const configPath = join(outputDir, 'config.json');
// TODO: properly type
const existingConfig = await readJSONFile<any>(configPath);
const existingConfig = await readJSONFile<BuildOutputConfig>(configPath);
if (existingConfig instanceof CantParseJSONFile) {
throw existingConfig;
}
@@ -585,15 +605,17 @@ async function doBuild(
const mergedOverrides: Record<string, PathOverride> =
overrides.length > 0 ? Object.assign({}, ...overrides) : undefined;
const framework = await getFramework(cwd, buildResults);
// Write out the final `config.json` file based on the
// user configuration and Builder build results
// TODO: properly type
const config = {
const config: BuildOutputConfig = {
version: 3,
routes: mergedRoutes,
images: mergedImages,
wildcard: mergedWildcard,
overrides: mergedOverrides,
framework,
};
await fs.writeJSON(join(outputDir, 'config.json'), config, { spaces: 2 });
@@ -608,6 +630,50 @@ async function doBuild(
);
}
async function getFramework(
cwd: string,
buildResults: Map<Builder, BuildResult | BuildOutputConfig>
): Promise<{ version: string } | undefined> {
const detectedFramework = await detectFrameworkRecord({
fs: new LocalFileSystemDetector(cwd),
frameworkList: frameworks,
});
if (!detectedFramework) {
return;
}
// determine framework version from build result
if (detectedFramework.useRuntime) {
for (const [build, buildResult] of buildResults.entries()) {
if (
'framework' in buildResult &&
build.use === detectedFramework.useRuntime.use
) {
return buildResult.framework;
}
}
}
// determine framework version from listed package.json version
if (detectedFramework.detectedVersion) {
// check for a valid, explicit version, not a range
if (semver.valid(detectedFramework.detectedVersion)) {
return {
version: detectedFramework.detectedVersion,
};
}
}
// determine framework version with runtime lookup
const frameworkVersion = await detectFrameworkVersion(detectedFramework);
if (frameworkVersion) {
return {
version: frameworkVersion,
};
}
}
function expandBuild(files: string[], build: Builder): Builder[] {
if (!build.use) {
throw new NowBuildError({
@@ -648,7 +714,7 @@ function expandBuild(files: string[], build: Builder): Builder[] {
function mergeImages(
images: BuildResultV2Typical['images'],
buildResults: Iterable<BuildResult>
buildResults: Iterable<BuildResult | BuildOutputConfig>
): BuildResultV2Typical['images'] {
for (const result of buildResults) {
if ('images' in result && result.images) {
@@ -659,7 +725,7 @@ function mergeImages(
}
function mergeWildcard(
buildResults: Iterable<BuildResult>
buildResults: Iterable<BuildResult | BuildOutputConfig>
): BuildResultV2Typical['wildcard'] {
let wildcard: BuildResultV2Typical['wildcard'] = undefined;
for (const result of buildResults) {

View File

@@ -0,0 +1,4 @@
{
"name": "next",
"version": "13.0.4"
}

View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"next": "13.0.4"
}
}

View File

@@ -2,12 +2,12 @@ import ms from 'ms';
import fs from 'fs-extra';
import { join } from 'path';
import { getWriteableDirectory } from '@vercel/build-utils';
import build from '../../../src/commands/build';
import { client } from '../../mocks/client';
import { defaultProject, useProject } from '../../mocks/project';
import { useTeams } from '../../mocks/team';
import { useUser } from '../../mocks/user';
import { setupFixture } from '../../helpers/setup-fixture';
import build from '../../../../src/commands/build';
import { client } from '../../../mocks/client';
import { defaultProject, useProject } from '../../../mocks/project';
import { useTeams } from '../../../mocks/team';
import { useUser } from '../../../mocks/user';
import { setupFixture } from '../../../helpers/setup-fixture';
import JSON5 from 'json5';
// TODO (@Ethan-Arrowood) - After shipping support for turbo and nx, revisit rush support
// import execa from 'execa';
@@ -15,7 +15,7 @@ import JSON5 from 'json5';
jest.setTimeout(ms('1 minute'));
const fixture = (name: string) =>
join(__dirname, '../../fixtures/unit/commands/build', name);
join(__dirname, '../../../fixtures/unit/commands/build', name);
describe('build', () => {
const originalCwd = process.cwd();

View File

@@ -74,9 +74,7 @@ export const frameworks = [
detectors: {
every: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"next":\\s*".+?"[^}]*}',
matchPackage: 'next',
},
],
},
@@ -119,9 +117,7 @@ export const frameworks = [
detectors: {
every: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"gatsby":\\s*".+?"[^}]*}',
matchPackage: 'gatsby',
},
],
},
@@ -244,9 +240,7 @@ export const frameworks = [
detectors: {
every: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"astro":\\s*".+?"[^}]*}',
matchPackage: 'astro',
},
],
},
@@ -288,9 +282,7 @@ export const frameworks = [
detectors: {
every: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"hexo":\\s*".+?"[^}]*}',
matchPackage: 'hexo',
},
],
},
@@ -325,9 +317,7 @@ export const frameworks = [
detectors: {
every: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"@11ty\\/eleventy":\\s*".+?"[^}]*}',
matchPackage: '@11ty/eleventy',
},
],
},
@@ -364,9 +354,7 @@ export const frameworks = [
detectors: {
some: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"@docusaurus\\/core":\\s*".+?"[^}]*}',
matchPackage: '@docusaurus/core',
},
],
},
@@ -452,9 +440,7 @@ export const frameworks = [
detectors: {
some: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"docusaurus":\\s*".+?"[^}]*}',
matchPackage: 'docusaurus',
},
],
},
@@ -503,9 +489,7 @@ export const frameworks = [
detectors: {
every: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"preact-cli":\\s*".+?"[^}]*}',
matchPackage: 'preact-cli',
},
],
},
@@ -549,14 +533,10 @@ export const frameworks = [
detectors: {
every: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"solid-js":\\s*".+?"[^}]*}',
matchPackage: 'solid-js',
},
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"solid-start":\\s*".+?"[^}]*}',
matchPackage: 'solid-start',
},
],
},
@@ -589,9 +569,7 @@ export const frameworks = [
detectors: {
some: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"@dojo\\/framework":\\s*".+?"[^}]*}',
matchPackage: '@dojo/framework',
},
{
path: '.dojorc',
@@ -651,9 +629,7 @@ export const frameworks = [
detectors: {
every: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"ember-cli":\\s*".+?"[^}]*}',
matchPackage: 'ember-cli',
},
],
},
@@ -698,9 +674,7 @@ export const frameworks = [
detectors: {
every: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"@vue\\/cli-service":\\s*".+?"[^}]*}',
matchPackage: '@vue/cli-service',
},
],
},
@@ -753,9 +727,7 @@ export const frameworks = [
detectors: {
every: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"@scullyio\\/init":\\s*".+?"[^}]*}',
matchPackage: '@scullyio/init',
},
],
},
@@ -790,9 +762,7 @@ export const frameworks = [
detectors: {
every: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"@ionic\\/angular":\\s*".+?"[^}]*}',
matchPackage: '@ionic/angular',
},
],
},
@@ -835,9 +805,7 @@ export const frameworks = [
detectors: {
every: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"@angular\\/cli":\\s*".+?"[^}]*}',
matchPackage: '@angular/cli',
},
],
},
@@ -895,9 +863,7 @@ export const frameworks = [
detectors: {
every: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"polymer-cli":\\s*".+?"[^}]*}',
matchPackage: 'polymer-cli',
},
],
},
@@ -953,14 +919,10 @@ export const frameworks = [
detectors: {
every: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"svelte":\\s*".+?"[^}]*}',
matchPackage: 'svelte',
},
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"sirv-cli":\\s*".+?"[^}]*}',
matchPackage: 'sirv-cli',
},
],
},
@@ -1081,9 +1043,7 @@ export const frameworks = [
detectors: {
every: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"@ionic\\/react":\\s*".+?"[^}]*}',
matchPackage: '@ionic/react',
},
],
},
@@ -1143,14 +1103,10 @@ export const frameworks = [
detectors: {
some: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"react-scripts":\\s*".+?"[^}]*}',
matchPackage: 'react-scripts',
},
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"react-dev-utils":\\s*".+?"[^}]*}',
matchPackage: 'react-dev-utils',
},
],
},
@@ -1209,9 +1165,7 @@ export const frameworks = [
detectors: {
every: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"gridsome":\\s*".+?"[^}]*}',
matchPackage: 'gridsome',
},
],
},
@@ -1246,9 +1200,7 @@ export const frameworks = [
detectors: {
every: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"umi":\\s*".+?"[^}]*}',
matchPackage: 'umi',
},
],
},
@@ -1292,9 +1244,7 @@ export const frameworks = [
detectors: {
every: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"sapper":\\s*".+?"[^}]*}',
matchPackage: 'sapper',
},
],
},
@@ -1329,9 +1279,7 @@ export const frameworks = [
detectors: {
every: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"saber":\\s*".+?"[^}]*}',
matchPackage: 'saber',
},
],
},
@@ -1380,9 +1328,7 @@ export const frameworks = [
detectors: {
every: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"@stencil\\/core":\\s*".+?"[^}]*}',
matchPackage: '@stencil/core',
},
],
},
@@ -1443,11 +1389,15 @@ export const frameworks = [
sort: 2,
envPrefix: 'NUXT_ENV_',
detectors: {
every: [
some: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"nuxt3?(-edge)?":\\s*".+?"[^}]*}',
matchPackage: 'nuxt',
},
{
matchPackage: 'nuxt3',
},
{
matchPackage: 'nuxt3-edge',
},
],
},
@@ -1503,9 +1453,7 @@ export const frameworks = [
detectors: {
every: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"@redwoodjs\\/core":\\s*".+?"[^}]*}',
matchPackage: '@redwoodjs/core',
},
],
},
@@ -1736,9 +1684,7 @@ export const frameworks = [
detectors: {
every: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"vite":\\s*".+?"[^}]*}',
matchPackage: 'vite',
},
],
},
@@ -1772,9 +1718,7 @@ export const frameworks = [
detectors: {
every: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"vitepress":\\s*".+?"[^}]*}',
matchPackage: 'vitepress',
},
],
},
@@ -1806,9 +1750,7 @@ export const frameworks = [
detectors: {
every: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*vuepress:\\s*".+?"[^}]*}',
matchPackage: 'vuepress',
},
],
},
@@ -1841,9 +1783,7 @@ export const frameworks = [
detectors: {
every: [
{
path: 'package.json',
matchContent:
'"(dev)?(d|D)ependencies":\\s*{[^}]*"parcel":\\s*".+?"[^}]*}',
matchPackage: 'parcel',
},
],
},

View File

@@ -2,15 +2,24 @@ import { Rewrite, Route } from '@vercel/routing-utils';
export interface FrameworkDetectionItem {
/**
* A file path
* @example "package.json"
* A file path to detect.
* If specified, "matchPackage" cannot be specified.
* @example "some-framework.config.json"
*/
path: string;
path?: string;
/**
* A matcher
* A matcher for the entire file.
* If specified, "matchPackage" cannot be specified.
* @example "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"next\":\\s*\".+?\"[^}]*}"
*/
matchContent?: string;
/**
* A matcher for a package specifically found in a "package.json" file.
* If specified, "path" and "matchContext" cannot be specified.
* If specified in multiple detectors, the first one will be used to resolve the framework version.
* @example "\"(dev)?(d|D)ependencies\":\\s*{[^}]*\"next\":\\s*\".+?\"[^}]*}"
*/
matchPackage?: string;
}
export interface SettingPlaceholder {

View File

@@ -17,7 +17,7 @@ const SchemaFrameworkDetectionItem = {
items: [
{
type: 'object',
required: ['path'],
required: [],
additionalProperties: false,
properties: {
path: {
@@ -26,6 +26,9 @@ const SchemaFrameworkDetectionItem = {
matchContent: {
type: 'string',
},
matchPackage: {
type: 'string',
},
},
},
],

View File

@@ -1,4 +1,5 @@
import type { Framework, FrameworkDetectionItem } from '@vercel/frameworks';
import { spawnSync } from 'child_process';
import { DetectorFilesystem } from './detectors/filesystem';
interface BaseFramework {
@@ -11,49 +12,96 @@ export interface DetectFrameworkOptions {
frameworkList: readonly BaseFramework[];
}
async function matches(fs: DetectorFilesystem, framework: BaseFramework) {
export interface DetectFrameworkRecordOptions {
fs: DetectorFilesystem;
frameworkList: readonly Framework[];
}
type MatchResult = {
framework: BaseFramework;
detectedVersion?: string;
};
async function matches(
fs: DetectorFilesystem,
framework: BaseFramework
): Promise<MatchResult | undefined> {
const { detectors } = framework;
if (!detectors) {
return false;
return;
}
const { every, some } = detectors;
if (every !== undefined && !Array.isArray(every)) {
return false;
return;
}
if (some !== undefined && !Array.isArray(some)) {
return false;
return;
}
const check = async ({ path, matchContent }: FrameworkDetectionItem) => {
const check = async ({
path,
matchContent,
matchPackage,
}: FrameworkDetectionItem): Promise<MatchResult | undefined> => {
if (matchPackage && matchContent) {
throw new Error(
`Cannot specify "matchPackage" and "matchContent" in the same detector for "${framework.slug}"`
);
}
if (matchPackage && path) {
throw new Error(
`Cannot specify "matchPackage" and "path" in the same detector for "${framework.slug}" because "path" is assumed to be "package.json".`
);
}
if (!path && !matchPackage) {
throw new Error(
`Must specify either "path" or "matchPackage" in detector for "${framework.slug}".`
);
}
if (!path) {
return false;
path = 'package.json';
}
if (matchPackage) {
matchContent = `"(dev)?(d|D)ependencies":\\s*{[^}]*"${matchPackage}":\\s*"(.+?)"[^}]*}`;
}
if ((await fs.hasPath(path)) === false) {
return false;
return;
}
if (matchContent) {
if ((await fs.isFile(path)) === false) {
return false;
return;
}
const regex = new RegExp(matchContent, 'gm');
const regex = new RegExp(matchContent, 'm');
const content = await fs.readFile(path);
if (!regex.test(content.toString())) {
return false;
const match = content.toString().match(regex);
if (!match) {
return;
}
if (matchPackage && match[3]) {
return {
framework,
detectedVersion: match[3],
};
}
}
return true;
return {
framework,
};
};
const result: boolean[] = [];
const result: (MatchResult | undefined)[] = [];
if (every) {
const everyResult = await Promise.all(every.map(item => check(item)));
@@ -61,11 +109,12 @@ async function matches(fs: DetectorFilesystem, framework: BaseFramework) {
}
if (some) {
let someResult = false;
let someResult: MatchResult | undefined;
for (const item of some) {
if (await check(item)) {
someResult = true;
const itemResult = await check(item);
if (itemResult) {
someResult = itemResult;
break;
}
}
@@ -73,9 +122,20 @@ async function matches(fs: DetectorFilesystem, framework: BaseFramework) {
result.push(someResult);
}
return result.every(res => res === true);
if (!result.every(res => !!res)) {
return;
}
const detectedVersion = result.find(
r => typeof r === 'object' && r.detectedVersion
)?.detectedVersion;
return {
framework,
detectedVersion,
};
}
// TODO: Deprecate and replace with `detectFrameworkRecord`
export async function detectFramework({
fs,
frameworkList,
@@ -90,3 +150,75 @@ export async function detectFramework({
);
return result.find(res => res !== null) ?? null;
}
/**
* Framework with a `detectedVersion` specifying the version
* or version range of the relevant package
*/
type VersionedFramework = Framework & {
detectedVersion?: string;
};
// Note: Does not currently support a `frameworkList` of monorepo managers
export async function detectFrameworkRecord({
fs,
frameworkList,
}: DetectFrameworkRecordOptions): Promise<VersionedFramework | null> {
const result = await Promise.all(
frameworkList.map(async frameworkMatch => {
const matchResult = await matches(fs, frameworkMatch);
if (matchResult) {
return {
...frameworkMatch,
detectedVersion: matchResult?.detectedVersion,
};
}
return null;
})
);
const frameworkRecord = result.find(res => res !== null) ?? null;
return frameworkRecord;
}
export async function detectFrameworkVersion(
frameworkRecord: Framework
): Promise<string | undefined> {
const allDetectors = [
...(frameworkRecord.detectors?.every || []),
...(frameworkRecord.detectors?.some || []),
];
const firstMatchPackage = allDetectors.find(d => d.matchPackage);
if (!firstMatchPackage?.matchPackage) {
return;
}
const version = lookupInstalledVersion(
process.execPath,
firstMatchPackage.matchPackage
);
if (version) {
return version;
}
return;
}
function lookupInstalledVersion(
cwd: string,
packageName: string
): string | undefined {
try {
const script = `require('${packageName}/package.json').version`;
return spawnSync(cwd, ['-p', script], {
encoding: 'utf-8',
}).stdout.trim();
} catch (error) {
console.debug(
`Error looking up version of installed package "${packageName}": ${error}`
);
}
return;
}

View File

@@ -5,7 +5,11 @@ export {
detectApiExtensions,
} from './detect-builders';
export { detectFileSystemAPI } from './detect-file-system-api';
export { detectFramework } from './detect-framework';
export {
detectFramework,
detectFrameworkRecord,
detectFrameworkVersion,
} from './detect-framework';
export { getProjectPaths } from './get-project-paths';
export { DetectorFilesystem } from './detectors/filesystem';
export { LocalFileSystemDetector } from './detectors/local-file-system-detector';

View File

@@ -0,0 +1,194 @@
import frameworkList from '@vercel/frameworks';
import { detectFrameworkRecord } from '../src';
import VirtualFilesystem from './virtual-file-system';
describe('detectFrameworkRecord', () => {
it('Do not detect anything', async () => {
const fs = new VirtualFilesystem({
'README.md': '# hi',
'api/cheese.js': 'export default (req, res) => res.end("cheese");',
});
const framework = await detectFrameworkRecord({ fs, frameworkList });
expect(framework?.slug).toBe(undefined);
});
it('Detects a framework record with a matchPackage detector', async () => {
const fs = new VirtualFilesystem({
'package.json': JSON.stringify({
dependencies: {
next: '9.0.0',
},
}),
});
const frameworkRecord = await detectFrameworkRecord({ fs, frameworkList });
if (!frameworkRecord) {
throw new Error(
'`frameworkRecord` was not detected, expected "nextjs" frameworks object'
);
}
expect(frameworkRecord.slug).toBe('nextjs');
expect(frameworkRecord.name).toBe('Next.js');
expect(frameworkRecord.detectedVersion).toBe('9.0.0');
});
it('Detects a framework record with a matchPackage detector with slashes', async () => {
const fs = new VirtualFilesystem({
'package.json': JSON.stringify({
dependencies: {
'@ionic/angular': '5.0.0',
},
}),
});
const frameworkRecord = await detectFrameworkRecord({ fs, frameworkList });
if (!frameworkRecord) {
throw new Error(
'`frameworkRecord` was not detected, expected "ionic-angular" frameworks object'
);
}
expect(frameworkRecord.slug).toBe('ionic-angular');
expect(frameworkRecord.detectedVersion).toBe('5.0.0');
});
it('Detect first framework version found', async () => {
const fs = new VirtualFilesystem({
'package.json': JSON.stringify({
dependencies: {
nuxt: '1.0.0',
nuxt3: '2.0.0',
'nuxt3-edge': '3.0.0',
},
}),
});
const framework = await detectFrameworkRecord({ fs, frameworkList });
expect(framework?.slug).toBe('nuxtjs');
expect(framework?.detectedVersion).toBe('1.0.0');
});
it('Detect frameworks based on ascending order in framework list', async () => {
const fs = new VirtualFilesystem({
'package.json': JSON.stringify({
dependencies: {
next: '9.0.0',
gatsby: '4.18.0',
},
}),
});
const framework = await detectFrameworkRecord({ fs, frameworkList });
expect(framework?.slug).toBe('nextjs');
});
it('Detect Nuxt.js', async () => {
const fs = new VirtualFilesystem({
'package.json': JSON.stringify({
dependencies: {
nuxt: '1.0.0',
},
}),
});
const framework = await detectFrameworkRecord({ fs, frameworkList });
expect(framework?.slug).toBe('nuxtjs');
});
it('Detect Nuxt.js 3 edge', async () => {
const fs = new VirtualFilesystem({
'package.json': JSON.stringify({
dependencies: {
'nuxt3-edge': '1.0.0',
},
}),
});
const framework = await detectFrameworkRecord({ fs, frameworkList });
expect(framework?.slug).toBe('nuxtjs');
});
it('Detect Gatsby', async () => {
const fs = new VirtualFilesystem({
'package.json': JSON.stringify({
dependencies: {
gatsby: '1.0.0',
},
}),
});
const framework = await detectFrameworkRecord({ fs, frameworkList });
expect(framework?.slug).toBe('gatsby');
});
it('Detect Hugo #1', async () => {
const fs = new VirtualFilesystem({
'config.yaml': 'baseURL: http://example.org/',
'content/post.md': '# hello world',
});
const framework = await detectFrameworkRecord({ fs, frameworkList });
expect(framework?.slug).toBe('hugo');
});
it('Detect Hugo #2', async () => {
const fs = new VirtualFilesystem({
'config.json': '{ "baseURL": "http://example.org/" }',
'content/post.md': '# hello world',
});
const framework = await detectFrameworkRecord({ fs, frameworkList });
expect(framework?.slug).toBe('hugo');
});
it('Detect Hugo #3', async () => {
const fs = new VirtualFilesystem({
'config.toml': 'baseURL = "http://example.org/"',
'content/post.md': '# hello world',
});
const framework = await detectFrameworkRecord({ fs, frameworkList });
expect(framework?.slug).toBe('hugo');
});
it('Detect Jekyll', async () => {
const fs = new VirtualFilesystem({
'_config.yml': 'config',
});
const framework = await detectFrameworkRecord({ fs, frameworkList });
expect(framework?.slug).toBe('jekyll');
});
it('Detect Middleman', async () => {
const fs = new VirtualFilesystem({
'config.rb': 'config',
});
const framework = await detectFrameworkRecord({ fs, frameworkList });
expect(framework?.slug).toBe('middleman');
});
it('Detect Scully', async () => {
const fs = new VirtualFilesystem({
'package.json': JSON.stringify({
dependencies: {
'@angular/cli': 'latest',
'@scullyio/init': 'latest',
},
}),
});
const framework = await detectFrameworkRecord({ fs, frameworkList });
expect(framework?.slug).toBe('scully');
});
it('Detect Zola', async () => {
const fs = new VirtualFilesystem({
'config.toml': 'base_url = "/"',
});
const framework = await detectFrameworkRecord({ fs, frameworkList });
expect(framework?.slug).toBe('zola');
});
});

View File

@@ -1,131 +1,7 @@
import path from 'path';
import frameworkList from '@vercel/frameworks';
import workspaceManagers from '../src/workspaces/workspace-managers';
import { detectFramework, DetectorFilesystem } from '../src';
import { DetectorFilesystemStat } from '../src/detectors/filesystem';
const posixPath = path.posix;
class VirtualFilesystem extends DetectorFilesystem {
private files: Map<string, Buffer>;
private cwd: string;
constructor(files: { [key: string]: string | Buffer }, cwd = '') {
super();
this.files = new Map();
this.cwd = cwd;
Object.entries(files).map(([key, value]) => {
const buffer = typeof value === 'string' ? Buffer.from(value) : value;
this.files.set(key, buffer);
});
}
private _normalizePath(rawPath: string): string {
return posixPath.normalize(rawPath);
}
async _hasPath(name: string): Promise<boolean> {
const basePath = this._normalizePath(posixPath.join(this.cwd, name));
for (const file of this.files.keys()) {
if (file.startsWith(basePath)) {
return true;
}
}
return false;
}
async _isFile(name: string): Promise<boolean> {
const basePath = this._normalizePath(posixPath.join(this.cwd, name));
return this.files.has(basePath);
}
async _readFile(name: string): Promise<Buffer> {
const basePath = this._normalizePath(posixPath.join(this.cwd, name));
const file = this.files.get(basePath);
if (file === undefined) {
throw new Error('File does not exist');
}
if (typeof file === 'string') {
return Buffer.from(file);
}
return file;
}
/**
* An example of how to implement readdir for a virtual filesystem.
*/
async _readdir(name = '/'): Promise<DetectorFilesystemStat[]> {
return (
[...this.files.keys()]
.map(filepath => {
const basePath = this._normalizePath(
posixPath.join(this.cwd, name === '/' ? '' : name)
);
const fileDirectoryName = posixPath.dirname(filepath);
if (fileDirectoryName === basePath) {
return {
name: posixPath.basename(filepath),
path: filepath.replace(
this.cwd === '' ? this.cwd : `${this.cwd}/`,
''
),
type: 'file',
};
}
if (
(basePath === '.' && fileDirectoryName !== '.') ||
fileDirectoryName.startsWith(basePath)
) {
let subDirectoryName = fileDirectoryName.replace(
basePath === '.' ? '' : `${basePath}/`,
''
);
if (subDirectoryName.includes('/')) {
subDirectoryName = subDirectoryName.split('/')[0];
}
return {
name: subDirectoryName,
path:
name === '/'
? subDirectoryName
: this._normalizePath(posixPath.join(name, subDirectoryName)),
type: 'dir',
};
}
return null;
})
// remove nulls
.filter((stat): stat is DetectorFilesystemStat => stat !== null)
// remove duplicates
.filter(
(stat, index, self) =>
index ===
self.findIndex(s => s.name === stat.name && s.path === stat.path)
)
);
}
/**
* An example of how to implement chdir for a virtual filesystem.
*/
_chdir(name: string): DetectorFilesystem {
const basePath = this._normalizePath(posixPath.join(this.cwd, name));
const files = Object.fromEntries(
[...this.files.keys()].map(key => [key, this.files.get(key) ?? ''])
);
return new VirtualFilesystem(files, basePath);
}
}
import { detectFramework } from '../src';
import VirtualFilesystem from './virtual-file-system';
describe('DetectorFilesystem', () => {
it('should return the directory contents relative to the cwd', async () => {

View File

@@ -0,0 +1,126 @@
import path from 'path';
import { DetectorFilesystem } from '../src';
import { DetectorFilesystemStat } from '../src/detectors/filesystem';
const posixPath = path.posix;
export default class VirtualFilesystem extends DetectorFilesystem {
private files: Map<string, Buffer>;
private cwd: string;
constructor(files: { [key: string]: string | Buffer }, cwd = '') {
super();
this.files = new Map();
this.cwd = cwd;
Object.entries(files).map(([key, value]) => {
const buffer = typeof value === 'string' ? Buffer.from(value) : value;
this.files.set(key, buffer);
});
}
private _normalizePath(rawPath: string): string {
return posixPath.normalize(rawPath);
}
async _hasPath(name: string): Promise<boolean> {
const basePath = this._normalizePath(posixPath.join(this.cwd, name));
for (const file of this.files.keys()) {
if (file.startsWith(basePath)) {
return true;
}
}
return false;
}
async _isFile(name: string): Promise<boolean> {
const basePath = this._normalizePath(posixPath.join(this.cwd, name));
return this.files.has(basePath);
}
async _readFile(name: string): Promise<Buffer> {
const basePath = this._normalizePath(posixPath.join(this.cwd, name));
const file = this.files.get(basePath);
if (file === undefined) {
throw new Error('File does not exist');
}
if (typeof file === 'string') {
return Buffer.from(file);
}
return file;
}
/**
* An example of how to implement readdir for a virtual filesystem.
*/
async _readdir(name = '/'): Promise<DetectorFilesystemStat[]> {
return (
[...this.files.keys()]
.map(filepath => {
const basePath = this._normalizePath(
posixPath.join(this.cwd, name === '/' ? '' : name)
);
const fileDirectoryName = posixPath.dirname(filepath);
if (fileDirectoryName === basePath) {
return {
name: posixPath.basename(filepath),
path: filepath.replace(
this.cwd === '' ? this.cwd : `${this.cwd}/`,
''
),
type: 'file',
};
}
if (
(basePath === '.' && fileDirectoryName !== '.') ||
fileDirectoryName.startsWith(basePath)
) {
let subDirectoryName = fileDirectoryName.replace(
basePath === '.' ? '' : `${basePath}/`,
''
);
if (subDirectoryName.includes('/')) {
subDirectoryName = subDirectoryName.split('/')[0];
}
return {
name: subDirectoryName,
path:
name === '/'
? subDirectoryName
: this._normalizePath(posixPath.join(name, subDirectoryName)),
type: 'dir',
};
}
return null;
})
// remove nulls
.filter((stat): stat is DetectorFilesystemStat => stat !== null)
// remove duplicates
.filter(
(stat, index, self) =>
index ===
self.findIndex(s => s.name === stat.name && s.path === stat.path)
)
);
}
/**
* An example of how to implement chdir for a virtual filesystem.
*/
_chdir(name: string): DetectorFilesystem {
const basePath = this._normalizePath(posixPath.join(this.cwd, name));
const files = Object.fromEntries(
[...this.files.keys()].map(key => [key, this.files.get(key) ?? ''])
);
return new VirtualFilesystem(files, basePath);
}
}

View File

@@ -932,6 +932,7 @@ export const build: BuildV2 = async ({
]
: []),
],
framework: { version: nextVersion },
};
}
@@ -2581,6 +2582,7 @@ export const build: BuildV2 = async ({
]),
]),
],
framework: { version: nextVersion },
};
};

View File

@@ -1755,5 +1755,6 @@ export async function serverBuild({
},
]),
],
framework: { version: nextVersion },
};
}

View File

@@ -10,7 +10,6 @@ import {
getSpawnOptions,
glob,
NodejsLambda,
readConfigFile,
runNpmInstall,
runPackageJsonScript,
scanParentDirs,
@@ -69,6 +68,8 @@ export const build: BuildV2 = async ({
env: spawnOpts.env || {},
});
let packageJson: PackageJson | undefined;
// Ensure `@remix-run/vercel` is in the project's `package.json`
const packageJsonPath = await walkParentDirs({
base: repoRootPath,
@@ -76,9 +77,9 @@ export const build: BuildV2 = async ({
filename: 'package.json',
});
if (packageJsonPath) {
const packageJson: PackageJson = JSON.parse(
await fs.readFile(packageJsonPath, 'utf8')
);
packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
}
if (packageJsonPath && packageJson) {
const { dependencies = {}, devDependencies = {} } = packageJson;
let modified = false;
@@ -139,17 +140,14 @@ export const build: BuildV2 = async ({
cwd: entrypointFsDirname,
});
} else {
const pkg = await readConfigFile<PackageJson>(
join(entrypointFsDirname, 'package.json')
);
if (hasScript('vercel-build', pkg)) {
if (hasScript('vercel-build', packageJson)) {
debug(`Executing "yarn vercel-build"`);
await runPackageJsonScript(
entrypointFsDirname,
'vercel-build',
spawnOpts
);
} else if (hasScript('build', pkg)) {
} else if (hasScript('build', packageJson)) {
debug(`Executing "yarn build"`);
await runPackageJsonScript(entrypointFsDirname, 'build', spawnOpts);
} else {
@@ -212,6 +210,13 @@ export const build: BuildV2 = async ({
),
]);
let framework: { version: string } | undefined;
if (packageJson?.version) {
framework = {
version: packageJson.version,
};
}
return {
routes: [
{
@@ -231,10 +236,11 @@ export const build: BuildV2 = async ({
render: renderFunction,
...staticFiles,
},
framework,
};
};
function hasScript(scriptName: string, pkg: PackageJson | null) {
function hasScript(scriptName: string, pkg: PackageJson | undefined) {
const scripts = (pkg && pkg.scripts) || {};
return typeof scripts[scriptName] === 'string';
}