feat(remix): generate remix vite application (#28555)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->
We currently still generate Remix Classic applications


## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
We should generate Remix Vite applications

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
This commit is contained in:
Colum Ferry 2024-10-29 12:38:13 +00:00 committed by GitHub
parent a9dbc71e9d
commit af9d980f34
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 593 additions and 864 deletions

View File

@ -20,11 +20,6 @@
"description": "The name of the application.",
"x-priority": "important"
},
"js": {
"type": "boolean",
"description": "Generate JavaScript files rather than TypeScript files.",
"default": false
},
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",

View File

@ -21,11 +21,6 @@
"x-prompt": "What project would you like to add Tailwind to?",
"pattern": "^[a-zA-Z].*$"
},
"js": {
"type": "boolean",
"description": "Generate a JavaScript config file instead of a TypeScript config file",
"default": false
},
"skipFormat": {
"type": "boolean",
"description": "Skip formatting files after generator runs",

View File

@ -32,10 +32,10 @@ describe('@nx/workspace:infer-targets', () => {
// default case, everything is generated with crystal, everything should be skipped
const remixApp = uniq('remix');
runCLI(
`generate @nx/remix:app apps/${remixApp} --unitTestRunner jest --e2eTestRunner=playwright --projectNameAndDirectoryFormat=as-provided --no-interactive`
`generate @nx/remix:app apps/${remixApp} --unitTestRunner jest --e2eTestRunner=playwright --no-interactive`
);
const output = runCLI(`generate infer-targets --no-interactive`);
const output = runCLI(`generate infer-targets --no-interactive --verbose`);
expect(output).toContain('@nx/remix:convert-to-inferred - Skipped');
expect(output).toContain('@nx/playwright:convert-to-inferred - Skipped');
@ -60,7 +60,7 @@ describe('@nx/workspace:infer-targets', () => {
return json;
});
const output2 = runCLI(`generate infer-targets --no-interactive`);
const output2 = runCLI(`generate infer-targets --no-interactive --verbose`);
expect(output2).toContain('@nx/remix:convert-to-inferred - Success');
expect(output2).toContain('@nx/eslint:convert-to-inferred - Success');
@ -70,7 +70,7 @@ describe('@nx/workspace:infer-targets', () => {
// default case, everything is generated with crystal, relevant plugins should be skipped
const remixApp = uniq('remix');
runCLI(
`generate @nx/remix:app apps/${remixApp} --unitTestRunner jest --e2eTestRunner=playwright --projectNameAndDirectoryFormat=as-provided --no-interactive`
`generate @nx/remix:app apps/${remixApp} --unitTestRunner jest --e2eTestRunner=playwright --no-interactive`
);
const output = runCLI(
@ -116,7 +116,7 @@ describe('@nx/workspace:infer-targets', () => {
// even if we make sure there are executors for remix & remix-e2e, only remix conversions will run with --project option
const remixApp = uniq('remix');
runCLI(
`generate @nx/remix:app apps/${remixApp} --unitTestRunner jest --e2eTestRunner=playwright --projectNameAndDirectoryFormat=as-provided --no-interactive`
`generate @nx/remix:app apps/${remixApp} --unitTestRunner jest --e2eTestRunner=playwright --no-interactive`
);
updateJson('nx.json', (json) => {

View File

@ -61,7 +61,7 @@ describe('Remix E2E Tests', () => {
const result = runCLI(`build ${plugin}`);
expect(result).toContain('Successfully ran target build');
checkFilesExist(`subdir/build/index.js`);
checkFilesExist(`subdir/build/server/index.js`);
}, 120000);
});

View File

@ -93,8 +93,8 @@
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
"@pnpm/lockfile-types": "^6.0.0",
"@reduxjs/toolkit": "1.9.0",
"@remix-run/dev": "^2.8.1",
"@remix-run/node": "^2.8.1",
"@remix-run/dev": "^2.13.1",
"@remix-run/node": "^2.13.1",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-image": "^3.0.3",

View File

@ -131,6 +131,35 @@
"alwaysAddToPackageJson": true
}
}
},
"20.1.0": {
"version": "20.1.0-beta.0",
"packages": {
"@remix-run/node": {
"version": "^2.13.1",
"alwaysAddToPackageJson": true
},
"@remix-run/react": {
"version": "^2.13.1",
"alwaysAddToPackageJson": true
},
"@remix-run/serve": {
"version": "^2.13.1",
"alwaysAddToPackageJson": true
},
"@remix-run/dev": {
"version": "^2.13.1",
"alwaysAddToPackageJson": true
},
"@remix-run/css-bundle": {
"version": "^2.13.1",
"alwaysAddToPackageJson": true
},
"@remix-run/eslint-config": {
"version": "^2.13.1",
"alwaysAddToPackageJson": true
}
}
}
}
}

View File

@ -35,7 +35,9 @@
"tslib": "^2.3.1",
"@phenomnomnominal/tsquery": "~5.0.1"
},
"peerDependencies": {},
"peerDependencies": {
"@remix-run/dev": "^2.13.1"
},
"publishConfig": {
"access": "public"
},

View File

@ -1,36 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Remix Application Integrated Repo --directory should create the application correctly 1`] = `
"import { createWatchPaths } from '@nx/remix';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* @type {import('@remix-run/dev').AppConfig}
*/
export default {
ignoredRouteFiles: ['**/.*'],
// appDirectory: "app",
// assetsBuildDirectory: "public/build",
// serverBuildPath: "build/index.js",
// publicPath: "/build/",
watchPaths: () => createWatchPaths(__dirname),
};
"
`;
exports[`Remix Application Integrated Repo --directory should create the application correctly 2`] = `
"import type { MetaFunction } from '@remix-run/node';
import {
"import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from '@remix-run/react';
import type { MetaFunction, LinksFunction } from '@remix-run/node';
export const meta: MetaFunction = () => [
{
@ -38,7 +16,20 @@ export const meta: MetaFunction = () => [
},
];
export default function App() {
export const links: LinksFunction = () => [
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
{
rel: 'preconnect',
href: 'https://fonts.gstatic.com',
crossOrigin: 'anonymous',
},
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
},
];
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
@ -48,18 +39,21 @@ export default function App() {
<Links />
</head>
<body>
<Outlet />
{children}
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}
"
`;
exports[`Remix Application Integrated Repo --directory should create the application correctly 3`] = `
exports[`Remix Application Integrated Repo --directory should create the application correctly 2`] = `
"import NxWelcome from '../nx-welcome';
export default function Index() {
@ -73,36 +67,14 @@ export default function Index() {
`;
exports[`Remix Application Integrated Repo --directory should extract the layout directory from the directory options if it exists 1`] = `
"import { createWatchPaths } from '@nx/remix';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* @type {import('@remix-run/dev').AppConfig}
*/
export default {
ignoredRouteFiles: ['**/.*'],
// appDirectory: "app",
// assetsBuildDirectory: "public/build",
// serverBuildPath: "build/index.js",
// publicPath: "/build/",
watchPaths: () => createWatchPaths(__dirname),
};
"
`;
exports[`Remix Application Integrated Repo --directory should extract the layout directory from the directory options if it exists 2`] = `
"import type { MetaFunction } from '@remix-run/node';
import {
"import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from '@remix-run/react';
import type { MetaFunction, LinksFunction } from '@remix-run/node';
export const meta: MetaFunction = () => [
{
@ -110,7 +82,20 @@ export const meta: MetaFunction = () => [
},
];
export default function App() {
export const links: LinksFunction = () => [
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
{
rel: 'preconnect',
href: 'https://fonts.gstatic.com',
crossOrigin: 'anonymous',
},
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
},
];
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
@ -120,18 +105,21 @@ export default function App() {
<Links />
</head>
<body>
<Outlet />
{children}
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}
"
`;
exports[`Remix Application Integrated Repo --directory should extract the layout directory from the directory options if it exists 3`] = `
exports[`Remix Application Integrated Repo --directory should extract the layout directory from the directory options if it exists 2`] = `
"import NxWelcome from '../nx-welcome';
export default function Index() {
@ -239,96 +227,7 @@ export default defineConfig({
"
`;
exports[`Remix Application Integrated Repo --js should create the application correctly 1`] = `
"import { createWatchPaths } from '@nx/remix';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* @type {import('@remix-run/dev').AppConfig}
*/
export default {
ignoredRouteFiles: ['**/.*'],
// appDirectory: "app",
// assetsBuildDirectory: "public/build",
// serverBuildPath: "build/index.js",
// publicPath: "/build/",
watchPaths: () => createWatchPaths(__dirname),
};
"
`;
exports[`Remix Application Integrated Repo --js should create the application correctly 2`] = `
"import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from '@remix-run/react';
export const meta = () => [
{
title: 'New Remix App',
},
];
export default function App() {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
"
`;
exports[`Remix Application Integrated Repo --js should create the application correctly 3`] = `
"import NxWelcome from '../nx-welcome';
export default function Index() {
return (
<div>
<NxWelcome title={'test'} />
</div>
);
}
"
`;
exports[`Remix Application Integrated Repo --unitTestRunner should generate the correct files for testing using jest 1`] = `
"import { createWatchPaths } from '@nx/remix';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* @type {import('@remix-run/dev').AppConfig}
*/
export default {
ignoredRouteFiles: ['**/.*'],
// appDirectory: "app",
// assetsBuildDirectory: "public/build",
// serverBuildPath: "build/index.js",
// publicPath: "/build/",
watchPaths: () => createWatchPaths(__dirname),
};
"
`;
exports[`Remix Application Integrated Repo --unitTestRunner should generate the correct files for testing using jest 2`] = `
"export default {
displayName: 'test',
preset: '../jest.preset.js',
@ -341,7 +240,7 @@ exports[`Remix Application Integrated Repo --unitTestRunner should generate the
"
`;
exports[`Remix Application Integrated Repo --unitTestRunner should generate the correct files for testing using jest 3`] = `
exports[`Remix Application Integrated Repo --unitTestRunner should generate the correct files for testing using jest 2`] = `
"import { installGlobals } from '@remix-run/node';
import '@testing-library/jest-dom/matchers';
installGlobals();
@ -349,27 +248,6 @@ installGlobals();
`;
exports[`Remix Application Integrated Repo --unitTestRunner should generate the correct files for testing using vitest 1`] = `
"import { createWatchPaths } from '@nx/remix';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* @type {import('@remix-run/dev').AppConfig}
*/
export default {
ignoredRouteFiles: ['**/.*'],
// appDirectory: "app",
// assetsBuildDirectory: "public/build",
// serverBuildPath: "build/index.js",
// publicPath: "/build/",
watchPaths: () => createWatchPaths(__dirname),
};
"
`;
exports[`Remix Application Integrated Repo --unitTestRunner should generate the correct files for testing using vitest 2`] = `
"/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
@ -400,14 +278,14 @@ export default defineConfig({
"
`;
exports[`Remix Application Integrated Repo --unitTestRunner should generate the correct files for testing using vitest 3`] = `
exports[`Remix Application Integrated Repo --unitTestRunner should generate the correct files for testing using vitest 2`] = `
"import { installGlobals } from '@remix-run/node';
import '@testing-library/jest-dom/matchers';
installGlobals();
"
`;
exports[`Remix Application Integrated Repo --unitTestRunner should generate the correct files for testing using vitest 4`] = `
exports[`Remix Application Integrated Repo --unitTestRunner should generate the correct files for testing using vitest 3`] = `
"{
"extends": "./tsconfig.json",
"compilerOptions": {
@ -441,36 +319,14 @@ exports[`Remix Application Integrated Repo --unitTestRunner should generate the
`;
exports[`Remix Application Integrated Repo should create the application correctly 1`] = `
"import { createWatchPaths } from '@nx/remix';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* @type {import('@remix-run/dev').AppConfig}
*/
export default {
ignoredRouteFiles: ['**/.*'],
// appDirectory: "app",
// assetsBuildDirectory: "public/build",
// serverBuildPath: "build/index.js",
// publicPath: "/build/",
watchPaths: () => createWatchPaths(__dirname),
};
"
`;
exports[`Remix Application Integrated Repo should create the application correctly 2`] = `
"import type { MetaFunction } from '@remix-run/node';
import {
"import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from '@remix-run/react';
import type { MetaFunction, LinksFunction } from '@remix-run/node';
export const meta: MetaFunction = () => [
{
@ -478,7 +334,20 @@ export const meta: MetaFunction = () => [
},
];
export default function App() {
export const links: LinksFunction = () => [
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
{
rel: 'preconnect',
href: 'https://fonts.gstatic.com',
crossOrigin: 'anonymous',
},
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
},
];
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
@ -488,18 +357,21 @@ export default function App() {
<Links />
</head>
<body>
<Outlet />
{children}
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}
"
`;
exports[`Remix Application Integrated Repo should create the application correctly 3`] = `
exports[`Remix Application Integrated Repo should create the application correctly 2`] = `
"import NxWelcome from '../nx-welcome';
export default function Index() {
@ -534,96 +406,7 @@ export default defineConfig({
"
`;
exports[`Remix Application Standalone Project Repo --js should create the application correctly 1`] = `
"import { createWatchPaths } from '@nx/remix';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* @type {import('@remix-run/dev').AppConfig}
*/
export default {
ignoredRouteFiles: ['**/.*'],
// appDirectory: "app",
// assetsBuildDirectory: "public/build",
// serverBuildPath: "build/index.js",
// publicPath: "/build/",
watchPaths: () => createWatchPaths(__dirname),
};
"
`;
exports[`Remix Application Standalone Project Repo --js should create the application correctly 2`] = `
"import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from '@remix-run/react';
export const meta = () => [
{
title: 'New Remix App',
},
];
export default function App() {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
"
`;
exports[`Remix Application Standalone Project Repo --js should create the application correctly 3`] = `
"import NxWelcome from '../nx-welcome';
export default function Index() {
return (
<div>
<NxWelcome title={'test'} />
</div>
);
}
"
`;
exports[`Remix Application Standalone Project Repo --unitTestRunner should generate the correct files for testing using jest 1`] = `
"import { createWatchPaths } from '@nx/remix';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* @type {import('@remix-run/dev').AppConfig}
*/
export default {
ignoredRouteFiles: ['**/.*'],
// appDirectory: "app",
// assetsBuildDirectory: "public/build",
// serverBuildPath: "build/index.js",
// publicPath: "/build/",
watchPaths: () => createWatchPaths(__dirname),
};
"
`;
exports[`Remix Application Standalone Project Repo --unitTestRunner should generate the correct files for testing using jest 2`] = `
"export default {
setupFilesAfterEnv: ['<rootDir>/test-setup.ts'],
displayName: 'test',
@ -640,14 +423,14 @@ exports[`Remix Application Standalone Project Repo --unitTestRunner should gener
"
`;
exports[`Remix Application Standalone Project Repo --unitTestRunner should generate the correct files for testing using jest 3`] = `
exports[`Remix Application Standalone Project Repo --unitTestRunner should generate the correct files for testing using jest 2`] = `
"import { installGlobals } from '@remix-run/node';
import '@testing-library/jest-dom/matchers';
installGlobals();
"
`;
exports[`Remix Application Standalone Project Repo --unitTestRunner should generate the correct files for testing using jest 4`] = `
exports[`Remix Application Standalone Project Repo --unitTestRunner should generate the correct files for testing using jest 3`] = `
"import { createRemixStub } from '@remix-run/testing';
import { render, screen, waitFor } from '@testing-library/react';
import Index from '../../app/routes/_index';
@ -668,27 +451,6 @@ test('renders loader data', async () => {
`;
exports[`Remix Application Standalone Project Repo --unitTestRunner should generate the correct files for testing using vitest 1`] = `
"import { createWatchPaths } from '@nx/remix';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* @type {import('@remix-run/dev').AppConfig}
*/
export default {
ignoredRouteFiles: ['**/.*'],
// appDirectory: "app",
// assetsBuildDirectory: "public/build",
// serverBuildPath: "build/index.js",
// publicPath: "/build/",
watchPaths: () => createWatchPaths(__dirname),
};
"
`;
exports[`Remix Application Standalone Project Repo --unitTestRunner should generate the correct files for testing using vitest 2`] = `
"/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
@ -719,7 +481,7 @@ export default defineConfig({
"
`;
exports[`Remix Application Standalone Project Repo --unitTestRunner should generate the correct files for testing using vitest 3`] = `
exports[`Remix Application Standalone Project Repo --unitTestRunner should generate the correct files for testing using vitest 2`] = `
"import { createRemixStub } from '@remix-run/testing';
import { render, screen, waitFor } from '@testing-library/react';
import Index from '../../app/routes/_index';
@ -739,7 +501,7 @@ test('renders loader data', async () => {
"
`;
exports[`Remix Application Standalone Project Repo --unitTestRunner should generate the correct files for testing using vitest 4`] = `
exports[`Remix Application Standalone Project Repo --unitTestRunner should generate the correct files for testing using vitest 3`] = `
"{
"extends": "./tsconfig.json",
"compilerOptions": {
@ -772,7 +534,7 @@ exports[`Remix Application Standalone Project Repo --unitTestRunner should gener
"
`;
exports[`Remix Application Standalone Project Repo --unitTestRunner should generate the correct files for testing using vitest 5`] = `
exports[`Remix Application Standalone Project Repo --unitTestRunner should generate the correct files for testing using vitest 4`] = `
"import { installGlobals } from '@remix-run/node';
import '@testing-library/jest-dom/matchers';
installGlobals();
@ -780,36 +542,14 @@ installGlobals();
`;
exports[`Remix Application Standalone Project Repo should create the application correctly 1`] = `
"import { createWatchPaths } from '@nx/remix';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* @type {import('@remix-run/dev').AppConfig}
*/
export default {
ignoredRouteFiles: ['**/.*'],
// appDirectory: "app",
// assetsBuildDirectory: "public/build",
// serverBuildPath: "build/index.js",
// publicPath: "/build/",
watchPaths: () => createWatchPaths(__dirname),
};
"
`;
exports[`Remix Application Standalone Project Repo should create the application correctly 2`] = `
"import type { MetaFunction } from '@remix-run/node';
import {
"import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from '@remix-run/react';
import type { MetaFunction, LinksFunction } from '@remix-run/node';
export const meta: MetaFunction = () => [
{
@ -817,7 +557,20 @@ export const meta: MetaFunction = () => [
},
];
export default function App() {
export const links: LinksFunction = () => [
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
{
rel: 'preconnect',
href: 'https://fonts.gstatic.com',
crossOrigin: 'anonymous',
},
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
},
];
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
@ -827,18 +580,21 @@ export default function App() {
<Links />
</head>
<body>
<Outlet />
{children}
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}
"
`;
exports[`Remix Application Standalone Project Repo should create the application correctly 3`] = `
exports[`Remix Application Standalone Project Repo should create the application correctly 2`] = `
"import NxWelcome from '../nx-welcome';
export default function Index() {
@ -851,7 +607,7 @@ export default function Index() {
"
`;
exports[`Remix Application Standalone Project Repo should create the application correctly 4`] = `
exports[`Remix Application Standalone Project Repo should create the application correctly 3`] = `
"import { createRemixStub } from '@remix-run/testing';
import { render, screen, waitFor } from '@testing-library/react';
import Index from '../../app/routes/_index';
@ -871,9 +627,36 @@ test('renders loader data', async () => {
"
`;
exports[`Remix Application Standalone Project Repo should create the application correctly 5`] = `null`;
exports[`Remix Application Standalone Project Repo should create the application correctly 4`] = `
"import { vitePlugin as remix } from '@remix-run/dev';
import { defineConfig } from 'vite';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
exports[`Remix Application Standalone Project Repo should create the application correctly 6`] = `
declare module '@remix-run/node' {
interface Future {
v3_singleFetch: true;
}
}
export default defineConfig({
root: __dirname,
plugins: [
remix({
future: {
v3_fetcherPersist: true,
v3_relativeSplatPath: true,
v3_throwAbortReason: true,
v3_singleFetch: true,
v3_lazyRouteDiscovery: true,
},
}),
nxViteTsPaths(),
],
});
"
`;
exports[`Remix Application Standalone Project Repo should create the application correctly 5`] = `
"{
"root": true,
"ignorePatterns": ["!**/*", "build", "public/build"],

View File

@ -30,7 +30,7 @@ describe('Remix Application', () => {
// ASSERT
expectTargetsToBeCorrect(tree, '.');
expect(tree.read('remix.config.js', 'utf-8')).toMatchSnapshot();
expect(tree.exists('remix.config.js')).toBeFalsy();
expect(tree.read('app/root.tsx', 'utf-8')).toMatchSnapshot();
expect(tree.read('app/routes/_index.tsx', 'utf-8')).toMatchSnapshot();
expect(
@ -40,29 +40,6 @@ describe('Remix Application', () => {
expect(tree.read('.eslintrc.json', 'utf-8')).toMatchSnapshot();
});
describe(`--js`, () => {
it('should create the application correctly', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
// ACT
await applicationGenerator(tree, {
name: 'test',
directory: '.',
js: true,
rootProject: true,
addPlugin: true,
});
// ASSERT
expectTargetsToBeCorrect(tree, '.');
expect(tree.read('remix.config.js', 'utf-8')).toMatchSnapshot();
expect(tree.read('app/root.js', 'utf-8')).toMatchSnapshot();
expect(tree.read('app/routes/_index.js', 'utf-8')).toMatchSnapshot();
});
});
describe('--unitTestRunner', () => {
it('should generate the correct files for testing using vitest', async () => {
// ARRANGE
@ -80,7 +57,7 @@ describe('Remix Application', () => {
// ASSERT
expectTargetsToBeCorrect(tree, '.');
expect(tree.read('remix.config.js', 'utf-8')).toMatchSnapshot();
expect(tree.exists('remix.config.js')).toBeFalsy();
expect(tree.read('vitest.config.ts', 'utf-8')).toMatchSnapshot();
expect(
tree.read('tests/routes/_index.spec.tsx', 'utf-8')
@ -105,7 +82,7 @@ describe('Remix Application', () => {
// ASSERT
expectTargetsToBeCorrect(tree, '.');
expect(tree.read('remix.config.js', 'utf-8')).toMatchSnapshot();
expect(tree.exists('remix.config.js')).toBeFalsy();
expect(tree.read('jest.config.ts', 'utf-8')).toMatchSnapshot();
expect(tree.read('test-setup.ts', 'utf-8')).toMatchSnapshot();
expect(
@ -186,37 +163,13 @@ describe('Remix Application', () => {
// ASSERT
expectTargetsToBeCorrect(tree, appDir);
expect(tree.read(`${appDir}/remix.config.js`, 'utf-8')).toMatchSnapshot();
expect(tree.exists(`${appDir}/remix.config.js`)).toBeFalsy();
expect(tree.read(`${appDir}/app/root.tsx`, 'utf-8')).toMatchSnapshot();
expect(
tree.read(`${appDir}/app/routes/_index.tsx`, 'utf-8')
).toMatchSnapshot();
});
describe('--js', () => {
it('should create the application correctly', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
// ACT
await applicationGenerator(tree, {
directory: 'test',
js: true,
addPlugin: true,
});
// ASSERT
expectTargetsToBeCorrect(tree, appDir);
expect(
tree.read(`${appDir}/remix.config.js`, 'utf-8')
).toMatchSnapshot();
expect(tree.read(`${appDir}/app/root.js`, 'utf-8')).toMatchSnapshot();
expect(
tree.read(`${appDir}/app/routes/_index.js`, 'utf-8')
).toMatchSnapshot();
});
});
describe('--directory', () => {
it('should create the application correctly', async () => {
// ARRANGE
@ -233,9 +186,7 @@ describe('Remix Application', () => {
// ASSERT
expectTargetsToBeCorrect(tree, newAppDir);
expect(
tree.read(`${newAppDir}/remix.config.js`, 'utf-8')
).toMatchSnapshot();
expect(tree.exists(`${newAppDir}/remix.config.js`)).toBeFalsy();
expect(
tree.read(`${newAppDir}/app/root.tsx`, 'utf-8')
).toMatchSnapshot();
@ -259,9 +210,7 @@ describe('Remix Application', () => {
// ASSERT
expectTargetsToBeCorrect(tree, newAppDir);
expect(
tree.read(`${newAppDir}/remix.config.js`, 'utf-8')
).toMatchSnapshot();
expect(tree.exists(`${newAppDir}/remix.config.js`)).toBeFalsy();
expect(
tree.read(`${newAppDir}/app/root.tsx`, 'utf-8')
).toMatchSnapshot();
@ -286,9 +235,7 @@ describe('Remix Application', () => {
// ASSERT
expectTargetsToBeCorrect(tree, appDir);
expect(
tree.read(`${appDir}/remix.config.js`, 'utf-8')
).toMatchSnapshot();
expect(tree.exists(`${appDir}/remix.config.js`)).toBeFalsy();
expect(
tree.read(`${appDir}/vitest.config.ts`, 'utf-8')
).toMatchSnapshot();
@ -312,9 +259,7 @@ describe('Remix Application', () => {
// ASSERT
expectTargetsToBeCorrect(tree, appDir);
expect(
tree.read(`${appDir}/remix.config.js`, 'utf-8')
).toMatchSnapshot();
expect(tree.exists(`${appDir}/remix.config.js`)).toBeFalsy();
expect(
tree.read(`${appDir}/jest.config.ts`, 'utf-8')
).toMatchSnapshot();

View File

@ -4,19 +4,16 @@ import {
formatFiles,
generateFiles,
GeneratorCallback,
getPackageManagerCommand,
joinPathFragments,
offsetFromRoot,
readJson,
readProjectConfiguration,
runTasksInSerial,
toJS,
Tree,
updateJson,
updateProjectConfiguration,
visitNotIgnoredFiles,
} from '@nx/devkit';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command';
import { initGenerator as jsInitGenerator } from '@nx/js';
import { extractTsConfigBase } from '@nx/js/src/utils/typescript/create-ts-config';
@ -37,10 +34,16 @@ import {
typescriptVersion,
typesReactDomVersion,
typesReactVersion,
viteVersion,
} from '../../utils/versions';
import initGenerator from '../init/init';
import { updateDependencies } from '../utils/update-dependencies';
import { addE2E, normalizeOptions, updateUnitTestConfig } from './lib';
import {
addE2E,
normalizeOptions,
updateUnitTestConfig,
addViteTempFilesToGitIgnore,
} from './lib';
import { NxRemixGeneratorSchema } from './schema';
export function remixApplicationGenerator(
@ -48,7 +51,7 @@ export function remixApplicationGenerator(
options: NxRemixGeneratorSchema
) {
return remixApplicationGeneratorInternal(tree, {
addPlugin: false,
addPlugin: true,
...options,
});
}
@ -60,62 +63,26 @@ export async function remixApplicationGeneratorInternal(
assertNotUsingTsSolutionSetup(tree, 'remix', 'application');
const options = await normalizeOptions(tree, _options);
if (!options.addPlugin) {
throw new Error(
`To generate a new Remix Vite application, you must use Inference Plugins. Check you do not have NX_ADD_PLUGINS=false or useInferencePlugins: false in your nx.json.`
);
}
const tasks: GeneratorCallback[] = [
await initGenerator(tree, {
skipFormat: true,
addPlugin: options.addPlugin,
addPlugin: true,
}),
await jsInitGenerator(tree, { skipFormat: true }),
];
addBuildTargetDefaults(tree, '@nx/remix:build');
addProjectConfiguration(tree, options.projectName, {
root: options.projectRoot,
sourceRoot: `${options.projectRoot}`,
projectType: 'application',
tags: options.parsedTags,
targets: !options.addPlugin
? {
build: {
executor: '@nx/remix:build',
outputs: ['{options.outputPath}'],
options: {
outputPath: joinPathFragments('dist', options.projectRoot),
},
},
serve: {
executor: `@nx/remix:serve`,
options: {
command: `${
getPackageManagerCommand().exec
} remix-serve build/index.js`,
manual: true,
port: 4200,
},
},
start: {
dependsOn: ['build'],
command: `remix-serve build/index.js`,
options: {
cwd: options.projectRoot,
},
},
['serve-static']: {
dependsOn: ['build'],
command: `remix-serve build/index.js`,
options: {
cwd: options.projectRoot,
},
},
typecheck: {
command: `tsc --project tsconfig.app.json`,
options: {
cwd: options.projectRoot,
},
},
}
: {},
targets: {},
});
const installTask = updateDependencies(tree);
@ -142,6 +109,7 @@ export async function remixApplicationGeneratorInternal(
typesReactDomVersion,
eslintVersion,
typescriptVersion,
viteVersion,
};
generateFiles(
@ -186,7 +154,7 @@ export async function remixApplicationGeneratorInternal(
skipFormat: true,
testEnvironment: 'jsdom',
skipViteConfig: true,
addPlugin: options.addPlugin,
addPlugin: true,
});
createOrEditViteConfig(
tree,
@ -216,7 +184,7 @@ export async function remixApplicationGeneratorInternal(
skipSerializers: false,
skipPackageJson: false,
skipFormat: true,
addPlugin: options.addPlugin,
addPlugin: true,
});
const projectConfig = readProjectConfiguration(tree, options.projectName);
if (projectConfig.targets['test']?.options) {
@ -267,10 +235,6 @@ export async function remixApplicationGeneratorInternal(
]);
}
if (options.js) {
toJS(tree);
}
if (options.rootProject && tree.exists('tsconfig.base.json')) {
// If this is a standalone project, merge tsconfig.json and tsconfig.base.json.
const tsConfigBaseJson = readJson(tree, 'tsconfig.base.json');
@ -371,6 +335,7 @@ export default {...nxPreset};
}
}
addViteTempFilesToGitIgnore(tree);
if (!options.skipFormat) {
await formatFiles(tree);
}

View File

@ -0,0 +1,18 @@
/**
* By default, Remix will handle hydrating your app on the client for you.
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
* For more information, see https://remix.run/file-conventions/entry.client
*/
import { RemixBrowser } from "@remix-run/react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<RemixBrowser />
</StrictMode>
);
});

View File

@ -0,0 +1,140 @@
/**
* By default, Remix will handle generating the HTTP Response for you.
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
* For more information, see https://remix.run/file-conventions/entry.server
*/
import { PassThrough } from "node:stream";
import type { AppLoadContext, EntryContext } from "@remix-run/node";
import { createReadableStreamFromReadable } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import { isbot } from "isbot";
import { renderToPipeableStream } from "react-dom/server";
const ABORT_DELAY = 5_000;
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
// This is ignored so we can keep it in the template for visibility. Feel
// free to delete this parameter in your app if you're not using it!
// eslint-disable-next-line @typescript-eslint/no-unused-vars
loadContext: AppLoadContext
) {
return isbot(request.headers.get("user-agent") || "")
? handleBotRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
)
: handleBrowserRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
);
}
function handleBotRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
onAllReady() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);
responseHeaders.set("Content-Type", "text/html");
resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
})
);
pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
}
);
setTimeout(abort, ABORT_DELAY);
});
}
function handleBrowserRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
onShellReady() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);
responseHeaders.set("Content-Type", "text/html");
resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
})
);
pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
}
);
setTimeout(abort, ABORT_DELAY);
});
}

View File

@ -1,18 +1,30 @@
import type { MetaFunction } from "@remix-run/node";
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
import type { MetaFunction, LinksFunction } from "@remix-run/node";
export const meta: MetaFunction = () => ([{
title: "New Remix App",
}]);
export default function App() {
export const links: LinksFunction = () => [
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
{
rel: "preconnect",
href: "https://fonts.gstatic.com",
crossOrigin: "anonymous",
},
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
},
];
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
@ -22,11 +34,14 @@ export default function App() {
<Links />
</head>
<body>
<Outlet />
{children}
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}

View File

@ -1,17 +0,0 @@
import {createWatchPaths} from '@nx/remix';
import {dirname} from 'path';
import {fileURLToPath} from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* @type {import('@remix-run/dev').AppConfig}
*/
export default {
ignoredRouteFiles: ["**/.*"],
// appDirectory: "app",
// assetsBuildDirectory: "public/build",
// serverBuildPath: "build/index.js",
// publicPath: "/build/",
watchPaths: () => createWatchPaths(__dirname),
};

View File

@ -1,2 +0,0 @@
/// <reference types="@remix-run/dev" />
/// <reference types="@remix-run/node" />

View File

@ -1,11 +1,14 @@
{
"extends": "./tsconfig.json",
"include": [
"remix.env.d.ts",
"app/**/*.ts",
"app/**/*.tsx",
"app/**/*.js",
"app/**/*.jsx"
"app/**/*.jsx",
"**/.server/**/*.ts",
"**/.server/**/*.tsx",
"**/.client/**/*.ts",
"**/.client/**/*.tsx"
],
"exclude": [
"tests/**/*.spec.ts",

View File

@ -2,16 +2,19 @@
"extends": "<%= offsetFromRoot %>tsconfig.base.json",
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2019"],
"types": ["@remix-run/node", "vite/client"],
"isolatedModules": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"moduleResolution": "node",
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"target": "ES2019",
"target": "ES2022",
"strict": true,
"allowJs": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
// Remix takes care of building everything in `remix build`.
// Vite takes care of building everything.
"noEmit": true
},
"include": [],

View File

@ -0,0 +1,25 @@
import { vitePlugin as remix } from '@remix-run/dev';
import { defineConfig } from 'vite';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
declare module '@remix-run/node' {
interface Future {
v3_singleFetch: true;
}
}
export default defineConfig({
root: __dirname,
plugins: [
remix({
future: {
v3_fetcherPersist: true,
v3_relativeSplatPath: true,
v3_throwAbortReason: true,
v3_singleFetch: true,
v3_lazyRouteDiscovery: true,
},
}),
nxViteTsPaths(),
],
});

View File

@ -1,8 +1,6 @@
{
"private": true,
"name": "<%= projectName %>",
"description": "",
"license": "",
"scripts": {},
"type": "module",
"dependencies": {
@ -18,10 +16,11 @@
"@types/react": "<%= typesReactVersion %>",
"@types/react-dom": "<%= typesReactDomVersion %>",
"eslint": "<%= eslintVersion %>",
"typescript": "<%= typescriptVersion %>"
"typescript": "<%= typescriptVersion %>",
"vite": "<%= viteVersion %>",
},
"engines": {
"node": ">=14"
"node": ">=20"
},
"sideEffects": false
}

View File

@ -83,10 +83,7 @@ export async function addE2E(tree: Tree, options: NormalizedSchema) {
tree,
'@nx/cypress/plugin',
buildTarget,
joinPathFragments(
options.e2eProjectRoot,
`cypress.config.${options.js ? 'js' : 'ts'}`
)
joinPathFragments(options.e2eProjectRoot, `cypress.config.ts`)
);
}

View File

@ -0,0 +1,16 @@
import { stripIndents, Tree } from '@nx/devkit';
export function addViteTempFilesToGitIgnore(tree: Tree) {
let newGitIgnoreContents = `**/vite.config.{js,ts,mjs,mts,cjs,cts}.timestamp*`;
if (tree.exists('.gitignore')) {
const gitIgnoreContents = tree.read('.gitignore', 'utf-8');
if (!gitIgnoreContents.includes(newGitIgnoreContents)) {
newGitIgnoreContents = stripIndents`${gitIgnoreContents}
${newGitIgnoreContents}`;
tree.write('.gitignore', newGitIgnoreContents);
}
} else {
tree.write('.gitignore', newGitIgnoreContents);
}
}

View File

@ -1,3 +1,4 @@
export * from './normalize-options';
export * from './update-unit-test-config';
export * from './add-e2e';
export * from './add-vite-temp-files-to-gitignore';

View File

@ -4,7 +4,6 @@ export interface NxRemixGeneratorSchema {
directory: string;
name?: string;
tags?: string;
js?: boolean;
linter?: Linter | LinterType;
unitTestRunner?: 'vitest' | 'jest' | 'none';
e2eTestRunner?: 'cypress' | 'playwright' | 'none';

View File

@ -20,11 +20,6 @@
"description": "The name of the application.",
"x-priority": "important"
},
"js": {
"type": "boolean",
"description": "Generate JavaScript files rather than TypeScript files.",
"default": false
},
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",

View File

@ -25,7 +25,7 @@ export async function convertToInferred(tree: Tree, options: Schema) {
buildTargetName: 'build',
devTargetName: 'dev',
startTargetName: 'start',
staticServeTargetName: 'static-serve',
serveStaticTargetName: 'serve-static',
typecheckTargetName: 'typecheck',
},
[

View File

@ -18,13 +18,13 @@ describe('Remix Init Generator', () => {
const pkgJson = readJson(tree, 'package.json');
expect(pkgJson.dependencies).toMatchInlineSnapshot(`
{
"@remix-run/serve": "^2.8.1",
"@remix-run/serve": "^2.13.1",
}
`);
expect(pkgJson.devDependencies).toMatchInlineSnapshot(`
{
"@nx/web": "0.0.1",
"@remix-run/dev": "^2.8.1",
"@remix-run/dev": "^2.13.1",
}
`);
@ -70,13 +70,13 @@ describe('Remix Init Generator', () => {
const pkgJson = readJson(tree, 'package.json');
expect(pkgJson.dependencies).toMatchInlineSnapshot(`
{
"@remix-run/serve": "^2.8.1",
"@remix-run/serve": "^2.13.1",
}
`);
expect(pkgJson.devDependencies).toMatchInlineSnapshot(`
{
"@nx/web": "0.0.1",
"@remix-run/dev": "^2.8.1",
"@remix-run/dev": "^2.13.1",
}
`);
});

View File

@ -7,7 +7,6 @@ export interface NormalizedSchema extends RemixGeneratorSchema {
parsedTags: string[];
unitTestRunner?: 'jest' | 'none' | 'vitest';
e2eTestRunner?: 'cypress' | 'none';
js?: boolean;
}
export function normalizeOptions(

View File

@ -26,7 +26,6 @@ export default async function (tree: Tree, _options: RemixGeneratorSchema) {
rootProject: true,
unitTestRunner: options.unitTestRunner ?? 'vitest',
e2eTestRunner: options.e2eTestRunner ?? 'cypress',
js: options.js ?? false,
addPlugin: addPluginDefault,
});
tasks.push(appGenTask);

View File

@ -1,86 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`setup-tailwind generator should add a js tailwind config to an application correctly 1`] = `
"import { createGlobPatternsForDependencies } from '@nx/react/tailwind';
export default {
content: [
'./app/**/*.{js,jsx,ts,tsx}',
...createGlobPatternsForDependencies(__dirname),
],
theme: {
extend: {},
},
plugins: [],
};
"
`;
exports[`setup-tailwind generator should add a js tailwind config to an application correctly 2`] = `
"@tailwind base;
@tailwind components;
@tailwind utilities;
"
`;
exports[`setup-tailwind generator should add a js tailwind config to an application correctly 3`] = `
"import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from '@remix-run/react';
import twStyles from './tailwind.css';
export const links = () => [{ rel: 'stylesheet', href: twStyles }];
export const meta = () => [
{
title: 'New Remix App',
},
];
export default function App() {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
"
`;
exports[`setup-tailwind generator should add a js tailwind config to an application correctly 4`] = `
"import { createWatchPaths } from '@nx/remix';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* @type {import('@remix-run/dev').AppConfig}
*/
export default {
tailwind: true,
ignoredRouteFiles: ['**/.*'],
// appDirectory: "app",
// assetsBuildDirectory: "public/build",
// serverBuildPath: "build/index.js",
// publicPath: "/build/",
watchPaths: () => createWatchPaths(__dirname),
};
"
`;
exports[`setup-tailwind generator should add a tailwind config to an application correctly 1`] = `
"import type { Config } from 'tailwindcss';
import { createGlobPatternsForDependencies } from '@nx/react/tailwind';
@ -99,26 +18,32 @@ export default {
`;
exports[`setup-tailwind generator should add a tailwind config to an application correctly 2`] = `
"export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
"
`;
exports[`setup-tailwind generator should add a tailwind config to an application correctly 3`] = `
"@tailwind base;
@tailwind components;
@tailwind utilities;
"
`;
exports[`setup-tailwind generator should add a tailwind config to an application correctly 3`] = `
"import type { MetaFunction, LinksFunction } from '@remix-run/node';
import {
exports[`setup-tailwind generator should add a tailwind config to an application correctly 4`] = `
"import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from '@remix-run/react';
import type { MetaFunction, LinksFunction } from '@remix-run/node';
import twStyles from './tailwind.css';
export const links: LinksFunction = () => [
{ rel: 'stylesheet', href: twStyles },
];
export const meta: MetaFunction = () => [
{
@ -126,7 +51,21 @@ export const meta: MetaFunction = () => [
},
];
export default function App() {
export const links: LinksFunction = () => [
{ rel: 'stylesheet', href: twStyles },
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
{
rel: 'preconnect',
href: 'https://fonts.gstatic.com',
crossOrigin: 'anonymous',
},
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
},
];
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
@ -136,35 +75,16 @@ export default function App() {
<Links />
</head>
<body>
<Outlet />
{children}
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
"
`;
exports[`setup-tailwind generator should add a tailwind config to an application correctly 4`] = `
"import { createWatchPaths } from '@nx/remix';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* @type {import('@remix-run/dev').AppConfig}
*/
export default {
tailwind: true,
ignoredRouteFiles: ['**/.*'],
// appDirectory: "app",
// assetsBuildDirectory: "public/build",
// serverBuildPath: "build/index.js",
// publicPath: "/build/",
watchPaths: () => createWatchPaths(__dirname),
};
export default function App() {
return <Outlet />;
}
"
`;

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -1 +0,0 @@
export * from './update-remix-config';

View File

@ -1,79 +0,0 @@
import { stripIndents } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { updateRemixConfig } from './update-remix-config';
describe('updateRemixConfig', () => {
it('should add tailwind property to an existing config that doesnt have it', () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
tree.write(
`remix.config.js`,
stripIndents`module.exports = {
ignoredRouteFiles: ['**/.*'],
watchPaths: ['../../libs']
};`
);
// ACT
updateRemixConfig(tree, '.');
// ASSERT
expect(tree.read('remix.config.js', 'utf-8')).toMatchInlineSnapshot(`
"module.exports = {
tailwind: true,
ignoredRouteFiles: ['**/.*'],
watchPaths: ['../../libs']
};"
`);
});
it('should update tailwind property if the config has it and set to false', () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
tree.write(
`remix.config.js`,
stripIndents`module.exports = {
ignoredRouteFiles: ['**/.*'],
tailwind: false,
watchPaths: ['../../libs']
};`
);
// ACT
updateRemixConfig(tree, '.');
// ASSERT
expect(tree.read('remix.config.js', 'utf-8')).toMatchInlineSnapshot(`
"module.exports = {
ignoredRouteFiles: ['**/.*'],
tailwind: true,
watchPaths: ['../../libs']
};"
`);
});
it('should not update tailwind property if the config has it and set to true', () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
tree.write(
`remix.config.js`,
stripIndents`module.exports = {
ignoredRouteFiles: ['**/.*'],
tailwind: true,
watchPaths: ['../../libs']
};`
);
// ACT
updateRemixConfig(tree, '.');
// ASSERT
expect(tree.read('remix.config.js', 'utf-8')).toMatchInlineSnapshot(`
"module.exports = {
ignoredRouteFiles: ['**/.*'],
tailwind: true,
watchPaths: ['../../libs']
};"
`);
});
});

View File

@ -1,48 +0,0 @@
import { joinPathFragments, type Tree } from '@nx/devkit';
import { tsquery } from '@phenomnomnominal/tsquery';
import { getRemixConfigPathFromProjectRoot } from '../../../utils/remix-config';
export function updateRemixConfig(tree: Tree, projectRoot: string) {
const pathToRemixConfig = getRemixConfigPathFromProjectRoot(
tree,
projectRoot
);
const fileContents = tree.read(pathToRemixConfig, 'utf-8');
const REMIX_CONFIG_OBJECT_SELECTOR = 'ObjectLiteralExpression';
const ast = tsquery.ast(fileContents);
const nodes = tsquery(ast, REMIX_CONFIG_OBJECT_SELECTOR, {
visitAllChildren: true,
});
if (nodes.length === 0) {
throw new Error(`Remix Config is not valid, unable to update the file.`);
}
const configObjectNode = nodes[0];
const propertyNodes = tsquery(configObjectNode, 'PropertyAssignment', {
visitAllChildren: true,
});
for (const propertyNode of propertyNodes) {
const nodeText = propertyNode.getText();
if (nodeText.includes('tailwind') && nodeText.includes('true')) {
return;
} else if (nodeText.includes('tailwind') && nodeText.includes('false')) {
const updatedFileContents = `${fileContents.slice(
0,
propertyNode.getStart()
)}tailwind: true${fileContents.slice(propertyNode.getEnd())}`;
tree.write(pathToRemixConfig, updatedFileContents);
return;
}
}
const updatedFileContents = `${fileContents.slice(
0,
configObjectNode.getStart() + 1
)}\ntailwind: true,${fileContents.slice(configObjectNode.getStart() + 1)}`;
tree.write(pathToRemixConfig, updatedFileContents);
}

View File

@ -1,5 +1,4 @@
export interface SetupTailwindSchema {
project: string;
js?: boolean;
skipFormat?: boolean;
}

View File

@ -20,11 +20,6 @@
"x-prompt": "What project would you like to add Tailwind to?",
"pattern": "^[a-zA-Z].*$"
},
"js": {
"type": "boolean",
"description": "Generate a JavaScript config file instead of a TypeScript config file",
"default": false
},
"skipFormat": {
"type": "boolean",
"description": "Skip formatting files after generator runs",

View File

@ -21,35 +21,11 @@ describe('setup-tailwind generator', () => {
// ASSERT
expect(tree.exists('tailwind.config.ts')).toBeTruthy();
expect(tree.read('tailwind.config.ts', 'utf-8')).toMatchSnapshot();
expect(tree.exists('postcss.config.js')).toBeTruthy();
expect(tree.read('postcss.config.js', 'utf-8')).toMatchSnapshot();
expect(tree.exists('app/tailwind.css')).toBeTruthy();
expect(tree.read('app/tailwind.css', 'utf-8')).toMatchSnapshot();
expect(tree.read('app/root.tsx', 'utf-8')).toMatchSnapshot();
expect(tree.read('remix.config.js', 'utf-8')).toMatchSnapshot();
expect(
readJson(tree, 'package.json').dependencies['tailwindcss']
).toBeTruthy();
});
it('should add a js tailwind config to an application correctly', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
await applicationGenerator(tree, {
name: 'test',
directory: '.',
js: true,
rootProject: true,
});
// ACT
await setupTailwind(tree, { project: 'test', js: true });
// ASSERT
expect(tree.exists('tailwind.config.js')).toBeTruthy();
expect(tree.read('tailwind.config.js', 'utf-8')).toMatchSnapshot();
expect(tree.exists('app/tailwind.css')).toBeTruthy();
expect(tree.read('app/tailwind.css', 'utf-8')).toMatchSnapshot();
expect(tree.read('app/root.js', 'utf-8')).toMatchSnapshot();
expect(tree.read('remix.config.js', 'utf-8')).toMatchSnapshot();
expect(
readJson(tree, 'package.json').dependencies['tailwindcss']
).toBeTruthy();

View File

@ -5,13 +5,15 @@ import {
installPackagesTask,
joinPathFragments,
readProjectConfiguration,
toJS,
type Tree,
} from '@nx/devkit';
import { upsertLinksFunction } from '../../utils/upsert-links-function';
import { tailwindVersion } from '../../utils/versions';
import { updateRemixConfig } from './lib';
import {
autoprefixerVersion,
postcssVersion,
tailwindVersion,
} from '../../utils/versions';
import type { SetupTailwindSchema } from './schema';
export default async function setupTailwind(
@ -25,18 +27,10 @@ export default async function setupTailwind(
);
}
updateRemixConfig(tree, project.root);
generateFiles(tree, joinPathFragments(__dirname, 'files'), project.root, {
tpl: '',
});
if (options.js) {
tree.rename(
joinPathFragments(project.root, 'app/root.js'),
joinPathFragments(project.root, 'app/root.tsx')
);
}
const pathToRoot = joinPathFragments(project.root, 'app/root.tsx');
upsertLinksFunction(
tree,
@ -50,14 +44,12 @@ export default async function setupTailwind(
tree,
{
tailwindcss: tailwindVersion,
postcss: postcssVersion,
autoprefixer: autoprefixerVersion,
},
{}
);
if (options.js) {
toJS(tree);
}
if (!options.skipFormat) {
await formatFiles(tree);
}

View File

@ -9,7 +9,7 @@ import presetGenerator from '../preset/preset.impl';
import routeGenerator from '../route/route.impl';
import styleGenerator from './style.impl';
describe('route', () => {
describe('style', () => {
let tree: Tree;
beforeEach(() => {
@ -63,21 +63,10 @@ describe('route', () => {
it('should place styles correctly when app dir is changed', async () => {
await applicationGenerator(tree, { name: 'demo', directory: 'apps/demo' });
tree.write(
'apps/demo/remix.config.js',
`
/**
* @type {import('@remix-run/dev').AppConfig}
*/
module.exports = {
ignoredRouteFiles: ["**/.*"],
appDirectory: "my-custom-dir",
};`
);
(remixConfigUtils.getRemixConfigValues as jest.Mock) = jest.fn(() =>
Promise.resolve({
ignoredRouteFiles: ['**/.*'],
appDirectory: 'my-custom-dir',
appDirectory: 'apps/demo/my-custom-dir',
})
);

View File

@ -2,12 +2,14 @@ import { type Tree, addDependenciesToPackageJson } from '@nx/devkit';
import {
eslintVersion,
isbotVersion,
nxVersion,
reactDomVersion,
reactVersion,
remixVersion,
typescriptVersion,
typesReactDomVersion,
typesReactVersion,
viteVersion,
} from '../../utils/versions';
export function updateDependencies(tree: Tree) {
@ -25,6 +27,8 @@ export function updateDependencies(tree: Tree) {
'@types/react-dom': typesReactDomVersion,
eslint: eslintVersion,
typescript: typescriptVersion,
vite: viteVersion,
'@nx/vite': nxVersion,
}
);
}

View File

@ -338,17 +338,22 @@ async function getBuildPaths(
// do nothing
}
const { resolveConfig } = await loadViteDynamicImport();
const viteBuildConfig = await resolveConfig(
const viteBuildConfig = (await resolveConfig(
{
configFile: configPath,
mode: 'development',
},
'build'
);
)) as any;
return {
buildDirectory: viteBuildConfig.build?.outDir ?? 'build',
serverBuildPath: viteBuildConfig.build?.outDir ?? 'build',
serverBuildPath: viteBuildConfig.build?.outDir
? join(
dirname(viteBuildConfig.build?.outDir),
`server/${viteBuildConfig.__remixPluginContext?.remixConfig.serverBuildFile}`
)
: 'build',
assetsBuildDirectory: 'build/client',
};
}

View File

@ -5,7 +5,26 @@ import {
workspaceRoot,
} from '@nx/devkit';
import type { AppConfig } from '@remix-run/dev';
import { createContext, SourceTextModule } from 'vm';
import { loadViteDynamicImport } from './executor-utils';
export function getRemixConfigPathDetails(tree: Tree, projectName: string) {
const project = readProjectConfiguration(tree, projectName);
if (!project) throw new Error(`Project does not exist: ${projectName}`);
for (const ext of ['.mjs', '.cjs', '.js', '.mts', '.cts', '.ts']) {
const configPath = joinPathFragments(project.root, `vite.config${ext}`);
if (tree.exists(configPath)) {
return [configPath, 'vite'];
}
}
for (const ext of ['.mjs', '.cjs', '.js']) {
const configPath = joinPathFragments(project.root, `remix.config${ext}`);
if (tree.exists(configPath)) {
return [configPath, 'classic'];
}
}
}
export function getRemixConfigPath(tree: Tree, projectName: string) {
const project = readProjectConfiguration(tree, projectName);
@ -39,21 +58,32 @@ export function getRemixConfigPathFromProjectRoot(
const _remixConfigCache: Record<string, AppConfig> = {};
export async function getRemixConfigValues(tree: Tree, projectName: string) {
const remixConfigPath = joinPathFragments(
workspaceRoot,
getRemixConfigPath(tree, projectName)
);
const [configPath, configType] = getRemixConfigPathDetails(tree, projectName);
const remixConfigPath = joinPathFragments(workspaceRoot, configPath);
const cacheKey = `${projectName}/${remixConfigPath}`;
let appConfig = _remixConfigCache[cacheKey];
let resolvedConfig: any;
if (!appConfig) {
try {
const importedConfig = await Function(
`return import("${remixConfigPath}?t=${Date.now()}")`
)();
appConfig = (importedConfig?.default || importedConfig) as AppConfig;
} catch {
appConfig = require(remixConfigPath);
if (configType === 'vite') {
const { resolveConfig } = await loadViteDynamicImport();
const viteBuildConfig = (await resolveConfig(
{
configFile: configPath,
mode: 'development',
},
'build'
)) as any;
appConfig = viteBuildConfig.__remixPluginContext?.remixConfig;
} else {
try {
const importedConfig = await Function(
`return import("${remixConfigPath}?t=${Date.now()}")`
)();
appConfig = (importedConfig?.default || importedConfig) as AppConfig;
} catch {
appConfig = require(remixConfigPath);
}
}
_remixConfigCache[cacheKey] = appConfig;
}

View File

@ -5,6 +5,7 @@ import {
Tree,
} from '@nx/devkit';
import { getRemixConfigValues } from './remix-config';
import { relative } from 'path';
/**
*
@ -91,5 +92,10 @@ export async function resolveRemixAppDirectory(
const project = readProjectConfiguration(tree, projectName);
const remixConfig = await getRemixConfigValues(tree, projectName);
return joinPathFragments(project.root, remixConfig.appDirectory ?? 'app');
return joinPathFragments(
project.root,
remixConfig.appDirectory
? relative(project.root, remixConfig.appDirectory)
: 'app'
);
}

View File

@ -2,7 +2,7 @@ import { readJson, Tree } from '@nx/devkit';
export const nxVersion = require('../../package.json').version;
export const remixVersion = '^2.8.1';
export const remixVersion = '^2.13.1';
export const isbotVersion = '^4.4.0';
export const reactVersion = '^18.2.0';
export const reactDomVersion = '^18.2.0';
@ -11,10 +11,13 @@ export const typesReactDomVersion = '^18.2.0';
export const eslintVersion = '^8.56.0';
export const typescriptVersion = '~5.5.2';
export const tailwindVersion = '^3.3.0';
export const postcssVersion = '^8.4.38';
export const autoprefixerVersion = '^10.4.19';
export const testingLibraryReactVersion = '^14.1.2';
// TODO(colum): Unpin this when @testing-library/jest-dom pushes a fix
export const testingLibraryJestDomVersion = '6.4.2';
export const testingLibraryUserEventsVersion = '^14.5.2';
export const viteVersion = '^5.0.0';
export function getRemixVersion(tree: Tree): string {
return getPackageVersion(tree, '@remix-run/dev') ?? remixVersion;

View File

@ -1,4 +1,5 @@
export const nxVersion = require('../../package.json').version;
// Also update @nx/remix/utils/versions when changing vite version
export const viteVersion = '^5.0.0';
export const vitestVersion = '^1.3.1';
export const vitePluginReactVersion = '^4.2.0';

59
pnpm-lock.yaml generated
View File

@ -368,11 +368,11 @@ importers:
specifier: 1.9.0
version: 1.9.0(react-redux@8.0.5(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1))(react@18.3.1)
'@remix-run/dev':
specifier: ^2.8.1
version: 2.12.0(@remix-run/react@2.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.4))(@types/node@20.16.10)(less@4.1.3)(sass@1.55.0)(stylus@0.59.0)(terser@5.31.6)(ts-node@10.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@types/node@20.16.10)(typescript@5.5.4))(typescript@5.5.4)(vite@5.0.8(@types/node@20.16.10)(less@4.1.3)(sass@1.55.0)(stylus@0.59.0)(terser@5.31.6))
specifier: ^2.13.1
version: 2.13.1(@remix-run/react@2.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.4))(@types/node@20.16.10)(less@4.1.3)(sass@1.55.0)(stylus@0.59.0)(terser@5.31.6)(ts-node@10.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@types/node@20.16.10)(typescript@5.5.4))(typescript@5.5.4)(vite@5.0.8(@types/node@20.16.10)(less@4.1.3)(sass@1.55.0)(stylus@0.59.0)(terser@5.31.6))
'@remix-run/node':
specifier: ^2.8.1
version: 2.12.0(typescript@5.5.4)
specifier: ^2.13.1
version: 2.13.1(typescript@5.5.4)
'@rollup/plugin-babel':
specifier: ^6.0.4
version: 6.0.4(@babel/core@7.25.2)(@types/babel__core@7.20.5)(rollup@4.22.0)
@ -5322,13 +5322,13 @@ packages:
react-redux:
optional: true
'@remix-run/dev@2.12.0':
resolution: {integrity: sha512-/87YQORdlJg5YChd7nVBM/hRXHZA4GfUjhKbZyNrh03bazCQBF+6EsXbzpJ6cCFOpZgecsN0Xv648Qw0VuJjwg==}
'@remix-run/dev@2.13.1':
resolution: {integrity: sha512-7+06Dail6zMyRlRvgrZ4cmQjs2gUb+M24iP4jbmql+0B7VAAPwzCRU0x+BF5z8GSef13kDrH3iXv/BQ2O2yOgw==}
engines: {node: '>=18.0.0'}
hasBin: true
peerDependencies:
'@remix-run/react': ^2.12.0
'@remix-run/serve': ^2.12.0
'@remix-run/react': ^2.13.1
'@remix-run/serve': ^2.13.1
typescript: ^5.1.0
vite: ^5.1.0
wrangler: ^3.28.2
@ -5342,8 +5342,8 @@ packages:
wrangler:
optional: true
'@remix-run/node@2.12.0':
resolution: {integrity: sha512-83Jaoc6gpSuD4e6rCk7N5ZHAXNmDw4fJC+kPeDCsd6+wLtTLSi7u9Zo9/Q7moLZ3oyH+aR+LGdkxLULYv+Q6Og==}
'@remix-run/node@2.13.1':
resolution: {integrity: sha512-2ly7bENj2n2FNBdEN60ZEbNCs5dAOex/QJoo6EZ8RNFfUQxVKAZkMwfQ4ETV2SLWDgkRLj3Jo5n/dx7O2ZGhGw==}
engines: {node: '>=18.0.0'}
peerDependencies:
typescript: ^5.1.0
@ -5366,6 +5366,10 @@ packages:
resolution: {integrity: sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==}
engines: {node: '>=14.0.0'}
'@remix-run/router@1.20.0':
resolution: {integrity: sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==}
engines: {node: '>=14.0.0'}
'@remix-run/server-runtime@2.12.0':
resolution: {integrity: sha512-o9ukOr3XKmyY8UufTrDdkgD3fiy+z+f4qEzvCQnvC0+EasCyN9hb1Vbui6Koo/5HKvahC4Ga8RcWyvhykKrG3g==}
engines: {node: '>=18.0.0'}
@ -5375,6 +5379,15 @@ packages:
typescript:
optional: true
'@remix-run/server-runtime@2.13.1':
resolution: {integrity: sha512-2DfBPRcHKVzE4bCNsNkKB50BhCCKF73x+jiS836OyxSIAL+x0tguV2AEjmGXefEXc5AGGzoxkus0AUUEYa29Vg==}
engines: {node: '>=18.0.0'}
peerDependencies:
typescript: ^5.1.0
peerDependenciesMeta:
typescript:
optional: true
'@remix-run/web-blob@3.1.0':
resolution: {integrity: sha512-owGzFLbqPH9PlKb8KvpNJ0NO74HWE2euAn61eEiyCXX/oteoVzTVSN8mpLgDjaxBf2btj5/nUllSUgpyd6IH6g==}
@ -22716,7 +22729,7 @@ snapshots:
react: 18.3.1
react-redux: 8.0.5(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1)
'@remix-run/dev@2.12.0(@remix-run/react@2.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.4))(@types/node@20.16.10)(less@4.1.3)(sass@1.55.0)(stylus@0.59.0)(terser@5.31.6)(ts-node@10.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@types/node@20.16.10)(typescript@5.5.4))(typescript@5.5.4)(vite@5.0.8(@types/node@20.16.10)(less@4.1.3)(sass@1.55.0)(stylus@0.59.0)(terser@5.31.6))':
'@remix-run/dev@2.13.1(@remix-run/react@2.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.4))(@types/node@20.16.10)(less@4.1.3)(sass@1.55.0)(stylus@0.59.0)(terser@5.31.6)(ts-node@10.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@types/node@20.16.10)(typescript@5.5.4))(typescript@5.5.4)(vite@5.0.8(@types/node@20.16.10)(less@4.1.3)(sass@1.55.0)(stylus@0.59.0)(terser@5.31.6))':
dependencies:
'@babel/core': 7.25.2
'@babel/generator': 7.25.6
@ -22728,10 +22741,10 @@ snapshots:
'@babel/types': 7.25.6
'@mdx-js/mdx': 2.3.0
'@npmcli/package-json': 4.0.1
'@remix-run/node': 2.12.0(typescript@5.5.4)
'@remix-run/node': 2.13.1(typescript@5.5.4)
'@remix-run/react': 2.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.4)
'@remix-run/router': 1.19.2
'@remix-run/server-runtime': 2.12.0(typescript@5.5.4)
'@remix-run/router': 1.20.0
'@remix-run/server-runtime': 2.13.1(typescript@5.5.4)
'@types/mdx': 2.0.13
'@vanilla-extract/integration': 6.5.0(@types/node@20.16.10)(less@4.1.3)(sass@1.55.0)(stylus@0.59.0)(terser@5.31.6)
arg: 5.0.2
@ -22791,9 +22804,9 @@ snapshots:
- ts-node
- utf-8-validate
'@remix-run/node@2.12.0(typescript@5.5.4)':
'@remix-run/node@2.13.1(typescript@5.5.4)':
dependencies:
'@remix-run/server-runtime': 2.12.0(typescript@5.5.4)
'@remix-run/server-runtime': 2.13.1(typescript@5.5.4)
'@remix-run/web-fetch': 4.4.2
'@web3-storage/multipart-parser': 1.0.0
cookie-signature: 1.2.1
@ -22817,6 +22830,8 @@ snapshots:
'@remix-run/router@1.19.2': {}
'@remix-run/router@1.20.0': {}
'@remix-run/server-runtime@2.12.0(typescript@5.5.4)':
dependencies:
'@remix-run/router': 1.19.2
@ -22829,6 +22844,18 @@ snapshots:
optionalDependencies:
typescript: 5.5.4
'@remix-run/server-runtime@2.13.1(typescript@5.5.4)':
dependencies:
'@remix-run/router': 1.20.0
'@types/cookie': 0.6.0
'@web3-storage/multipart-parser': 1.0.0
cookie: 0.6.0
set-cookie-parser: 2.7.0
source-map: 0.7.3
turbo-stream: 2.4.0
optionalDependencies:
typescript: 5.5.4
'@remix-run/web-blob@3.1.0':
dependencies:
'@remix-run/web-stream': 1.1.0