From 7a4dfbb24ee16460a3f3b9b6441821d00aa4b135 Mon Sep 17 00:00:00 2001 From: Philippe Serhal Date: Wed, 28 Aug 2024 17:11:52 -0400 Subject: [PATCH] fix(build-info): fix detection of Remix sites --- package-lock.json | 4 +- .../build-info/src/frameworks/framework.ts | 25 +++++- packages/build-info/src/frameworks/index.ts | 2 + .../src/frameworks/remix-classic.test.ts | 84 +++++++++++++++++++ .../src/frameworks/remix-classic.ts | 44 ++++++++++ .../build-info/src/frameworks/remix.test.ts | 84 +++++++++++++++++++ packages/build-info/src/frameworks/remix.ts | 26 ++++-- packages/build-info/src/frameworks/vite.ts | 3 + packages/build-info/src/project.ts | 1 + 9 files changed, 265 insertions(+), 8 deletions(-) create mode 100644 packages/build-info/src/frameworks/remix-classic.test.ts create mode 100644 packages/build-info/src/frameworks/remix-classic.ts create mode 100644 packages/build-info/src/frameworks/remix.test.ts diff --git a/package-lock.json b/package-lock.json index 468c93e4ad..6c5a4a2041 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26294,7 +26294,7 @@ "chalk": "^5.0.0", "clean-stack": "^4.0.0", "execa": "^6.0.0", - "fdir": "^6.3.0", + "fdir": "^6.0.1", "figures": "^5.0.0", "filter-obj": "^5.0.0", "got": "^12.0.0", @@ -27111,7 +27111,7 @@ "dependencies": { "execa": "^6.0.0", "map-obj": "^5.0.0", - "micromatch": "^4.0.8", + "micromatch": "^4.0.2", "moize": "^6.1.3", "path-exists": "^5.0.0" }, diff --git a/packages/build-info/src/frameworks/framework.ts b/packages/build-info/src/frameworks/framework.ts index c31311b900..fbdfa17ca2 100644 --- a/packages/build-info/src/frameworks/framework.ts +++ b/packages/build-info/src/frameworks/framework.ts @@ -45,8 +45,21 @@ export interface Framework { id: string name: string category: Category + /** + * If this is set, at least ONE of these must exist, anywhere in the project + */ configFiles: string[] + /** + * if this is set, NONE of these must exist, anywhere in the project + */ + excludedConfigFiles: string[] + /** + * If this is set, at least ONE of these must be present in the `package.json` `dependencies|devDependencies` + */ npmDependencies: string[] + /** + * if this is set, NONE of these must be present in the `package.json` `dependencies|devDependencies` + */ excludedNpmDependencies?: string[] version?: SemVer /** Information on how it was detected and how accurate the detection is */ @@ -145,6 +158,7 @@ export abstract class BaseFramework implements Framework { detected?: Detection version?: SemVer configFiles: string[] = [] + excludedConfigFiles: string[] = [] npmDependencies: string[] = [] excludedNpmDependencies: string[] = [] plugins: string[] = [] @@ -256,6 +270,14 @@ export abstract class BaseFramework implements Framework { /** detect if the framework config file is located somewhere up the tree */ private async detectConfigFile(): Promise { + if (this.excludedConfigFiles?.length) { + const config = await this.project.fs.findUp(this.excludedConfigFiles, { + cwd: this.path || this.project.baseDirectory, + stopAt: this.project.root, + }) + console.log('hello', config) + if (config) return + } if (this.configFiles?.length) { const config = await this.project.fs.findUp(this.configFiles, { cwd: this.path || this.project.baseDirectory, @@ -275,9 +297,10 @@ export abstract class BaseFramework implements Framework { /** * Checks if the project is using a specific framework: - * - if `npmDependencies` is set, one of them must be present in then `package.json` `dependencies|devDependencies` + * - if `npmDependencies` is set, one of them must be present in the `package.json` `dependencies|devDependencies` * - if `excludedNpmDependencies` is set, none of them must be present in the `package.json` `dependencies|devDependencies` * - if `configFiles` is set, one of the files must exist + * - if `excludedConfigFiles` is set, none of the files must exist */ async detect(): Promise { // we can force frameworks as well diff --git a/packages/build-info/src/frameworks/index.ts b/packages/build-info/src/frameworks/index.ts index 624ddd522b..5b14fa43aa 100644 --- a/packages/build-info/src/frameworks/index.ts +++ b/packages/build-info/src/frameworks/index.ts @@ -31,6 +31,7 @@ import { Qwik } from './qwik.js' import { ReactStatic } from './react-static.js' import { CreateReactApp } from './react.js' import { RedwoodJS } from './redwoodjs.js' +import { RemixClassic } from './remix-classic.js' import { Remix } from './remix.js' import { Roots } from './roots.js' import { Sapper } from './sapper.js' @@ -66,6 +67,7 @@ export const frameworks = [ ReactStatic, RedwoodJS, Remix, + RemixClassic, Solid, SolidStart, Stencil, diff --git a/packages/build-info/src/frameworks/remix-classic.test.ts b/packages/build-info/src/frameworks/remix-classic.test.ts new file mode 100644 index 0000000000..2071c7c599 --- /dev/null +++ b/packages/build-info/src/frameworks/remix-classic.test.ts @@ -0,0 +1,84 @@ +import { beforeEach, expect, test } from 'vitest' + +import { mockFileSystem } from '../../tests/mock-file-system.js' +import { NodeFS } from '../node/file-system.js' +import { Project } from '../project.js' + +beforeEach((ctx) => { + ctx.fs = new NodeFS() +}) + +test('detects a Remix Classic Compiler site with origin SSR as remix-classic', async ({ fs }) => { + const cwd = mockFileSystem({ + 'remix.config.js': '', + 'package.json': JSON.stringify({ + scripts: { + build: 'remix build', + dev: 'remix dev', + start: 'netlify serve', + typecheck: 'tsc', + }, + dependencies: { + '@netlify/functions': '^2.8.1', + '@remix-run/css-bundle': '^2.9.2', + '@remix-run/node': '^2.9.2', + '@remix-run/react': '^2.9.2', + react: '^18.2.0', + 'react-dom': '^18.2.0', + }, + devDependencies: { + '@netlify/remix-adapter': '^2.4.0', + '@remix-run/dev': '^2.9.2', + '@remix-run/serve': '^2.9.2', + }, + }), + }) + const detected = await new Project(fs, cwd).detectFrameworks() + + const detectedFrameworks = (detected ?? []).map((framework) => framework.id) + expect(detectedFrameworks).not.toContain('remix') + expect(detectedFrameworks).not.toContain('vite') + + expect(detected?.[0].id).toBe('remix-classic') + expect(detected?.[0].build.command).toBe('remix build') + expect(detected?.[0].build.directory).toBe('public') + expect(detected?.[0].dev?.command).toBe('remix watch') +}) + +test('detects a Remix Classic Compiler site with edge SSR as remix-classic', async ({ fs }) => { + const cwd = mockFileSystem({ + 'remix.config.js': '', + 'package.json': JSON.stringify({ + scripts: { + build: 'remix build', + dev: 'remix dev', + start: 'netlify serve', + typecheck: 'tsc', + }, + dependencies: { + '@netlify/functions': '^2.8.1', + '@netlify/remix-runtime': '^2.3.0', + '@remix-run/css-bundle': '^2.9.2', + '@remix-run/node': '^2.9.2', + '@remix-run/react': '^2.9.2', + react: '^18.2.0', + 'react-dom': '^18.2.0', + }, + devDependencies: { + '@netlify/remix-edge-adapter': '^3.3.0', + '@remix-run/dev': '^2.9.2', + '@remix-run/serve': '^2.9.2', + }, + }), + }) + const detected = await new Project(fs, cwd).detectFrameworks() + + const detectedFrameworks = (detected ?? []).map((framework) => framework.id) + expect(detectedFrameworks).not.toContain('remix') + expect(detectedFrameworks).not.toContain('vite') + + expect(detected?.[0].id).toBe('remix-classic') + expect(detected?.[0].build.command).toBe('remix build') + expect(detected?.[0].build.directory).toBe('public') + expect(detected?.[0].dev?.command).toBe('remix watch') +}) diff --git a/packages/build-info/src/frameworks/remix-classic.ts b/packages/build-info/src/frameworks/remix-classic.ts new file mode 100644 index 0000000000..b078ba59ca --- /dev/null +++ b/packages/build-info/src/frameworks/remix-classic.ts @@ -0,0 +1,44 @@ +import { BaseFramework, Category, Framework } from './framework.js' + +export class RemixClassic extends BaseFramework implements Framework { + readonly id = 'remix-classic' + name = 'Remix (Classic Compiler)' + npmDependencies = [ + '@remix-run/react', + '@remix-run/dev', + '@remix-run/server-runtime', + '@netlify/remix-adapter', + '@netlify/remix-edge-adapter', + '@netlify/remix-runtime', + // Deprecated package name (deprecated in 1.6, removed in 2.0) + 'remix', + // Deprecated Netlify packages + '@remix-run/netlify', + '@remix-run/netlify-edge', + ] + configFiles = ['remix.config.js'] + excludedConfigFiles = [ + 'vite.config.js', + 'vite.config.mjs', + 'vite.config.cjs', + 'vite.config.ts', + 'vite.config.mts', + 'vite.config.cts', + ] + category = Category.SSG + + dev = { + command: 'remix watch', + } + + build = { + command: 'remix build', + directory: 'public', + } + + logo = { + default: '/logos/remix/default.svg', + light: '/logos/remix/light.svg', + dark: '/logos/remix/dark.svg', + } +} diff --git a/packages/build-info/src/frameworks/remix.test.ts b/packages/build-info/src/frameworks/remix.test.ts new file mode 100644 index 0000000000..e5ef8fbc12 --- /dev/null +++ b/packages/build-info/src/frameworks/remix.test.ts @@ -0,0 +1,84 @@ +import { beforeEach, expect, test } from 'vitest' + +import { mockFileSystem } from '../../tests/mock-file-system.js' +import { NodeFS } from '../node/file-system.js' +import { Project } from '../project.js' + +beforeEach((ctx) => { + ctx.fs = new NodeFS() +}) + +test('detects a Remix Vite site with origin SSR as remix', async ({ fs }) => { + const cwd = mockFileSystem({ + 'vite.config.js': '', + 'package.json': JSON.stringify({ + scripts: { + build: 'remix vite:build', + dev: 'remix vite:dev', + start: 'remix-serve ./build/server/index.js', + }, + dependencies: { + '@remix-run/node': '^2.9.2', + '@remix-run/react': '^2.9.2', + '@remix-run/serve': '^2.9.2', + react: '^18.2.0', + 'react-dom': '^18.2.0', + }, + devDependencies: { + '@netlify/remix-adapter': '^2.4.0', + '@remix-run/dev': '^2.9.2', + '@types/react': '^18.2.20', + '@types/react-dom': '^18.2.7', + vite: '^5.0.0', + 'vite-tsconfig-paths': '^4.2.1', + }, + }), + }) + const detected = await new Project(fs, cwd).detectFrameworks() + expect(detected?.[0].id).toBe('remix') + expect(detected?.[0].build.command).toBe('remix vite:build') + expect(detected?.[0].build.directory).toBe('dist') + expect(detected?.[0].dev?.command).toBe('remix vite:dev') + + const detectedFrameworks = (detected ?? []).map((framework) => framework.id) + expect(detectedFrameworks).not.toContain('remix-classic') + expect(detectedFrameworks).not.toContain('vite') +}) + +test('detects a Remix Vite site with edge SSR as remix', async ({ fs }) => { + const cwd = mockFileSystem({ + 'vite.config.js': '', + 'package.json': JSON.stringify({ + scripts: { + build: 'remix vite:build', + dev: 'remix vite:dev', + start: 'remix-serve ./build/server/index.js', + }, + dependencies: { + '@netlify/remix-runtime': '^2.3.0', + '@remix-run/node': '^2.9.2', + '@remix-run/react': '^2.9.2', + '@remix-run/serve': '^2.9.2', + react: '^18.2.0', + 'react-dom': '^18.2.0', + }, + devDependencies: { + '@netlify/remix-edge-adapter': '^3.3.0', + '@remix-run/dev': '^2.9.2', + '@types/react': '^18.2.20', + '@types/react-dom': '^18.2.7', + vite: '^5.0.0', + 'vite-tsconfig-paths': '^4.2.1', + }, + }), + }) + const detected = await new Project(fs, cwd).detectFrameworks() + expect(detected?.[0].id).toBe('remix') + expect(detected?.[0].build.command).toBe('remix vite:build') + expect(detected?.[0].build.directory).toBe('dist') + expect(detected?.[0].dev?.command).toBe('remix vite:dev') + + const detectedFrameworks = (detected ?? []).map((framework) => framework.id) + expect(detectedFrameworks).not.toContain('remix-classic') + expect(detectedFrameworks).not.toContain('vite') +}) diff --git a/packages/build-info/src/frameworks/remix.ts b/packages/build-info/src/frameworks/remix.ts index 9ad2ceeb6a..be4bca7373 100644 --- a/packages/build-info/src/frameworks/remix.ts +++ b/packages/build-info/src/frameworks/remix.ts @@ -3,17 +3,33 @@ import { BaseFramework, Category, Framework } from './framework.js' export class Remix extends BaseFramework implements Framework { readonly id = 'remix' name = 'Remix' - npmDependencies = ['remix', '@remix-run/netlify', '@remix-run/netlify-edge'] - configFiles = ['remix.config.js'] + npmDependencies = [ + '@remix-run/react', + '@remix-run/dev', + '@remix-run/server-runtime', + '@netlify/remix-adapter', + '@netlify/remix-edge-adapter', + '@netlify/remix-runtime', + ] + configFiles = [ + 'vite.config.js', + 'vite.config.mjs', + 'vite.config.cjs', + 'vite.config.ts', + 'vite.config.mts', + 'vite.config.cts', + ] + excludedConfigFiles = ['remix.config.js'] category = Category.SSG dev = { - command: 'remix watch', + command: 'remix vite:dev', + port: 5173, } build = { - command: 'remix build', - directory: 'public', + command: 'remix vite:build', + directory: 'dist', } logo = { diff --git a/packages/build-info/src/frameworks/vite.ts b/packages/build-info/src/frameworks/vite.ts index 2ee26ef80f..52f2a258c1 100644 --- a/packages/build-info/src/frameworks/vite.ts +++ b/packages/build-info/src/frameworks/vite.ts @@ -5,6 +5,9 @@ export class Vite extends BaseFramework implements Framework { name = 'Vite' npmDependencies = ['vite'] excludedNpmDependencies = [ + '@remix-run/react', + '@remix-run/dev', + '@remix-run/server-runtime', '@shopify/hydrogen', '@builder.io/qwik', 'solid-start', diff --git a/packages/build-info/src/project.ts b/packages/build-info/src/project.ts index a528c047b4..50fb552f97 100644 --- a/packages/build-info/src/project.ts +++ b/packages/build-info/src/project.ts @@ -337,6 +337,7 @@ export class Project { // 2. only a config file was specified and matched // 3. an npm dependency was specified but matched over the config file (least accurate) // and prefer SSG over build tools + this.logger.debug(`[project.ts]: ${JSON.stringify(detected, null, 2)}`) return filterByRelevance(detected) } catch { return []