diff --git a/packages/remix/plugin.ts b/packages/remix/plugin.ts index 649b4f675f..f4e3c95ef5 100644 --- a/packages/remix/plugin.ts +++ b/packages/remix/plugin.ts @@ -1,5 +1,6 @@ export { createNodes, + createNodesV2, createDependencies, RemixPluginOptions, } from './src/plugins/plugin'; diff --git a/packages/remix/src/generators/init/init.spec.ts b/packages/remix/src/generators/init/init.spec.ts index 92b6626577..b75cc510ed 100644 --- a/packages/remix/src/generators/init/init.spec.ts +++ b/packages/remix/src/generators/init/init.spec.ts @@ -39,6 +39,7 @@ describe('Remix Init Generator', () => { "options": { "buildTargetName": "build", "devTargetName": "dev", + "serveStaticTargetName": "serve-static", "startTargetName": "start", "typecheckTargetName": "typecheck", }, diff --git a/packages/remix/src/generators/init/init.ts b/packages/remix/src/generators/init/init.ts index 2f2c9e591d..edbd94bf2c 100644 --- a/packages/remix/src/generators/init/init.ts +++ b/packages/remix/src/generators/init/init.ts @@ -8,10 +8,10 @@ import { createProjectGraphAsync, } from '@nx/devkit'; import { - addPluginV1, + addPlugin, generateCombinations, } from '@nx/devkit/src/utils/add-plugin'; -import { createNodes } from '../../plugins/plugin'; +import { createNodesV2 } from '../../plugins/plugin'; import { nxVersion, remixVersion } from '../../utils/versions'; import { type Schema } from './schema'; @@ -44,11 +44,11 @@ export async function remixInitGeneratorInternal(tree: Tree, options: Schema) { nxJson.useInferencePlugins !== false; options.addPlugin ??= addPluginDefault; if (options.addPlugin) { - await addPluginV1( + await addPlugin( tree, await createProjectGraphAsync(), '@nx/remix/plugin', - createNodes, + createNodesV2, { startTargetName: ['start', 'remix:start', 'remix-start'], buildTargetName: ['build', 'remix:build', 'remix-build'], @@ -58,6 +58,11 @@ export async function remixInitGeneratorInternal(tree: Tree, options: Schema) { 'remix:typecheck', 'remix-typecheck', ], + serveStaticTargetName: [ + 'serve-static', + 'vite:serve-static', + 'vite-serve-static', + ], }, options.updatePackageScripts ); diff --git a/packages/remix/src/plugins/__snapshots__/plugin.spec.ts.snap b/packages/remix/src/plugins/__snapshots__/plugin.spec.ts.snap index 2de78a52b6..8d57192912 100644 --- a/packages/remix/src/plugins/__snapshots__/plugin.spec.ts.snap +++ b/packages/remix/src/plugins/__snapshots__/plugin.spec.ts.snap @@ -1,169 +1,359 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`@nx/remix/plugin non-root project should create nodes 1`] = ` -{ - "projects": { - "my-app": { - "root": "my-app", - "targets": { - "build": { - "cache": true, - "command": "remix build", - "dependsOn": [ - "^build", - ], - "inputs": [ - "production", - "^production", - { - "externalDependencies": [ - "@remix-run/dev", +exports[`@nx/remix/plugin Remix Classic Compiler non-root project should create nodes 1`] = ` +[ + [ + "my-app/remix.config.cjs", + { + "projects": { + "my-app": { + "metadata": {}, + "root": "my-app", + "targets": { + "build": { + "cache": true, + "command": "remix build", + "dependsOn": [ + "^build", + ], + "inputs": [ + "production", + "^production", + { + "externalDependencies": [ + "@remix-run/dev", + ], + }, + ], + "options": { + "cwd": "my-app", + }, + "outputs": [ + "{workspaceRoot}/my-app/build", + "{workspaceRoot}/my-app/public/build", ], }, - ], - "options": { - "cwd": "my-app", - }, - "outputs": [ - "{workspaceRoot}/my-app/build", - "{workspaceRoot}/my-app/public/build", - ], - }, - "dev": { - "command": "remix dev --manual", - "options": { - "cwd": "my-app", - }, - }, - "serve-static": { - "command": "remix-serve build/index.js", - "dependsOn": [ - "build", - ], - "options": { - "cwd": "my-app", - }, - }, - "start": { - "command": "remix-serve build/index.js", - "dependsOn": [ - "build", - ], - "options": { - "cwd": "my-app", - }, - }, - "static-serve": { - "command": "remix-serve build/index.js", - "dependsOn": [ - "build", - ], - "options": { - "cwd": "my-app", - }, - }, - "tsc": { - "cache": true, - "command": "tsc", - "inputs": [ - "production", - "^production", - { - "externalDependencies": [ - "typescript", - ], + "dev": { + "command": "remix dev --manual", + "options": { + "cwd": "my-app", + }, + }, + "serve-static": { + "command": "remix-serve build/index.js", + "dependsOn": [ + "build", + ], + "options": { + "cwd": "my-app", + }, + }, + "start": { + "command": "remix-serve build/index.js", + "dependsOn": [ + "build", + ], + "options": { + "cwd": "my-app", + }, + }, + "static-serve": { + "command": "remix-serve build/index.js", + "dependsOn": [ + "build", + ], + "options": { + "cwd": "my-app", + }, + }, + "tsc": { + "cache": true, + "command": "tsc", + "inputs": [ + "production", + "^production", + { + "externalDependencies": [ + "typescript", + ], + }, + ], + "options": { + "cwd": "my-app", + }, }, - ], - "options": { - "cwd": "my-app", }, }, }, }, - }, -} + ], +] `; -exports[`@nx/remix/plugin root project should create nodes 1`] = ` -{ - "projects": { - ".": { - "root": ".", - "targets": { - "build": { - "cache": true, - "command": "remix build", - "dependsOn": [ - "^build", - ], - "inputs": [ - "production", - "^production", - { - "externalDependencies": [ - "@remix-run/dev", +exports[`@nx/remix/plugin Remix Classic Compiler root project should create nodes 1`] = ` +[ + [ + "remix.config.cjs", + { + "projects": { + ".": { + "metadata": {}, + "root": ".", + "targets": { + "build": { + "cache": true, + "command": "remix build", + "dependsOn": [ + "^build", + ], + "inputs": [ + "production", + "^production", + { + "externalDependencies": [ + "@remix-run/dev", + ], + }, + ], + "options": { + "cwd": ".", + }, + "outputs": [ + "{workspaceRoot}/build", + "{workspaceRoot}/public/build", ], }, - ], - "options": { - "cwd": ".", - }, - "outputs": [ - "{workspaceRoot}/build", - "{workspaceRoot}/public/build", - ], - }, - "dev": { - "command": "remix dev --manual", - "options": { - "cwd": ".", - }, - }, - "serve-static": { - "command": "remix-serve build/index.js", - "dependsOn": [ - "build", - ], - "options": { - "cwd": ".", - }, - }, - "start": { - "command": "remix-serve build/index.js", - "dependsOn": [ - "build", - ], - "options": { - "cwd": ".", - }, - }, - "static-serve": { - "command": "remix-serve build/index.js", - "dependsOn": [ - "build", - ], - "options": { - "cwd": ".", - }, - }, - "typecheck": { - "cache": true, - "command": "tsc", - "inputs": [ - "production", - "^production", - { - "externalDependencies": [ - "typescript", - ], + "dev": { + "command": "remix dev --manual", + "options": { + "cwd": ".", + }, + }, + "serve-static": { + "command": "remix-serve build/index.js", + "dependsOn": [ + "build", + ], + "options": { + "cwd": ".", + }, + }, + "start": { + "command": "remix-serve build/index.js", + "dependsOn": [ + "build", + ], + "options": { + "cwd": ".", + }, + }, + "static-serve": { + "command": "remix-serve build/index.js", + "dependsOn": [ + "build", + ], + "options": { + "cwd": ".", + }, + }, + "typecheck": { + "cache": true, + "command": "tsc", + "inputs": [ + "production", + "^production", + { + "externalDependencies": [ + "typescript", + ], + }, + ], + "options": { + "cwd": ".", + }, }, - ], - "options": { - "cwd": ".", }, }, }, }, - }, -} + ], +] +`; + +exports[`@nx/remix/plugin Remix Vite Compiler non-root project should create nodes 1`] = ` +[ + [ + "my-app/vite.config.js", + { + "projects": { + "my-app": { + "metadata": {}, + "root": "my-app", + "targets": { + "build": { + "cache": true, + "command": "remix vite:build", + "dependsOn": [ + "^build", + ], + "inputs": [ + "production", + "^production", + { + "externalDependencies": [ + "@remix-run/dev", + ], + }, + ], + "options": { + "cwd": "my-app", + }, + "outputs": [ + "{workspaceRoot}/my-app/build", + ], + }, + "dev": { + "command": "remix vite:dev", + "options": { + "cwd": "my-app", + }, + }, + "serve-static": { + "command": "remix-serve build/server/index.js", + "dependsOn": [ + "build", + ], + "options": { + "cwd": "my-app", + }, + }, + "start": { + "command": "remix-serve build/server/index.js", + "dependsOn": [ + "build", + ], + "options": { + "cwd": "my-app", + }, + }, + "static-serve": { + "command": "remix-serve build/server/index.js", + "dependsOn": [ + "build", + ], + "options": { + "cwd": "my-app", + }, + }, + "tsc": { + "cache": true, + "command": "tsc", + "inputs": [ + "production", + "^production", + { + "externalDependencies": [ + "typescript", + ], + }, + ], + "options": { + "cwd": "my-app", + }, + }, + }, + }, + }, + }, + ], +] +`; + +exports[`@nx/remix/plugin Remix Vite Compiler root project should create nodes 1`] = ` +[ + [ + "vite.config.js", + { + "projects": { + ".": { + "metadata": {}, + "root": ".", + "targets": { + "build": { + "cache": true, + "command": "remix vite:build", + "dependsOn": [ + "^build", + ], + "inputs": [ + "production", + "^production", + { + "externalDependencies": [ + "@remix-run/dev", + ], + }, + ], + "options": { + "cwd": ".", + }, + "outputs": [ + "{workspaceRoot}/build", + ], + }, + "dev": { + "command": "remix vite:dev", + "options": { + "cwd": ".", + }, + }, + "serve-static": { + "command": "remix-serve build/server/index.js", + "dependsOn": [ + "build", + ], + "options": { + "cwd": ".", + }, + }, + "start": { + "command": "remix-serve build/server/index.js", + "dependsOn": [ + "build", + ], + "options": { + "cwd": ".", + }, + }, + "static-serve": { + "command": "remix-serve build/server/index.js", + "dependsOn": [ + "build", + ], + "options": { + "cwd": ".", + }, + }, + "typecheck": { + "cache": true, + "command": "tsc", + "inputs": [ + "production", + "^production", + { + "externalDependencies": [ + "typescript", + ], + }, + ], + "options": { + "cwd": ".", + }, + }, + }, + }, + }, + }, + ], +] `; diff --git a/packages/remix/src/plugins/plugin.spec.ts b/packages/remix/src/plugins/plugin.spec.ts index 4192b4e206..57f233c7aa 100644 --- a/packages/remix/src/plugins/plugin.spec.ts +++ b/packages/remix/src/plugins/plugin.spec.ts @@ -1,48 +1,56 @@ import { type CreateNodesContext, joinPathFragments } from '@nx/devkit'; -import { createNodes } from './plugin'; +import { createNodesV2 as createNodes } from './plugin'; import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; +import { loadViteDynamicImport } from '../utils/executor-utils'; + +jest.mock('../utils/executor-utils', () => ({ + loadViteDynamicImport: jest.fn().mockResolvedValue({ + resolveConfig: jest.fn().mockResolvedValue({}), + }), +})); describe('@nx/remix/plugin', () => { let createNodesFunction = createNodes[1]; let context: CreateNodesContext; let cwd = process.cwd(); - describe('root project', () => { - const tempFs = new TempFs('test'); + describe('Remix Classic Compiler', () => { + describe('root project', () => { + const tempFs = new TempFs('test'); - beforeEach(() => { - context = { - nxJsonConfiguration: { - targetDefaults: { - build: { - cache: false, - inputs: ['foo', '^foo'], + beforeEach(() => { + context = { + nxJsonConfiguration: { + targetDefaults: { + build: { + cache: false, + inputs: ['foo', '^foo'], + }, + dev: { + command: 'npm run dev', + }, + start: { + command: 'npm run start', + }, + typecheck: { + command: 'tsc', + }, }, - dev: { - command: 'npm run dev', - }, - start: { - command: 'npm run start', - }, - typecheck: { - command: 'tsc', + namedInputs: { + default: ['{projectRoot}/**/*'], + production: ['!{projectRoot}/**/*.spec.ts'], }, }, - namedInputs: { - default: ['{projectRoot}/**/*'], - production: ['!{projectRoot}/**/*.spec.ts'], - }, - }, - workspaceRoot: tempFs.tempDir, - configFiles: [], - }; - tempFs.createFileSync( - 'package.json', - JSON.stringify('{name: "my-app", type: "module"}') - ); - tempFs.createFileSync( - 'remix.config.cjs', - `/** + workspaceRoot: tempFs.tempDir, + configFiles: [], + }; + tempFs.createFileSync( + 'package.json', + JSON.stringify('{name: "my-app", type: "module"}') + ); + tempFs.createFileSync( + 'remix.config.cjs', + `/** * @type {import('@remix-run/dev').AppConfig} */ module.exports = { @@ -50,92 +58,241 @@ module.exports = { watchPaths: () => require('@nx/remix').createWatchPaths(__dirname), }; ` - ); - process.chdir(tempFs.tempDir); + ); + process.chdir(tempFs.tempDir); + }); + + afterEach(() => { + jest.resetModules(); + tempFs.cleanup(); + process.chdir(cwd); + }); + + it('should create nodes', async () => { + // ACT + const nodes = await createNodesFunction( + ['remix.config.cjs'], + { + buildTargetName: 'build', + devTargetName: 'dev', + startTargetName: 'start', + typecheckTargetName: 'typecheck', + staticServeTargetName: 'static-serve', + }, + context + ); + + // ASSERT + expect(nodes).toMatchSnapshot(); + }); }); - afterEach(() => { - jest.resetModules(); - tempFs.cleanup(); - process.chdir(cwd); - }); + describe('non-root project', () => { + const tempFs = new TempFs('test'); - it('should create nodes', async () => { - // ACT - const nodes = await createNodesFunction( - 'remix.config.cjs', - { - buildTargetName: 'build', - devTargetName: 'dev', - startTargetName: 'start', - typecheckTargetName: 'typecheck', - staticServeTargetName: 'static-serve', - }, - context - ); + beforeEach(() => { + context = { + nxJsonConfiguration: { + namedInputs: { + default: ['{projectRoot}/**/*'], + production: ['!{projectRoot}/**/*.spec.ts'], + }, + }, + workspaceRoot: tempFs.tempDir, + configFiles: [], + }; - // ASSERT - expect(nodes).toMatchSnapshot(); + tempFs.createFileSync( + 'my-app/project.json', + JSON.stringify({ name: 'my-app' }) + ); + + tempFs.createFileSync( + 'my-app/remix.config.cjs', + `/** + * @type {import('@remix-run/dev').AppConfig} + */ +module.exports = { + ignoredRouteFiles: ['**/.*'], + watchPaths: () => require('@nx/remix').createWatchPaths(__dirname), +}; +` + ); + + process.chdir(tempFs.tempDir); + }); + + afterEach(() => { + jest.resetModules(); + tempFs.cleanup(); + process.chdir(cwd); + }); + + it('should create nodes', async () => { + // ACT + const nodes = await createNodesFunction( + ['my-app/remix.config.cjs'], + { + buildTargetName: 'build', + devTargetName: 'dev', + startTargetName: 'start', + typecheckTargetName: 'tsc', + staticServeTargetName: 'static-serve', + }, + context + ); + + // ASSERT + expect(nodes).toMatchSnapshot(); + }); }); }); - describe('non-root project', () => { - const tempFs = new TempFs('test'); + describe('Remix Vite Compiler', () => { + describe('root project', () => { + const tempFs = new TempFs('test'); - beforeEach(() => { - context = { - nxJsonConfiguration: { - namedInputs: { - default: ['{projectRoot}/**/*'], - production: ['!{projectRoot}/**/*.spec.ts'], + beforeEach(() => { + context = { + nxJsonConfiguration: { + targetDefaults: { + build: { + cache: false, + inputs: ['foo', '^foo'], + }, + dev: { + command: 'npm run dev', + }, + start: { + command: 'npm run start', + }, + typecheck: { + command: 'tsc', + }, + }, + namedInputs: { + default: ['{projectRoot}/**/*'], + production: ['!{projectRoot}/**/*.spec.ts'], + }, }, - }, - workspaceRoot: tempFs.tempDir, - configFiles: [], - }; + workspaceRoot: tempFs.tempDir, + configFiles: [], + }; + tempFs.createFileSync( + 'package.json', + JSON.stringify('{name: "my-app", type: "module"}') + ); + tempFs.createFileSync( + 'vite.config.js', + `const {defineConfig} = require('vite'); + const { vitePlugin: remix } = require('@remix-run/dev'); + module.exports = defineConfig({ + plugins:[remix()] + });` + ); + process.chdir(tempFs.tempDir); + (loadViteDynamicImport as jest.Mock).mockResolvedValue({ + resolveConfig: jest.fn().mockResolvedValue({ + build: { + lib: { + entry: 'index.ts', + name: 'my-app', + }, + }, + }), + }); + }); - tempFs.createFileSync( - 'my-app/project.json', - JSON.stringify({ name: 'my-app' }) - ); + afterEach(() => { + jest.resetModules(); + tempFs.cleanup(); + process.chdir(cwd); + }); - tempFs.createFileSync( - 'my-app/remix.config.cjs', - `/** - * @type {import('@remix-run/dev').AppConfig} - */ -module.exports = { - ignoredRouteFiles: ['**/.*'], - watchPaths: () => require('@nx/remix').createWatchPaths(__dirname), -}; -` - ); + it('should create nodes', async () => { + // ACT + const nodes = await createNodesFunction( + ['vite.config.js'], + { + buildTargetName: 'build', + devTargetName: 'dev', + startTargetName: 'start', + typecheckTargetName: 'typecheck', + staticServeTargetName: 'static-serve', + }, + context + ); - process.chdir(tempFs.tempDir); + // ASSERT + expect(nodes).toMatchSnapshot(); + }); }); - afterEach(() => { - jest.resetModules(); - tempFs.cleanup(); - process.chdir(cwd); - }); + describe('non-root project', () => { + const tempFs = new TempFs('test'); - it('should create nodes', async () => { - // ACT - const nodes = await createNodesFunction( - 'my-app/remix.config.cjs', - { - buildTargetName: 'build', - devTargetName: 'dev', - startTargetName: 'start', - typecheckTargetName: 'tsc', - staticServeTargetName: 'static-serve', - }, - context - ); + beforeEach(() => { + context = { + nxJsonConfiguration: { + namedInputs: { + default: ['{projectRoot}/**/*'], + production: ['!{projectRoot}/**/*.spec.ts'], + }, + }, + workspaceRoot: tempFs.tempDir, + configFiles: [], + }; - // ASSERT - expect(nodes).toMatchSnapshot(); + tempFs.createFileSync( + 'my-app/project.json', + JSON.stringify({ name: 'my-app' }) + ); + + tempFs.createFileSync( + 'my-app/vite.config.js', + `const {defineConfig} = require('vite'); + const { vitePlugin: remix } = require('@remix-run/dev'); + module.exports = defineConfig({ + plugins:[remix()] + });` + ); + (loadViteDynamicImport as jest.Mock).mockResolvedValue({ + resolveConfig: jest.fn().mockResolvedValue({ + build: { + lib: { + entry: 'index.ts', + name: 'my-app', + }, + }, + }), + }); + + process.chdir(tempFs.tempDir); + }); + + afterEach(() => { + jest.resetModules(); + tempFs.cleanup(); + process.chdir(cwd); + }); + + it('should create nodes', async () => { + // ACT + const nodes = await createNodesFunction( + ['my-app/vite.config.js'], + { + buildTargetName: 'build', + devTargetName: 'dev', + startTargetName: 'start', + typecheckTargetName: 'tsc', + staticServeTargetName: 'static-serve', + }, + context + ); + + // ASSERT + expect(nodes).toMatchSnapshot(); + }); }); }); }); diff --git a/packages/remix/src/plugins/plugin.ts b/packages/remix/src/plugins/plugin.ts index 2a65d594ef..ca9d7e8bb9 100644 --- a/packages/remix/src/plugins/plugin.ts +++ b/packages/remix/src/plugins/plugin.ts @@ -1,44 +1,27 @@ import { workspaceDataDirectory } from 'nx/src/utils/cache-directory'; +import { hashObject } from 'nx/src/hasher/file-hasher'; import { type CreateDependencies, type CreateNodes, type CreateNodesContext, + createNodesFromFiles, + CreateNodesV2, detectPackageManager, joinPathFragments, + logger, + ProjectConfiguration, readJsonFile, type TargetConfiguration, writeJsonFile, } from '@nx/devkit'; import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes'; import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs'; +import { loadConfigFile } from '@nx/devkit/src/utils/config-utils'; import { getLockFileName } from '@nx/js'; import { type AppConfig } from '@remix-run/dev'; -import { dirname, join } from 'path'; -import { existsSync, readdirSync } from 'fs'; -import { loadConfigFile } from '@nx/devkit/src/utils/config-utils'; - -const cachePath = join(workspaceDataDirectory, 'remix.hash'); -const targetsCache = readTargetsCache(); - -function readTargetsCache(): Record< - string, - Record -> { - return existsSync(cachePath) ? readJsonFile(cachePath) : {}; -} - -function writeTargetsToCache() { - const oldCache = readTargetsCache(); - writeJsonFile(cachePath, { - ...oldCache, - ...targetsCache, - }); -} - -export const createDependencies: CreateDependencies = () => { - writeTargetsToCache(); - return []; -}; +import { dirname, isAbsolute, join, relative } from 'path'; +import { existsSync, readdirSync, readFileSync } from 'fs'; +import { loadViteDynamicImport } from '../utils/executor-utils'; export interface RemixPluginOptions { buildTargetName?: string; @@ -51,60 +34,133 @@ export interface RemixPluginOptions { staticServeTargetName?: string; serveStaticTargetName?: string; } +type RemixTargets = Pick; -export const createNodes: CreateNodes = [ - '**/remix.config.{js,cjs,mjs}', - async (configFilePath, options, context) => { - const projectRoot = dirname(configFilePath); - const fullyQualifiedProjectRoot = join(context.workspaceRoot, projectRoot); - // Do not create a project if package.json and project.json isn't there - const siblingFiles = readdirSync(fullyQualifiedProjectRoot); - if ( - !siblingFiles.includes('package.json') && - !siblingFiles.includes('project.json') && - !siblingFiles.includes('vite.config.ts') && - !siblingFiles.includes('vite.config.js') - ) { - return {}; +function readTargetsCache( + cachePath: string +): Record> { + return existsSync(cachePath) ? readJsonFile(cachePath) : {}; +} + +function writeTargetsToCache( + cachePath: string, + results: Record +) { + writeJsonFile(cachePath, results); +} + +/** + * @deprecated The 'createDependencies' function is now a no-op. This functionality is included in 'createNodesV2'. + */ +export const createDependencies: CreateDependencies = () => { + return []; +}; + +const remixConfigGlob = '**/{remix,vite}.config.{js,cjs,mjs}'; + +export const createNodesV2: CreateNodesV2 = [ + remixConfigGlob, + async (configFilePaths, options, context) => { + const optionsHash = hashObject(options); + const cachePath = join(workspaceDataDirectory, `remix-${optionsHash}.hash`); + const targetsCache = readTargetsCache(cachePath); + try { + return await createNodesFromFiles( + (configFile, options, context) => + createNodesInternal(configFile, options, context, targetsCache), + configFilePaths, + options, + context + ); + } finally { + writeTargetsToCache(cachePath, targetsCache); } - - options = normalizeOptions(options); - - const hash = await calculateHashForCreateNodes( - projectRoot, - options, - context, - [getLockFileName(detectPackageManager(context.workspaceRoot))] - ); - targetsCache[hash] ??= await buildRemixTargets( - configFilePath, - projectRoot, - options, - context, - siblingFiles - ); - - return { - projects: { - [projectRoot]: { - root: projectRoot, - targets: targetsCache[hash], - }, - }, - }; }, ]; +export const createNodes: CreateNodes = [ + remixConfigGlob, + async (configFilePath, options, context) => { + logger.warn( + '`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.' + ); + return createNodesInternal(configFilePath, options, context, {}); + }, +]; + +async function createNodesInternal( + configFilePath: string, + options: RemixPluginOptions, + context: CreateNodesContext, + targetsCache: Record +) { + const projectRoot = dirname(configFilePath); + const fullyQualifiedProjectRoot = join(context.workspaceRoot, projectRoot); + // Do not create a project if package.json and project.json isn't there + const siblingFiles = readdirSync(fullyQualifiedProjectRoot); + if ( + !siblingFiles.includes('package.json') && + !siblingFiles.includes('project.json') + ) { + return {}; + } + + options = normalizeOptions(options); + + const remixCompiler = determineIsRemixVite( + configFilePath, + context.workspaceRoot + ); + + if (remixCompiler === RemixCompiler.IsNotRemix) { + return {}; + } + + const hash = + (await calculateHashForCreateNodes(projectRoot, options, context, [ + getLockFileName(detectPackageManager(context.workspaceRoot)), + ])) + configFilePath; + + targetsCache[hash] ??= await buildRemixTargets( + configFilePath, + projectRoot, + options, + context, + siblingFiles, + remixCompiler + ); + + const { targets, metadata } = targetsCache[hash]; + + const project: ProjectConfiguration = { + root: projectRoot, + targets, + metadata, + }; + + return { + projects: { + [projectRoot]: project, + }, + }; +} + async function buildRemixTargets( configFilePath: string, projectRoot: string, options: RemixPluginOptions, context: CreateNodesContext, - siblingFiles: string[] + siblingFiles: string[], + remixCompiler: RemixCompiler ) { const namedInputs = getNamedInputs(projectRoot, context); const { buildDirectory, assetsBuildDirectory, serverBuildPath } = - await getBuildPaths(configFilePath, context.workspaceRoot); + await getBuildPaths( + configFilePath, + projectRoot, + context.workspaceRoot, + remixCompiler + ); const targets: Record = {}; targets[options.buildTargetName] = buildTarget( @@ -112,24 +168,32 @@ async function buildRemixTargets( projectRoot, buildDirectory, assetsBuildDirectory, - namedInputs + namedInputs, + remixCompiler + ); + targets[options.devTargetName] = devTarget( + serverBuildPath, + projectRoot, + remixCompiler ); - targets[options.devTargetName] = devTarget(serverBuildPath, projectRoot); targets[options.startTargetName] = startTarget( projectRoot, serverBuildPath, - options.buildTargetName + options.buildTargetName, + remixCompiler ); // TODO(colum): Remove for Nx 21 targets[options.staticServeTargetName] = startTarget( projectRoot, serverBuildPath, - options.buildTargetName + options.buildTargetName, + remixCompiler ); targets[options.serveStaticTargetName] = startTarget( projectRoot, serverBuildPath, - options.buildTargetName + options.buildTargetName, + remixCompiler ); targets[options.typecheckTargetName] = typecheckTarget( projectRoot, @@ -137,7 +201,7 @@ async function buildRemixTargets( siblingFiles ); - return targets; + return { targets, metadata: {} }; } function buildTarget( @@ -145,7 +209,8 @@ function buildTarget( projectRoot: string, buildDirectory: string, assetsBuildDirectory: string, - namedInputs: { [inputName: string]: any[] } + namedInputs: { [inputName: string]: any[] }, + remixCompiler: RemixCompiler ): TargetConfiguration { const serverBuildOutputPath = projectRoot === '.' @@ -157,6 +222,15 @@ function buildTarget( ? joinPathFragments(`{workspaceRoot}`, assetsBuildDirectory) : joinPathFragments(`{workspaceRoot}`, projectRoot, assetsBuildDirectory); + const outputs = + remixCompiler === RemixCompiler.IsVte + ? [ + projectRoot === '.' + ? joinPathFragments(`{workspaceRoot}`, buildDirectory) + : joinPathFragments(`{workspaceRoot}`, projectRoot, buildDirectory), + ] + : [serverBuildOutputPath, assetsBuildOutputPath]; + return { cache: true, dependsOn: [`^${buildTargetName}`], @@ -166,18 +240,25 @@ function buildTarget( : ['default', '^default']), { externalDependencies: ['@remix-run/dev'] }, ], - outputs: [serverBuildOutputPath, assetsBuildOutputPath], - command: 'remix build', + outputs, + command: + remixCompiler === RemixCompiler.IsVte + ? 'remix vite:build' + : 'remix build', options: { cwd: projectRoot }, }; } function devTarget( serverBuildPath: string, - projectRoot: string + projectRoot: string, + remixCompiler: RemixCompiler ): TargetConfiguration { return { - command: 'remix dev --manual', + command: + remixCompiler === RemixCompiler.IsVte + ? 'remix vite:dev' + : 'remix dev --manual', options: { cwd: projectRoot }, }; } @@ -185,11 +266,19 @@ function devTarget( function startTarget( projectRoot: string, serverBuildPath: string, - buildTargetName: string + buildTargetName: string, + remixCompiler: RemixCompiler ): TargetConfiguration { + let serverPath = serverBuildPath; + if (remixCompiler === RemixCompiler.IsVte) { + if (serverBuildPath === 'build') { + serverPath = `${serverBuildPath}/server/index.js`; + } + } + return { dependsOn: [buildTargetName], - command: `remix-serve ${serverBuildPath}`, + command: `remix-serve ${serverPath}`, options: { cwd: projectRoot, }, @@ -222,19 +311,46 @@ function typecheckTarget( async function getBuildPaths( configFilePath: string, - workspaceRoot: string + projectRoot: string, + workspaceRoot: string, + remixCompiler: RemixCompiler ): Promise<{ buildDirectory: string; - assetsBuildDirectory: string; - serverBuildPath: string; + assetsBuildDirectory?: string; + serverBuildPath?: string; }> { const configPath = join(workspaceRoot, configFilePath); - let appConfig = await loadConfigFile(configPath); - return { - buildDirectory: 'build', - serverBuildPath: appConfig.serverBuildPath ?? 'build/index.js', - assetsBuildDirectory: appConfig.assetsBuildDirectory ?? 'public/build', - }; + if (remixCompiler === RemixCompiler.IsClassic) { + let appConfig = await loadConfigFile(configPath); + return { + buildDirectory: 'build', + serverBuildPath: appConfig.serverBuildPath ?? 'build/index.js', + assetsBuildDirectory: appConfig.assetsBuildDirectory ?? 'public/build', + }; + } else { + // Workaround for the `build$3 is not a function` error that we sometimes see in agents. + // This should be removed later once we address the issue properly + try { + const importEsbuild = () => new Function('return import("esbuild")')(); + await importEsbuild(); + } catch { + // do nothing + } + const { resolveConfig } = await loadViteDynamicImport(); + const viteBuildConfig = await resolveConfig( + { + configFile: configPath, + mode: 'development', + }, + 'build' + ); + + return { + buildDirectory: viteBuildConfig.build?.outDir ?? 'build', + serverBuildPath: viteBuildConfig.build?.outDir ?? 'build', + assetsBuildDirectory: 'build/client', + }; + } } function normalizeOptions(options: RemixPluginOptions) { @@ -249,3 +365,28 @@ function normalizeOptions(options: RemixPluginOptions) { return options; } + +function determineIsRemixVite(configFilePath: string, workspaceRoot: string) { + if (configFilePath.includes('remix.config')) { + return RemixCompiler.IsClassic; + } + + const fileContents = readFileSync( + join(workspaceRoot, configFilePath), + 'utf8' + ); + if ( + fileContents.includes('@remix-run/dev') && + (fileContents.includes('vitePlugin()') || fileContents.includes('remix()')) + ) { + return RemixCompiler.IsVte; + } else { + return RemixCompiler.IsNotRemix; + } +} + +enum RemixCompiler { + IsClassic = 1, + IsVte = 2, + IsNotRemix = 3, +} diff --git a/packages/remix/src/utils/executor-utils.ts b/packages/remix/src/utils/executor-utils.ts new file mode 100644 index 0000000000..f81d8b5590 --- /dev/null +++ b/packages/remix/src/utils/executor-utils.ts @@ -0,0 +1,3 @@ +export function loadViteDynamicImport() { + return Function('return import("vite")')() as Promise; +} diff --git a/packages/vite/src/generators/init/init.spec.ts b/packages/vite/src/generators/init/init.spec.ts index 002049cd98..b65057323d 100644 --- a/packages/vite/src/generators/init/init.spec.ts +++ b/packages/vite/src/generators/init/init.spec.ts @@ -83,6 +83,7 @@ describe('@nx/vite:init', () => { "serveStaticTargetName": "serve-static", "serveTargetName": "serve", "testTargetName": "test", + "typecheckTargetName": "typecheck", }, "plugin": "@nx/vite/plugin", }, diff --git a/packages/vite/src/generators/init/init.ts b/packages/vite/src/generators/init/init.ts index 6a5018ec96..b5d3dc91c0 100644 --- a/packages/vite/src/generators/init/init.ts +++ b/packages/vite/src/generators/init/init.ts @@ -76,6 +76,7 @@ export async function initGeneratorInternal( 'vite:serve-static', 'vite-serve-static', ], + typecheckTargetName: ['typecheck', 'vite:typecheck', 'vite-typecheck'], }, schema.updatePackageScripts );