chore: synchronize with flecks-vite
This commit is contained in:
parent
9494e0b901
commit
2e6e339d6d
|
@ -20,30 +20,19 @@ module.exports = {
|
|||
es6: true,
|
||||
},
|
||||
globals: {
|
||||
process: false,
|
||||
WeakRef: false,
|
||||
},
|
||||
ignorePatterns: ['!**/.server', '!**/.client'],
|
||||
rules: {
|
||||
'no-constant-condition': ['error', {checkLoops: false}],
|
||||
},
|
||||
|
||||
// Base config
|
||||
extends: ['eslint:recommended'],
|
||||
rules: {
|
||||
'no-constant-condition': ['error', {checkLoops: false}],
|
||||
'require-yield': 0,
|
||||
},
|
||||
|
||||
overrides: [
|
||||
// Tests
|
||||
{
|
||||
files: ['**/*.test.{js,jsx,ts,tsx}'],
|
||||
rules: {
|
||||
'no-empty-pattern': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// React
|
||||
{
|
||||
files: ['**/*.{jsx,tsx}'],
|
||||
files: ['**/*.{js,jsx}'],
|
||||
plugins: ['react', 'jsx-a11y'],
|
||||
extends: [
|
||||
'plugin:react/recommended',
|
||||
|
@ -57,8 +46,8 @@ module.exports = {
|
|||
},
|
||||
formComponents: ['Form'],
|
||||
linkComponents: [
|
||||
{ name: 'Link', linkAttribute: 'to' },
|
||||
{ name: 'NavLink', linkAttribute: 'to' },
|
||||
{name: 'Link', linkAttribute: 'to'},
|
||||
{name: 'NavLink', linkAttribute: 'to'},
|
||||
],
|
||||
},
|
||||
rules: {
|
||||
|
@ -72,15 +61,27 @@ module.exports = {
|
|||
// Node
|
||||
{
|
||||
files: [
|
||||
'app/websocket.js',
|
||||
'.eslintrc.cjs',
|
||||
'server.js',
|
||||
'**/.server/**',
|
||||
'*.server.{js,jsx}',
|
||||
'**/{build,node}.js',
|
||||
'vite.config.js',
|
||||
'vitest.workspace.js',
|
||||
],
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
|
||||
// game scripts
|
||||
{
|
||||
files: [
|
||||
'resources/**/*.js',
|
||||
],
|
||||
rules: {
|
||||
'require-yield': 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -2,7 +2,7 @@ node_modules
|
|||
|
||||
/.cache
|
||||
/build
|
||||
/coverage
|
||||
.env
|
||||
|
||||
/data
|
||||
/dev
|
||||
.env
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
process.env.STORYBOOK = 1
|
||||
process.env.REMIX_DISABLED = 1
|
||||
|
||||
/** @type { import('@storybook/react-vite').StorybookConfig } */
|
||||
const config = {
|
||||
stories: ['../stories/**/*.mdx', '../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||
addons: [
|
||||
'@chromatic-com/storybook',
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
],
|
||||
core: {
|
||||
disableTelemetry: true,
|
||||
},
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
stories: ['../app/**/*.stories.mdx', '../app/**/*.stories.@(js|jsx|mjs)'],
|
||||
};
|
||||
export default config;
|
||||
|
|
9
.vscode/css_custom_data.json
vendored
Normal file
9
.vscode/css_custom_data.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"version": 1.1,
|
||||
"atDirectives": [
|
||||
{
|
||||
"name": "@tailwind",
|
||||
"description": "Use the @tailwind directive to insert Tailwind's `base`, `components`, `utilities`, and `screens` styles into your CSS."
|
||||
}
|
||||
]
|
||||
}
|
49
.vscode/launch.json
vendored
49
.vscode/launch.json
vendored
|
@ -7,59 +7,38 @@
|
|||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Silphius Chrome",
|
||||
"url": "https://localhost:3000",
|
||||
"name": "Client",
|
||||
"webRoot": "${workspaceFolder}",
|
||||
"runtimeArgs": ["--auto-open-devtools-for-tabs"]
|
||||
"outputCapture": "std",
|
||||
"runtimeArgs": [
|
||||
"https://localhost:3200/",
|
||||
"https://localhost:3200/storybook"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Silphius Dev",
|
||||
"name": "Server",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"resolveSourceMapLocations": [],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {
|
||||
"MKCERT": "1",
|
||||
"PORT": "3200"
|
||||
},
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev"],
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Storybook Chrome",
|
||||
"url": "http://localhost:6006",
|
||||
"webRoot": "${workspaceFolder}",
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Storybook Dev",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"resolveSourceMapLocations": [],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "storybook", "--", "--no-open"],
|
||||
},
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "Silphius",
|
||||
"name": "App",
|
||||
"configurations": [
|
||||
"Silphius Dev",
|
||||
"Silphius Chrome",
|
||||
"Server",
|
||||
"Client",
|
||||
],
|
||||
"stopAll": true,
|
||||
},
|
||||
{
|
||||
"name": "Storybook",
|
||||
"configurations": [
|
||||
"Storybook Dev",
|
||||
"Storybook Chrome",
|
||||
],
|
||||
"stopAll": true,
|
||||
}
|
||||
]
|
||||
}
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"css.customData": [".vscode/css_custom_data.json"]
|
||||
}
|
|
@ -4,9 +4,9 @@
|
|||
* 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";
|
||||
import {RemixBrowser} from '@remix-run/react';
|
||||
import {startTransition, StrictMode} from 'react';
|
||||
import {hydrateRoot} from 'react-dom/client';
|
||||
|
||||
startTransition(() => {
|
||||
hydrateRoot(
|
||||
|
|
|
@ -4,17 +4,18 @@
|
|||
* For more information, see https://remix.run/file-conventions/entry.server
|
||||
*/
|
||||
|
||||
import { PassThrough } from "node:stream";
|
||||
import {PassThrough} from 'node:stream';
|
||||
|
||||
import { createReadableStreamFromReadable } from "@remix-run/node";
|
||||
import { RemixServer } from "@remix-run/react";
|
||||
import { isbot } from "isbot";
|
||||
import { renderToPipeableStream } from "react-dom/server";
|
||||
import {createReadableStreamFromReadable} from '@remix-run/node';
|
||||
import {RemixServer} from '@remix-run/react';
|
||||
import {isbot} from 'isbot';
|
||||
import {renderToPipeableStream} from 'react-dom/server';
|
||||
import {hooks} from 'virtual:sylvite/server';
|
||||
|
||||
const ABORT_DELAY = 5_000;
|
||||
|
||||
export async function handleUpgrade(request, socket, head) {
|
||||
const {handleUpgrade} = await import('./silphius/server/websocket.js');
|
||||
const {handleUpgrade} = await import('./websocket.js');
|
||||
handleUpgrade(request, socket, head);
|
||||
}
|
||||
|
||||
|
@ -23,23 +24,21 @@ export default function handleRequest(
|
|||
responseStatusCode,
|
||||
responseHeaders,
|
||||
remixContext,
|
||||
// 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 no-unused-vars
|
||||
loadContext
|
||||
) {
|
||||
return isbot(request.headers.get("user-agent") || "")
|
||||
return isbot(request.headers.get('user-agent') || '')
|
||||
? handleBotRequest(
|
||||
request,
|
||||
responseStatusCode,
|
||||
responseHeaders,
|
||||
remixContext
|
||||
remixContext,
|
||||
)
|
||||
: handleBrowserRequest(
|
||||
request,
|
||||
responseStatusCode,
|
||||
responseHeaders,
|
||||
remixContext
|
||||
remixContext,
|
||||
loadContext,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -51,7 +50,7 @@ function handleBotRequest(
|
|||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let shellRendered = false;
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
const {pipe, abort} = renderToPipeableStream(
|
||||
<RemixServer
|
||||
context={remixContext}
|
||||
url={request.url}
|
||||
|
@ -63,7 +62,7 @@ function handleBotRequest(
|
|||
const body = new PassThrough();
|
||||
const stream = createReadableStreamFromReadable(body);
|
||||
|
||||
responseHeaders.set("Content-Type", "text/html");
|
||||
responseHeaders.set('Content-Type', 'text/html');
|
||||
|
||||
resolve(
|
||||
new Response(stream, {
|
||||
|
@ -97,11 +96,12 @@ function handleBrowserRequest(
|
|||
request,
|
||||
responseStatusCode,
|
||||
responseHeaders,
|
||||
remixContext
|
||||
remixContext,
|
||||
loadContext,
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let shellRendered = false;
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
const {pipe, abort} = renderToPipeableStream(
|
||||
<RemixServer
|
||||
context={remixContext}
|
||||
url={request.url}
|
||||
|
@ -110,15 +110,11 @@ function handleBrowserRequest(
|
|||
{
|
||||
onShellReady() {
|
||||
shellRendered = true;
|
||||
const body = new PassThrough();
|
||||
const body = hooks.call('@/sylvite/cha0s-remix:requestBody', new PassThrough(), request, loadContext);
|
||||
const stream = createReadableStreamFromReadable(body);
|
||||
|
||||
responseHeaders.set("Content-Type", "text/html");
|
||||
// security
|
||||
// responseHeaders.set("Cross-Origin-Embedder-Policy", "require-corp");
|
||||
// responseHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
|
||||
// responseHeaders.set("Cross-Origin-Resource-Policy", "same-site");
|
||||
// responseHeaders.set("Content-Security-Policy", "default-src 'self'");
|
||||
responseHeaders.set('Content-Type', 'text/html');
|
||||
hooks.call('@/sylvite/cha0s-remix:responseHeaders', responseHeaders, request, loadContext);
|
||||
|
||||
resolve(
|
||||
new Response(stream, {
|
||||
|
|
14
app/lib/event-source.js
Normal file
14
app/lib/event-source.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import streamResponse from './stream-response';
|
||||
|
||||
export function eventSource(request, init) {
|
||||
const headers = new Headers();
|
||||
headers.set('Content-Type', 'text/event-stream');
|
||||
return streamResponse(request, headers, (send, close) => {
|
||||
const encoder = new TextEncoder();
|
||||
function sendEvent(data, event = 'message') {
|
||||
send(encoder.encode(`event: ${event}\n`));
|
||||
send(encoder.encode(`data: ${data}\n\n`));
|
||||
}
|
||||
return init(sendEvent, close);
|
||||
});
|
||||
}
|
7
app/lib/long-poll.js
Normal file
7
app/lib/long-poll.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import streamResponse from './stream-response';
|
||||
|
||||
export function longPoll(request, init) {
|
||||
const headers = new Headers();
|
||||
headers.set('Content-Type', 'text/html');
|
||||
return streamResponse(request, headers, init);
|
||||
}
|
|
@ -6,4 +6,8 @@ export function singleton(key, value) {
|
|||
|
||||
singleton.reset = function (key) {
|
||||
delete global.__singletons[key];
|
||||
}
|
||||
};
|
||||
|
||||
singleton.set = function (key, value) {
|
||||
global.__singletons[key] = value;
|
||||
};
|
||||
|
|
30
app/lib/stream-response.js
Normal file
30
app/lib/stream-response.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
export default function streamResponse(request, headers, init) {
|
||||
headers.set('Connection', 'keep-alive');
|
||||
headers.set('Cache-Control', 'no-store, no-transform');
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
function send(chunk) {
|
||||
controller.enqueue(chunk);
|
||||
}
|
||||
const cleanup = init(send, close);
|
||||
let closed = false;
|
||||
function close() {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
cleanup();
|
||||
closed = true;
|
||||
request.signal.removeEventListener('abort', close);
|
||||
controller.close();
|
||||
}
|
||||
request.signal.addEventListener('abort', close);
|
||||
if (request.signal.aborted) {
|
||||
close();
|
||||
}
|
||||
},
|
||||
});
|
||||
return new Response(stream, {
|
||||
headers,
|
||||
status: 200,
|
||||
});
|
||||
}
|
33
app/lib/top-level-long-poll.js
Normal file
33
app/lib/top-level-long-poll.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
import {Buffer} from 'node:buffer';
|
||||
import {Transform} from 'node:stream';
|
||||
|
||||
const TERMINATION = Buffer.from('</body></html>');
|
||||
|
||||
export class TopLevelLongPoll extends Transform {
|
||||
|
||||
constructor(pump) {
|
||||
super();
|
||||
this.pump = pump;
|
||||
}
|
||||
|
||||
async _flush(done) {
|
||||
await this.pump((chunk) => {
|
||||
this.push(chunk);
|
||||
});
|
||||
this.push(TERMINATION);
|
||||
done();
|
||||
}
|
||||
|
||||
_transform(chunk, encoding, done) {
|
||||
if (0 === Buffer.compare(TERMINATION, chunk.slice(-TERMINATION.length))) {
|
||||
this.push(chunk.slice(0, -TERMINATION.length));
|
||||
done();
|
||||
}
|
||||
else {
|
||||
this.push(chunk);
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -4,11 +4,11 @@ import {
|
|||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
} from "@remix-run/react";
|
||||
} from '@remix-run/react';
|
||||
|
||||
import './root.css';
|
||||
|
||||
export function Layout({ children }) {
|
||||
export function Layout({children}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
|
17
app/silphius/build.js
Normal file
17
app/silphius/build.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import {fileURLToPath} from 'node:url';
|
||||
|
||||
export async function implement({hooks}) {
|
||||
hooks.tap('sylvite:viteConfig', (config) => ({
|
||||
...config,
|
||||
resolve: {
|
||||
...config.resolve,
|
||||
alias: [
|
||||
...config.resolve.alias,
|
||||
{
|
||||
find: '%',
|
||||
replacement: fileURLToPath(new URL('../../resources', import.meta.url))
|
||||
},
|
||||
],
|
||||
},
|
||||
}));
|
||||
}
|
15
app/silphius/node.js
Normal file
15
app/silphius/node.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import express from 'express';
|
||||
|
||||
export function implement({hooks}) {
|
||||
hooks.tap('@/sylvite/cha0s-remix:initializeExpress', () => {
|
||||
// patch pixi server context
|
||||
import('./server/pixi-context.js');
|
||||
});
|
||||
hooks.tap('@/sylvite/cha0s-remix:additionalAssets', (app) => {
|
||||
// resources
|
||||
app.use(
|
||||
'/resources',
|
||||
express.static('resources', { maxAge: '1h' }),
|
||||
);
|
||||
});
|
||||
}
|
10
app/sylvite/cha0s-remix/cors/node.js
Normal file
10
app/sylvite/cha0s-remix/cors/node.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import {randomBytes} from 'node:crypto';
|
||||
|
||||
export function implement({hooks}) {
|
||||
hooks.tap('@/sylvite/cha0s-remix:getRemixLoadContext', (context) => {
|
||||
return {
|
||||
...context,
|
||||
nonce: randomBytes(16).toString('hex'),
|
||||
};
|
||||
});
|
||||
}
|
31
app/sylvite/cha0s-remix/cors/server.js
Normal file
31
app/sylvite/cha0s-remix/cors/server.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import {Transform} from 'node:stream';
|
||||
|
||||
export function implement({hooks}) {
|
||||
hooks.tap('@/sylvite/cha0s-remix:responseHeaders', (headers, request, loadContext) => {
|
||||
if ('production' !== process.env.NODE_ENV) {
|
||||
return;
|
||||
}
|
||||
// high security
|
||||
headers.set('Cross-Origin-Embedder-Policy', 'require-corp');
|
||||
headers.set('Cross-Origin-Opener-Policy', 'same-origin');
|
||||
headers.set('Cross-Origin-Resource-Policy', 'same-site');
|
||||
headers.set('Content-Security-Policy', [
|
||||
'default-src', "'self'", 'ws:', 'wss:',
|
||||
`'nonce-${loadContext.nonce}'`,
|
||||
].join(' '));
|
||||
});
|
||||
class Noncer extends Transform {
|
||||
constructor(nonce) {
|
||||
super();
|
||||
this.nonce = nonce;
|
||||
}
|
||||
_transform(chunk, encoding, done) {
|
||||
const string = chunk.toString();
|
||||
this.push(string.replaceAll('<script', `<script nonce="${this.nonce}"`));
|
||||
done();
|
||||
}
|
||||
}
|
||||
hooks.tap('@/sylvite/cha0s-remix:requestBody', (stream, request, loadContext) => {
|
||||
return stream.pipe(new Noncer(loadContext.nonce));
|
||||
});
|
||||
}
|
33
app/sylvite/cha0s-remix/node.js
Normal file
33
app/sylvite/cha0s-remix/node.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
import compression from 'compression';
|
||||
|
||||
export function register({tapable: {SyncHook, SyncWaterfallHook}}) {
|
||||
const additionalAssets = new SyncHook(['app']);
|
||||
const beforeExpressCatchAll = new SyncHook(['app']);
|
||||
const getRemixLoadContext = new SyncWaterfallHook(['context', 'request']);
|
||||
const httpServerOptions = new SyncWaterfallHook(['options']);
|
||||
const initializeExpress = new SyncHook(['app']);
|
||||
return {
|
||||
additionalAssets,
|
||||
beforeExpressCatchAll,
|
||||
httpServerOptions,
|
||||
initializeExpress,
|
||||
getRemixLoadContext,
|
||||
};
|
||||
}
|
||||
|
||||
export function implement({config: {compression: useCompression = true}, hooks}) {
|
||||
hooks.tap('@/sylvite/cha0s-remix:initializeExpress', (app) => {
|
||||
// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
|
||||
app.disable('x-powered-by');
|
||||
app.set('trust proxy', process.env.TRUSTED_PROXIES);
|
||||
if (useCompression) {
|
||||
app.use(compression());
|
||||
}
|
||||
});
|
||||
hooks.tap('@/sylvite/cha0s-remix:getRemixLoadContext', (context, request) => {
|
||||
return {
|
||||
...context,
|
||||
ip: request.ip,
|
||||
};
|
||||
});
|
||||
}
|
21
app/sylvite/cha0s-remix/remix/build.js
Normal file
21
app/sylvite/cha0s-remix/remix/build.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import {dirname, relative, resolve} from 'node:path';
|
||||
|
||||
export function register({loaded, meta, tapable: {SyncHook}}) {
|
||||
const appDir = [meta.dirname, 'app'].join('/');
|
||||
const defineRemixRoutes = new SyncHook(['route']);
|
||||
// resolve paths relative to /app to avoid leaking path into manifest
|
||||
defineRemixRoutes.intercept({
|
||||
register: (tap) => {
|
||||
const {fn, name} = tap;
|
||||
const local = dirname(loaded[name].resolved);
|
||||
tap.fn = (route) => {
|
||||
fn((routePath, filePath, ...optionsAndOrChildren) => {
|
||||
route(routePath, relative(appDir, resolve(local, filePath)), ...optionsAndOrChildren);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
return {
|
||||
defineRemixRoutes,
|
||||
};
|
||||
}
|
8
app/sylvite/cha0s-remix/server.js
Normal file
8
app/sylvite/cha0s-remix/server.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
export function register({tapable: {SyncHook, SyncWaterfallHook}}) {
|
||||
const requestBody = new SyncWaterfallHook(['stream', 'request', 'loadContext']);
|
||||
const responseHeaders = new SyncHook(['headers', 'request', 'loadContext']);
|
||||
return {
|
||||
requestBody,
|
||||
responseHeaders,
|
||||
};
|
||||
}
|
3
app/websocket-routes.js
Normal file
3
app/websocket-routes.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export const routes = import.meta.glob(
|
||||
['./websocket/*.js', '!./websocket/*.test.js'],
|
||||
);
|
38
app/websocket.js
Normal file
38
app/websocket.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
import {WebSocketServer} from 'ws';
|
||||
|
||||
import {singleton} from '@/lib/singleton.js';
|
||||
|
||||
let wss = singleton('cha0s-remix$$wss');
|
||||
if (!wss) {
|
||||
wss = new WebSocketServer({
|
||||
clientTracking: !!import.meta.hot,
|
||||
noServer: true,
|
||||
});
|
||||
wss.on('connection', async (websocket, request) => {
|
||||
const {routes} = await import('./websocket-routes.js');
|
||||
const {pathname} = new URL(request.url, 'https://example.com');
|
||||
const M = routes[`./websocket${'/' === pathname ? '/_index' : pathname}.js`];
|
||||
if (!M) {
|
||||
websocket.close(1008, 'does not exist');
|
||||
return;
|
||||
}
|
||||
const {handleConnection} = await M();
|
||||
handleConnection(websocket, request);
|
||||
});
|
||||
singleton.set('cha0s-remix$$wss', wss);
|
||||
}
|
||||
|
||||
export function handleUpgrade(request, socket, head) {
|
||||
wss.handleUpgrade(request, socket, head, (websocket) => {
|
||||
wss.emit('connection', websocket, request);
|
||||
});
|
||||
}
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.on('vite:beforeUpdate', async () => {
|
||||
for (const websocket of wss.clients) {
|
||||
websocket.close(1001, 'hmr');
|
||||
}
|
||||
});
|
||||
import.meta.hot.accept();
|
||||
}
|
5
app/websocket/_index.js
Normal file
5
app/websocket/_index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
// This is the / route. Do something with the websocket.
|
||||
export async function handleConnection(websocket, request) { // eslint-disable-line no-unused-vars
|
||||
websocket.on('message', (message) => { // eslint-disable-line no-unused-vars
|
||||
});
|
||||
}
|
10
app/websocket/example.js
Normal file
10
app/websocket/example.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
export async function handleConnection(websocket, request) {
|
||||
websocket.on('message', (message) => {
|
||||
console.log(
|
||||
'[websocket] /example received',
|
||||
message.toString(),
|
||||
'from',
|
||||
request.socket.remoteAddress,
|
||||
);
|
||||
});
|
||||
}
|
21
environment.server.js
Normal file
21
environment.server.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
export default function environment() {
|
||||
const {
|
||||
HTTPS_CERT,
|
||||
HTTPS_KEY,
|
||||
MKCERT,
|
||||
NODE_ENV,
|
||||
PORT = 3100,
|
||||
} = process.env;
|
||||
const useMkcert = !!MKCERT;
|
||||
const cacheDirectory = `${import.meta.dirname}/node_modules/.cache`;
|
||||
const httpsCert = HTTPS_CERT || (useMkcert && `${cacheDirectory}/localhost.pem`);
|
||||
const httpsKey = HTTPS_KEY || (useMkcert && `${cacheDirectory}/localhost-key.pem`);
|
||||
return {
|
||||
httpsCert,
|
||||
httpsKey,
|
||||
isInsecure: !httpsCert || !httpsKey,
|
||||
isProduction: NODE_ENV === 'production',
|
||||
port: PORT,
|
||||
useMkcert,
|
||||
};
|
||||
}
|
11301
package-lock.json
generated
11301
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
32
package.json
32
package.json
|
@ -5,11 +5,10 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "remix vite:build",
|
||||
"dev": "NODE_OPTIONS=--use-openssl-ca node ./server.js",
|
||||
"dev": "node ./server.js",
|
||||
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
|
||||
"start": "cross-env NODE_ENV=production npm run dev",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"storybook:build": "storybook build",
|
||||
"start": "cross-env NODE_ENV=production node ./server.js",
|
||||
"storybook": "storybook dev -p 11337 --no-open",
|
||||
"test": "vitest app"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -24,13 +23,14 @@
|
|||
"@pixi/spritesheet": "^7.4.2",
|
||||
"@pixi/tilemap": "^4.1.0",
|
||||
"@react-hook/resize-observer": "^2.0.1",
|
||||
"@remix-run/express": "^2.9.2",
|
||||
"@remix-run/node": "^2.9.2",
|
||||
"@remix-run/react": "^2.9.2",
|
||||
"@remix-run/express": "^2.11.2",
|
||||
"@remix-run/node": "^2.11.2",
|
||||
"@remix-run/react": "^2.11.2",
|
||||
"@remix-run/serve": "^2.11.2",
|
||||
"alea": "^1.0.1",
|
||||
"compression": "^1.7.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"express": "^4.18.2",
|
||||
"express": "^4.19.2",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"isbot": "^4.1.0",
|
||||
"kefir": "^3.8.8",
|
||||
|
@ -42,33 +42,39 @@
|
|||
"remark-mdx": "^3.0.1",
|
||||
"remark-parse": "^11.0.0",
|
||||
"simplex-noise": "^4.0.1",
|
||||
"sylvite": "^1.0.3",
|
||||
"unified": "^11.0.5",
|
||||
"unist-util-visit-parents": "^6.0.1",
|
||||
"ws": "^8.17.0"
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^1.5.0",
|
||||
"@remix-run/dev": "^2.9.2",
|
||||
"@remix-run/dev": "^2.11.2",
|
||||
"@storybook/addon-essentials": "^8.1.6",
|
||||
"@storybook/addon-interactions": "^8.1.6",
|
||||
"@storybook/addon-links": "^8.1.6",
|
||||
"@storybook/addon-onboarding": "^8.1.6",
|
||||
"@storybook/blocks": "^8.1.6",
|
||||
"@storybook/react": "^8.1.6",
|
||||
"@storybook/react-vite": "^8.1.6",
|
||||
"@storybook/test": "^8.1.6",
|
||||
"@testing-library/react": "^16.0.1",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"@vitest/coverage-v8": "^1.6.0",
|
||||
"@vitest/browser": "^2.0.5",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.38.0",
|
||||
"eslint-plugin-import": "^2.28.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"image-size": "^1.1.1",
|
||||
"playwright": "^1.47.0",
|
||||
"postcss": "^8.4.38",
|
||||
"storybook": "^8.1.6",
|
||||
"vite": "^5.1.0",
|
||||
"vitest": "^1.6.0"
|
||||
"vitest": "^2.0.5"
|
||||
},
|
||||
"packageManager": "npm@10.5.2",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
|
|
5
postcss.config.js
Normal file
5
postcss.config.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default {
|
||||
plugins: {
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
164
server.js
164
server.js
|
@ -1,35 +1,56 @@
|
|||
import {createRequestHandler} from '@remix-run/express';
|
||||
import compression from 'compression';
|
||||
import express from 'express';
|
||||
import morgan from 'morgan';
|
||||
import sylvite from 'sylvite';
|
||||
|
||||
// patch pixi server context
|
||||
import('./app/silphius/server/pixi-context.js');
|
||||
import environment from './environment.server.js';
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const isInsecure = process.env.SILPHIUS_INSECURE_HTTP;
|
||||
const cacheDirectory = `${import.meta.dirname}/node_modules/.cache`;
|
||||
const {hooks} = await sylvite({
|
||||
entry: 'node',
|
||||
...await import('./sylvite.config.js'),
|
||||
});
|
||||
|
||||
const {
|
||||
httpsCert,
|
||||
httpsKey,
|
||||
isInsecure,
|
||||
isProduction,
|
||||
port,
|
||||
useMkcert,
|
||||
} = environment();
|
||||
|
||||
const app = express();
|
||||
hooks.call('@/sylvite/cha0s-remix:initializeExpress', app);
|
||||
|
||||
const httpServerOptions = hooks.call('@/sylvite/cha0s-remix:httpServerOptions', {});
|
||||
|
||||
let server;
|
||||
if (isInsecure) {
|
||||
// http://
|
||||
const {createServer} = await import('node:http');
|
||||
server = createServer(app);
|
||||
server = createServer(httpServerOptions, app);
|
||||
}
|
||||
else {
|
||||
const {execSync} = await import('node:child_process');
|
||||
const {mkdirSync, readFileSync, statSync} = await import('node:fs');
|
||||
const cacheDirectory = `${import.meta.dirname}/node_modules/.cache`;
|
||||
mkdirSync(cacheDirectory, {recursive: true});
|
||||
try {
|
||||
statSync(`${cacheDirectory}/localhost-key.pem`);
|
||||
}
|
||||
catch (error) { // eslint-disable-line no-unused-vars
|
||||
execSync(`mkcert -cert-file ${cacheDirectory}/localhost.pem -key-file ${cacheDirectory}/localhost-key.pem localhost`)
|
||||
const {readFile} = await import('node:fs/promises');
|
||||
// generate certificates
|
||||
if (useMkcert) {
|
||||
const {execSync} = await import('node:child_process');
|
||||
const {mkdirSync, statSync} = await import('node:fs');
|
||||
mkdirSync(cacheDirectory, {recursive: true});
|
||||
try {
|
||||
statSync(`${cacheDirectory}/localhost-key.pem`);
|
||||
}
|
||||
catch (error) { // eslint-disable-line no-unused-vars
|
||||
execSync(`mkcert -cert-file ${cacheDirectory}/localhost.pem -key-file ${cacheDirectory}/localhost-key.pem localhost`)
|
||||
}
|
||||
}
|
||||
// https://
|
||||
const [cert, key] = await Promise.all([readFile(httpsCert), readFile(httpsKey)]);
|
||||
const serverOptions = {
|
||||
key: readFileSync(`${cacheDirectory}/localhost-key.pem`),
|
||||
cert: readFileSync(`${cacheDirectory}/localhost.pem`),
|
||||
cert,
|
||||
key,
|
||||
...httpServerOptions,
|
||||
};
|
||||
const {createServer} = await import('node:https');
|
||||
server = createServer(serverOptions, app);
|
||||
|
@ -43,73 +64,66 @@ app.use(async (req, res, next) => {
|
|||
await promise;
|
||||
next();
|
||||
});
|
||||
const port = process.env.PORT || 3000;
|
||||
server.listen(port, () =>
|
||||
console.log(`Express server listening at http${isInsecure ? '' : 's'}://localhost:${port}`)
|
||||
);
|
||||
server.listen(port, () => {
|
||||
console.log(`Express server listening at http${isInsecure ? '' : 's'}://localhost:${port}`);
|
||||
});
|
||||
|
||||
// possibly load dev server and build the request handler up front
|
||||
const viteDevServer = isProduction
|
||||
? undefined
|
||||
: await import('vite').then((vite) =>
|
||||
vite.createServer({
|
||||
server: {middlewareMode: {server}},
|
||||
})
|
||||
);
|
||||
const build = () => (
|
||||
viteDevServer
|
||||
? viteDevServer.ssrLoadModule('virtual:remix/server-build')
|
||||
: import('./build/server/index.js')
|
||||
);
|
||||
let remixHandler;
|
||||
if (viteDevServer) {
|
||||
const {createViteRuntime} = await import('vite');
|
||||
const runtime = await createViteRuntime(viteDevServer);
|
||||
const {handleUpgrade} = await runtime.executeEntrypoint('/app/silphius/server/websocket.js');
|
||||
const requestHandlerOptions = {
|
||||
getLoadContext: (req) => {
|
||||
return hooks.call('@/sylvite/cha0s-remix:getRemixLoadContext', {}, req);
|
||||
},
|
||||
};
|
||||
|
||||
if (isProduction) {
|
||||
// remix handler
|
||||
requestHandlerOptions.build = () => import('./build/server/index.js');
|
||||
// websocket handler
|
||||
const {entry: {module: {handleUpgrade}}} = await requestHandlerOptions.build();
|
||||
server.on('upgrade', handleUpgrade);
|
||||
remixHandler = createRequestHandler({build});
|
||||
// serve assets
|
||||
app.use('/assets', express.static('build/client/assets', {
|
||||
// vite fingerprints its assets so we can cache forever.
|
||||
immutable: true,
|
||||
maxAge: '1y',
|
||||
}));
|
||||
app.use(express.static('build/client', {
|
||||
// everything else (like favicon.ico) is cached for an hour.
|
||||
maxAge: '1h',
|
||||
}));
|
||||
}
|
||||
else {
|
||||
const ssr = await build();
|
||||
server.on('upgrade', ssr.entry.module.handleUpgrade);
|
||||
remixHandler = createRequestHandler({
|
||||
build: async () => {
|
||||
return ssr;
|
||||
},
|
||||
const viteDevServer = await import('vite').then((vite) => (
|
||||
vite.createServer({
|
||||
server: {
|
||||
middlewareMode: true,
|
||||
},
|
||||
})
|
||||
));
|
||||
// remix handler
|
||||
requestHandlerOptions.build = () => viteDevServer.ssrLoadModule('virtual:remix/server-build');
|
||||
const {createViteRuntime} = await import('vite');
|
||||
// websocket handler
|
||||
const runtime = await createViteRuntime(viteDevServer);
|
||||
const {handleUpgrade} = await runtime.executeEntrypoint('/app/websocket.js');
|
||||
server.on('upgrade', handleUpgrade);
|
||||
// serve assets
|
||||
app.use(viteDevServer.middlewares);
|
||||
app.get('/storybook', async (req, res) => {
|
||||
const url = new URL(req.url, `http${isInsecure ? '' : 's'}://${req.headers.host}`);
|
||||
url.pathname = '/';
|
||||
url.port = 11337;
|
||||
res.redirect(url.href);
|
||||
});
|
||||
}
|
||||
|
||||
// configure middleware
|
||||
app.use(compression());
|
||||
|
||||
// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
|
||||
app.disable('x-powered-by');
|
||||
|
||||
// handle asset requests
|
||||
if (viteDevServer) {
|
||||
app.use(viteDevServer.middlewares);
|
||||
}
|
||||
else {
|
||||
// Vite fingerprints its assets so we can cache forever.
|
||||
app.use(
|
||||
'/assets',
|
||||
express.static('build/client/assets', { immutable: true, maxAge: '1y' })
|
||||
);
|
||||
// Everything else (like favicon.ico) is cached for an hour. You may want to be
|
||||
// more aggressive with this caching.
|
||||
app.use(express.static('build/client', { maxAge: '1h' }));
|
||||
}
|
||||
|
||||
// silphius resources
|
||||
app.use(
|
||||
'/resources',
|
||||
express.static('resources', { maxAge: '1h' }),
|
||||
);
|
||||
hooks.call('@/sylvite/cha0s-remix:additionalAssets', app);
|
||||
|
||||
app.use(morgan('tiny'));
|
||||
|
||||
// handle SSR requests
|
||||
app.all('*', remixHandler);
|
||||
hooks.call('@/sylvite/cha0s-remix:beforeExpressCatchAll', app);
|
||||
|
||||
// finally let requests resolve
|
||||
// handle SSR requests
|
||||
app.all('*', createRequestHandler(requestHandlerOptions));
|
||||
|
||||
// finalize; let requests resolve
|
||||
resolve();
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
import {useEffect, useRef, useState} from 'react';
|
||||
|
||||
import Dom from '@/react/components/dom.jsx';
|
||||
import {RESOLUTION} from '@/lib/constants.js';
|
||||
|
||||
function Decorator({children, style}) {
|
||||
const ref = useRef();
|
||||
const [scale, setScale] = useState(0);
|
||||
useEffect(() => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
function onResize() {
|
||||
const {parentNode} = ref.current;
|
||||
const {width} = parentNode.getBoundingClientRect();
|
||||
setScale(width / RESOLUTION.x);
|
||||
}
|
||||
window.addEventListener('resize', onResize);
|
||||
onResize();
|
||||
return () => {
|
||||
window.removeEventListener('resize', onResize);
|
||||
}
|
||||
}, [ref.current]);
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
style={{
|
||||
backgroundColor: '#1099bb',
|
||||
flexDirection: 'column',
|
||||
opacity: 0 === scale ? 0 : 1,
|
||||
position: 'relative',
|
||||
height: `calc(${RESOLUTION.y}px * ${scale})`,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Dom>
|
||||
<div style={style}>
|
||||
{children}
|
||||
</div>
|
||||
</Dom>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function(options = {}) {
|
||||
return function decorate(Decorating) {
|
||||
return (
|
||||
<Decorator style={options.style}>
|
||||
<Decorating />
|
||||
</Decorator>
|
||||
);
|
||||
};
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
import {useArgs} from '@storybook/preview-api';
|
||||
import {fn} from '@storybook/test';
|
||||
|
||||
import Hotbar from '@/react/components/hotbar.jsx';
|
||||
|
||||
import DomDecorator from './dom-decorator.jsx';
|
||||
|
||||
const slots = Array(10).fill({});
|
||||
slots[2] = {qty: 24, icon: '/resources/potion/icon.png'};
|
||||
|
||||
export default {
|
||||
title: 'Dom/Inventory/Hotbar',
|
||||
component: Hotbar,
|
||||
decorators: [
|
||||
(Hotbar, ctx) => {
|
||||
const [, updateArgs] = useArgs();
|
||||
const {onSlotMouseDown} = ctx.args;
|
||||
ctx.args.onSlotMouseDown = (i) => {
|
||||
updateArgs({active: i});
|
||||
if (onSlotMouseDown) {
|
||||
onSlotMouseDown(i);
|
||||
}
|
||||
};
|
||||
return Hotbar();
|
||||
},
|
||||
DomDecorator({
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
}),
|
||||
],
|
||||
tags: ['autodocs'],
|
||||
args: {
|
||||
active: 0,
|
||||
onSlotMouseDown: fn(),
|
||||
slots,
|
||||
},
|
||||
argTypes: {
|
||||
active: {
|
||||
control: {
|
||||
type: 'number',
|
||||
min: 0,
|
||||
max: 9,
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Default = {};
|
|
@ -1,66 +0,0 @@
|
|||
import Slot from '@/react/components/slot.jsx';
|
||||
|
||||
import DomDecorator from './dom-decorator.jsx';
|
||||
|
||||
export default {
|
||||
title: 'Dom/Inventory/Slot',
|
||||
component: Slot,
|
||||
decorators: [
|
||||
DomDecorator({
|
||||
style: {
|
||||
border: '2px solid #999',
|
||||
lineHeight: 0,
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
},
|
||||
}),
|
||||
],
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
image: {
|
||||
control: 'text',
|
||||
},
|
||||
qty: {
|
||||
control: {
|
||||
type: 'number',
|
||||
min: 1,
|
||||
max: 9999,
|
||||
}
|
||||
},
|
||||
},
|
||||
args: {
|
||||
icon: '/resources/potion/icon.png',
|
||||
},
|
||||
};
|
||||
|
||||
export const Single = {
|
||||
args: {
|
||||
qty: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export const OneDigit = {
|
||||
args: {
|
||||
qty: 9,
|
||||
},
|
||||
};
|
||||
|
||||
export const TwoDigit = {
|
||||
args: {
|
||||
qty: 99,
|
||||
},
|
||||
};
|
||||
|
||||
export const ThreeDigit = {
|
||||
args: {
|
||||
qty: 999,
|
||||
},
|
||||
};
|
||||
|
||||
export const FourDigit = {
|
||||
args: {
|
||||
qty: 9999,
|
||||
},
|
||||
};
|
16
sylvite.config.js
Normal file
16
sylvite.config.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
export const manifest = {
|
||||
'@/sylvite/cha0s-remix': {},
|
||||
'@/sylvite/cha0s-remix/cors': {},
|
||||
'@/sylvite/cha0s-remix/remix': {},
|
||||
'@/silphius': {},
|
||||
};
|
||||
|
||||
export const meta = {
|
||||
dirname: import.meta.dirname,
|
||||
resolve: (url) => {
|
||||
if (url.startsWith('@')) {
|
||||
url = ['./app', url.slice(1)].join('');
|
||||
}
|
||||
return import.meta.resolve(url);
|
||||
},
|
||||
}
|
9
tailwind.config.js
Normal file
9
tailwind.config.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
export default {
|
||||
content: [
|
||||
'./app/**/{**,.client,.server}/**/*.{js,jsx}',
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
|
@ -3,19 +3,37 @@ import {fileURLToPath} from 'node:url';
|
|||
|
||||
import {vitePlugin as remix} from '@remix-run/dev';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import sylvite from 'sylvite';
|
||||
import {defineConfig} from 'vite';
|
||||
|
||||
const cacheDirectory = `${import.meta.dirname}/node_modules/.cache`;
|
||||
import environment from './environment.server.js';
|
||||
|
||||
const {
|
||||
httpsCert,
|
||||
httpsKey,
|
||||
isInsecure,
|
||||
} = environment();
|
||||
|
||||
const {hooks} = await sylvite({
|
||||
...await import('./sylvite.config.js'),
|
||||
});
|
||||
|
||||
const plugins = [];
|
||||
|
||||
if (!process.env.STORYBOOK) {
|
||||
if (!process.env.REMIX_DISABLED) {
|
||||
plugins.push(
|
||||
remix({
|
||||
future: {
|
||||
v3_fetcherPersist: true,
|
||||
v3_relativeSplatPath: true,
|
||||
v3_throwAbortReason: true,
|
||||
unstable_singleFetch: true,
|
||||
unstable_lazyRouteDiscovery: true,
|
||||
},
|
||||
routes(defineRoutes) {
|
||||
return defineRoutes((route) => {
|
||||
hooks.call('./sylvite/remix:defineRemixRoutes', route);
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
@ -24,7 +42,7 @@ else {
|
|||
plugins.push(react());
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
export default defineConfig(hooks.call('sylvite:viteConfig', {
|
||||
esbuild: {
|
||||
supported: {
|
||||
'top-level-await': true,
|
||||
|
@ -35,22 +53,20 @@ export default defineConfig({
|
|||
alias: [
|
||||
{
|
||||
find: '@',
|
||||
replacement: fileURLToPath(new URL('./app', import.meta.url))
|
||||
},
|
||||
{
|
||||
find: '%',
|
||||
replacement: fileURLToPath(new URL('./resources', import.meta.url))
|
||||
replacement: fileURLToPath(new URL('./app', import.meta.url)),
|
||||
},
|
||||
],
|
||||
},
|
||||
server: {
|
||||
host: true,
|
||||
https: {
|
||||
key: readFileSync(`${cacheDirectory}/localhost-key.pem`),
|
||||
cert: readFileSync(`${cacheDirectory}/localhost.pem`),
|
||||
},
|
||||
...(!isInsecure && {
|
||||
https: {
|
||||
key: readFileSync(httpsKey),
|
||||
cert: readFileSync(httpsCert),
|
||||
},
|
||||
}),
|
||||
},
|
||||
worker: {
|
||||
format: 'es',
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
|
31
vitest.workspace.js
Normal file
31
vitest.workspace.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import {defineWorkspace} from 'vitest/config';
|
||||
|
||||
process.env.REMIX_DISABLED = 1;
|
||||
|
||||
export default defineWorkspace([
|
||||
{
|
||||
extends: './vite.config.js',
|
||||
test: {
|
||||
include: [
|
||||
'app/**/*.test.js',
|
||||
],
|
||||
name: 'unit',
|
||||
environment: 'node',
|
||||
},
|
||||
},
|
||||
{
|
||||
extends: './vite.config.js',
|
||||
test: {
|
||||
include: [
|
||||
'app/**/*.test.jsx',
|
||||
],
|
||||
name: 'browser',
|
||||
browser: {
|
||||
enabled: true,
|
||||
headless: true,
|
||||
name: 'chromium',
|
||||
provider: 'playwright',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
Loading…
Reference in New Issue
Block a user