feat(world-streaming): 添加世界流式加载系统 (#288)

实现基于区块的世界流式加载系统,支持开放世界游戏:

运行时包 (@esengine/world-streaming):
- ChunkComponent: 区块实体组件,包含坐标、边界、状态
- StreamingAnchorComponent: 流式锚点组件(玩家/摄像机)
- ChunkLoaderComponent: 流式加载配置组件
- ChunkStreamingSystem: 区块加载/卸载调度系统
- ChunkCullingSystem: 区块可见性剔除系统
- ChunkManager: 区块生命周期管理服务
- SpatialHashGrid: 空间哈希网格
- ChunkSerializer: 区块序列化

编辑器包 (@esengine/world-streaming-editor):
- ChunkVisualizer: 区块可视化覆盖层
- ChunkLoaderInspectorProvider: 区块加载器检视器
- StreamingAnchorInspectorProvider: 流式锚点检视器
- WorldStreamingPlugin: 完整插件导出
This commit is contained in:
YHH
2025-12-06 13:56:01 +08:00
committed by GitHub
parent 3cbfa1e4cb
commit 0c03b13d74
31 changed files with 2802 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
{
"name": "@esengine/world-streaming-editor",
"version": "1.0.0",
"description": "Editor support for @esengine/world-streaming - chunk visualization and inspector",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"build:watch": "tsup --watch",
"type-check": "tsc --noEmit",
"clean": "rimraf dist"
},
"dependencies": {
"@esengine/world-streaming": "workspace:*"
},
"peerDependencies": {
"@esengine/editor-core": "workspace:*"
},
"devDependencies": {
"@esengine/ecs-framework": "workspace:*",
"@esengine/engine-core": "workspace:*",
"@esengine/editor-core": "workspace:*",
"@esengine/build-config": "workspace:*",
"lucide-react": "^0.545.0",
"react": "^18.3.1",
"zustand": "^5.0.8",
"@types/react": "^18.3.12",
"rimraf": "^5.0.5",
"tsup": "^8.0.0",
"typescript": "^5.3.3"
},
"keywords": [
"ecs",
"world-streaming",
"chunk",
"editor"
],
"author": "ESEngine",
"license": "MIT"
}

View File

@@ -0,0 +1,227 @@
/**
* 世界流式加载编辑器模块
* World Streaming Editor Module
*
* Registers chunk visualizer, inspector providers and tools for world streaming.
*/
import type { ServiceContainer, Entity } from '@esengine/ecs-framework';
import { Core } from '@esengine/ecs-framework';
import type {
IEditorModuleLoader,
PanelDescriptor,
EntityCreationTemplate,
ComponentInspectorProviderDef,
IPlugin,
ModuleManifest
} from '@esengine/editor-core';
import {
PanelPosition,
InspectorRegistry,
EntityStoreService,
MessageHub,
ComponentRegistry
} from '@esengine/editor-core';
import { TransformComponent } from '@esengine/engine-core';
import {
ChunkComponent,
StreamingAnchorComponent,
ChunkLoaderComponent,
WorldStreamingModule
} from '@esengine/world-streaming';
import { ChunkLoaderInspectorProvider } from './providers/ChunkLoaderInspectorProvider';
import { StreamingAnchorInspectorProvider } from './providers/StreamingAnchorInspectorProvider';
import './styles/ChunkVisualizer.css';
/**
* 世界流式加载编辑器模块
* World Streaming Editor Module
*/
export class WorldStreamingEditorModule implements IEditorModuleLoader {
async install(services: ServiceContainer): Promise<void> {
const inspectorRegistry = services.resolve(InspectorRegistry);
if (inspectorRegistry) {
inspectorRegistry.register(new ChunkLoaderInspectorProvider());
inspectorRegistry.register(new StreamingAnchorInspectorProvider());
}
const componentRegistry = services.resolve(ComponentRegistry);
if (componentRegistry) {
componentRegistry.register({
name: 'ChunkLoader',
type: ChunkLoaderComponent,
category: 'components.category.streaming',
description: 'Chunk-based world streaming controller',
icon: 'Grid3X3'
});
componentRegistry.register({
name: 'StreamingAnchor',
type: StreamingAnchorComponent,
category: 'components.category.streaming',
description: 'Streaming anchor point (player/camera)',
icon: 'Anchor'
});
componentRegistry.register({
name: 'Chunk',
type: ChunkComponent,
category: 'components.category.streaming',
description: 'Chunk entity marker',
icon: 'Square'
});
}
}
async uninstall(): Promise<void> {
// 清理 | Clean up
}
getPanels(): PanelDescriptor[] {
return [];
}
getInspectorProviders(): ComponentInspectorProviderDef[] {
return [
{
componentType: 'ChunkLoader',
priority: 100,
render: (component, entity, onChange) => {
const provider = new ChunkLoaderInspectorProvider();
return provider.render(
{ entityId: String(entity.id), component },
{ target: component, onChange }
);
}
},
{
componentType: 'StreamingAnchor',
priority: 100,
render: (component, entity, onChange) => {
const provider = new StreamingAnchorInspectorProvider();
return provider.render(
{ entityId: String(entity.id), component },
{ target: component, onChange }
);
}
}
];
}
getEntityCreationTemplates(): EntityCreationTemplate[] {
return [
{
id: 'create-streaming-anchor',
label: '创建流式锚点',
icon: 'Anchor',
category: 'streaming',
order: 100,
create: (): number => {
const scene = Core.scene;
if (!scene) {
throw new Error('Scene not available');
}
const entityStore = Core.services.resolve(EntityStoreService);
const messageHub = Core.services.resolve(MessageHub);
if (!entityStore || !messageHub) {
throw new Error('EntityStoreService or MessageHub not available');
}
const anchorCount = entityStore.getAllEntities()
.filter((e: Entity) => e.name.startsWith('StreamingAnchor ')).length;
const entityName = `StreamingAnchor ${anchorCount + 1}`;
const entity = scene.createEntity(entityName);
entity.addComponent(new TransformComponent());
entity.addComponent(new StreamingAnchorComponent());
entityStore.addEntity(entity);
messageHub.publish('entity:added', { entity });
messageHub.publish('scene:modified', {});
entityStore.selectEntity(entity);
return entity.id;
}
},
{
id: 'create-chunk-loader',
label: '创建区块加载器',
icon: 'Grid3X3',
category: 'streaming',
order: 101,
create: (): number => {
const scene = Core.scene;
if (!scene) {
throw new Error('Scene not available');
}
const entityStore = Core.services.resolve(EntityStoreService);
const messageHub = Core.services.resolve(MessageHub);
if (!entityStore || !messageHub) {
throw new Error('EntityStoreService or MessageHub not available');
}
const entity = scene.createEntity('ChunkLoader');
entity.addComponent(new ChunkLoaderComponent());
entityStore.addEntity(entity);
messageHub.publish('entity:added', { entity });
messageHub.publish('scene:modified', {});
entityStore.selectEntity(entity);
return entity.id;
}
}
];
}
getFileActionHandlers() {
return [];
}
getFileCreationTemplates() {
return [];
}
}
export const worldStreamingEditorModule = new WorldStreamingEditorModule();
/**
* 世界流式加载插件清单
* World Streaming Plugin Manifest
*/
const manifest: ModuleManifest = {
id: '@esengine/world-streaming',
name: '@esengine/world-streaming',
displayName: 'World Streaming',
version: '1.0.0',
description: 'Chunk-based world streaming for open world games',
category: 'Other',
isCore: false,
defaultEnabled: true,
isEngineModule: true,
canContainContent: false,
dependencies: ['engine-core'],
exports: {
components: ['ChunkComponent', 'StreamingAnchorComponent', 'ChunkLoaderComponent'],
systems: ['ChunkStreamingSystem', 'ChunkCullingSystem'],
other: ['ChunkManager']
}
};
/**
* 完整的世界流式加载插件(运行时 + 编辑器)
* Complete World Streaming Plugin (runtime + editor)
*/
export const WorldStreamingPlugin: IPlugin = {
manifest,
runtimeModule: new WorldStreamingModule(),
editorModule: worldStreamingEditorModule
};
export default worldStreamingEditorModule;

View File

@@ -0,0 +1,279 @@
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import type { ChunkManager, IChunkCoord, IChunkInfo } from '@esengine/world-streaming';
import { EChunkState } from '@esengine/world-streaming';
import '../styles/ChunkVisualizer.css';
export interface ChunkVisualizerProps {
chunkManager: ChunkManager | null;
cameraX: number;
cameraY: number;
cameraZoom: number;
viewWidth: number;
viewHeight: number;
bShowCoords?: boolean;
bShowStats?: boolean;
bShowRadii?: boolean;
anchorPositions?: Array<{ x: number; y: number }>;
loadRadius?: number;
unloadRadius?: number;
}
interface ChunkDisplayInfo {
coord: IChunkCoord;
state: EChunkState;
screenX: number;
screenY: number;
screenWidth: number;
screenHeight: number;
isAnchorChunk: boolean;
}
/**
* 区块可视化组件
*
* Chunk visualization overlay for editor viewport.
*/
export const ChunkVisualizer: React.FC<ChunkVisualizerProps> = ({
chunkManager,
cameraX,
cameraY,
cameraZoom,
viewWidth,
viewHeight,
bShowCoords = true,
bShowStats = true,
bShowRadii = true,
anchorPositions = [],
loadRadius = 2,
unloadRadius = 4
}) => {
const [chunks, setChunks] = useState<ChunkDisplayInfo[]>([]);
const [stats, setStats] = useState({
loaded: 0,
loading: 0,
pendingLoad: 0,
pendingUnload: 0
});
const chunkSize = chunkManager?.chunkSize ?? 512;
const worldToScreen = useCallback((worldX: number, worldY: number) => {
const screenX = (worldX - cameraX) * cameraZoom + viewWidth / 2;
const screenY = (worldY - cameraY) * cameraZoom + viewHeight / 2;
return { x: screenX, y: screenY };
}, [cameraX, cameraY, cameraZoom, viewWidth, viewHeight]);
const anchorChunkCoords = useMemo(() => {
return anchorPositions.map(pos => ({
x: Math.floor(pos.x / chunkSize),
y: Math.floor(pos.y / chunkSize)
}));
}, [anchorPositions, chunkSize]);
useEffect(() => {
if (!chunkManager) {
setChunks([]);
setStats({ loaded: 0, loading: 0, pendingLoad: 0, pendingUnload: 0 });
return;
}
const updateChunks = () => {
const visibleChunks: ChunkDisplayInfo[] = [];
let loadedCount = 0;
let loadingCount = 0;
const halfViewW = viewWidth / 2 / cameraZoom;
const halfViewH = viewHeight / 2 / cameraZoom;
const margin = chunkSize * 2;
const minChunkX = Math.floor((cameraX - halfViewW - margin) / chunkSize);
const maxChunkX = Math.ceil((cameraX + halfViewW + margin) / chunkSize);
const minChunkY = Math.floor((cameraY - halfViewH - margin) / chunkSize);
const maxChunkY = Math.ceil((cameraY + halfViewH + margin) / chunkSize);
for (let cx = minChunkX; cx <= maxChunkX; cx++) {
for (let cy = minChunkY; cy <= maxChunkY; cy++) {
const coord = { x: cx, y: cy };
const chunkInfo = chunkManager.getChunk(coord);
const worldMinX = cx * chunkSize;
const worldMinY = cy * chunkSize;
const screenPos = worldToScreen(worldMinX, worldMinY);
const screenSize = chunkSize * cameraZoom;
const isAnchorChunk = anchorChunkCoords.some(
ac => ac.x === cx && ac.y === cy
);
const state = chunkInfo?.state ?? EChunkState.Unloaded;
if (state === EChunkState.Loaded) loadedCount++;
if (state === EChunkState.Loading) loadingCount++;
visibleChunks.push({
coord,
state,
screenX: screenPos.x,
screenY: screenPos.y,
screenWidth: screenSize,
screenHeight: screenSize,
isAnchorChunk
});
}
}
setChunks(visibleChunks);
setStats({
loaded: loadedCount,
loading: loadingCount,
pendingLoad: chunkManager.pendingLoadCount,
pendingUnload: chunkManager.pendingUnloadCount
});
};
updateChunks();
const interval = setInterval(updateChunks, 100);
return () => clearInterval(interval);
}, [chunkManager, cameraX, cameraY, cameraZoom, viewWidth, viewHeight, chunkSize, worldToScreen, anchorChunkCoords]);
const getChunkClassName = (chunk: ChunkDisplayInfo): string => {
const classes = ['chunk-grid-cell'];
switch (chunk.state) {
case EChunkState.Loaded:
classes.push('loaded');
break;
case EChunkState.Loading:
classes.push('loading');
break;
case EChunkState.Unloading:
classes.push('unloading');
break;
case EChunkState.Failed:
classes.push('failed');
break;
}
if (chunk.isAnchorChunk) {
classes.push('anchor-chunk');
}
return classes.join(' ');
};
const radiusIndicators = useMemo(() => {
if (!bShowRadii || anchorPositions.length === 0) return null;
return anchorPositions.map((pos, idx) => {
const loadRadiusWorld = (loadRadius + 0.5) * chunkSize;
const unloadRadiusWorld = (unloadRadius + 0.5) * chunkSize;
const anchorChunkX = Math.floor(pos.x / chunkSize);
const anchorChunkY = Math.floor(pos.y / chunkSize);
const anchorChunkCenterX = (anchorChunkX + 0.5) * chunkSize;
const anchorChunkCenterY = (anchorChunkY + 0.5) * chunkSize;
const loadScreenPos = worldToScreen(
anchorChunkCenterX - loadRadiusWorld,
anchorChunkCenterY - loadRadiusWorld
);
const loadScreenSize = loadRadiusWorld * 2 * cameraZoom;
const unloadScreenPos = worldToScreen(
anchorChunkCenterX - unloadRadiusWorld,
anchorChunkCenterY - unloadRadiusWorld
);
const unloadScreenSize = unloadRadiusWorld * 2 * cameraZoom;
return (
<React.Fragment key={idx}>
<div
className="unload-radius-indicator"
style={{
left: unloadScreenPos.x,
top: unloadScreenPos.y,
width: unloadScreenSize,
height: unloadScreenSize
}}
/>
<div
className="load-radius-indicator"
style={{
left: loadScreenPos.x,
top: loadScreenPos.y,
width: loadScreenSize,
height: loadScreenSize
}}
/>
</React.Fragment>
);
});
}, [bShowRadii, anchorPositions, loadRadius, unloadRadius, chunkSize, worldToScreen, cameraZoom]);
const anchorMarkers = useMemo(() => {
return anchorPositions.map((pos, idx) => {
const screenPos = worldToScreen(pos.x, pos.y);
return (
<div
key={`anchor-${idx}`}
className="anchor-marker"
style={{
left: screenPos.x,
top: screenPos.y
}}
/>
);
});
}, [anchorPositions, worldToScreen]);
return (
<div className="chunk-visualizer">
<div className="chunk-visualizer-overlay">
{chunks.map((chunk) => (
<div
key={`${chunk.coord.x},${chunk.coord.y}`}
className={getChunkClassName(chunk)}
style={{
left: chunk.screenX,
top: chunk.screenY,
width: chunk.screenWidth,
height: chunk.screenHeight
}}
>
{bShowCoords && chunk.screenWidth > 40 && (
<span className="chunk-coord-label">
{chunk.coord.x},{chunk.coord.y}
</span>
)}
</div>
))}
{radiusIndicators}
{anchorMarkers}
</div>
{bShowStats && (
<div className="chunk-stats-panel">
<h4>Chunks</h4>
<div className="chunk-stats-row">
<span className="label">Loaded:</span>
<span className="value loaded">{stats.loaded}</span>
</div>
<div className="chunk-stats-row">
<span className="label">Loading:</span>
<span className="value loading">{stats.loading}</span>
</div>
<div className="chunk-stats-row">
<span className="label">Pending:</span>
<span className="value pending">{stats.pendingLoad}</span>
</div>
<div className="chunk-stats-row">
<span className="label">Unload Queue:</span>
<span className="value">{stats.pendingUnload}</span>
</div>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,20 @@
/**
* 世界流式加载编辑器模块入口
* World Streaming Editor Module Entry
*/
// Module
export {
WorldStreamingEditorModule,
worldStreamingEditorModule,
WorldStreamingPlugin,
worldStreamingEditorModule as default
} from './WorldStreamingEditorModule';
// Providers
export { ChunkLoaderInspectorProvider } from './providers/ChunkLoaderInspectorProvider';
export { StreamingAnchorInspectorProvider } from './providers/StreamingAnchorInspectorProvider';
// Components
export { ChunkVisualizer } from './components/ChunkVisualizer';
export type { ChunkVisualizerProps } from './components/ChunkVisualizer';

View File

@@ -0,0 +1,83 @@
/**
* ChunkLoader Inspector Provider
*
* Custom inspector for ChunkLoaderComponent with streaming configuration.
*/
import React, { useState, useCallback } from 'react';
import { Settings, Play, Pause, RefreshCw } from 'lucide-react';
import type { IInspectorProvider, InspectorContext } from '@esengine/editor-core';
import type { ChunkLoaderComponent } from '@esengine/world-streaming';
interface ChunkLoaderInspectorData {
entityId: string;
component: ChunkLoaderComponent;
}
export class ChunkLoaderInspectorProvider implements IInspectorProvider<ChunkLoaderInspectorData> {
readonly id = 'chunk-loader-inspector';
readonly name = 'Chunk Loader Inspector';
readonly priority = 100;
canHandle(target: unknown): target is ChunkLoaderInspectorData {
if (typeof target !== 'object' || target === null) return false;
const obj = target as Record<string, unknown>;
return 'entityId' in obj && 'component' in obj &&
obj.component !== null &&
typeof obj.component === 'object' &&
'chunkSize' in (obj.component as Record<string, unknown>) &&
'loadRadius' in (obj.component as Record<string, unknown>);
}
render(data: ChunkLoaderInspectorData, _context: InspectorContext): React.ReactElement {
const { component } = data;
return (
<div className="entity-inspector">
<div className="inspector-section">
<div className="section-title">
<Settings size={14} style={{ marginRight: '6px' }} />
Streaming Configuration
</div>
<div className="property-row">
<label>Chunk Size</label>
<span>{component.chunkSize} units</span>
</div>
<div className="property-row">
<label>Load Radius</label>
<span>{component.loadRadius} chunks</span>
</div>
<div className="property-row">
<label>Unload Radius</label>
<span>{component.unloadRadius} chunks</span>
</div>
<div className="property-row">
<label>Max Loads/Frame</label>
<span>{component.maxLoadsPerFrame}</span>
</div>
<div className="property-row">
<label>Unload Delay</label>
<span>{component.unloadDelay}ms</span>
</div>
<div className="property-row">
<label>Prefetch</label>
<span>{component.bEnablePrefetch ? 'Enabled' : 'Disabled'}</span>
</div>
{component.bEnablePrefetch && (
<div className="property-row">
<label>Prefetch Radius</label>
<span>{component.prefetchRadius} chunks</span>
</div>
)}
</div>
</div>
);
}
}

View File

@@ -0,0 +1,81 @@
/**
* StreamingAnchor Inspector Provider
*
* Custom inspector for StreamingAnchorComponent.
*/
import React from 'react';
import { Anchor, Navigation } from 'lucide-react';
import type { IInspectorProvider, InspectorContext } from '@esengine/editor-core';
import type { StreamingAnchorComponent } from '@esengine/world-streaming';
interface StreamingAnchorInspectorData {
entityId: string;
component: StreamingAnchorComponent;
}
export class StreamingAnchorInspectorProvider implements IInspectorProvider<StreamingAnchorInspectorData> {
readonly id = 'streaming-anchor-inspector';
readonly name = 'Streaming Anchor Inspector';
readonly priority = 100;
canHandle(target: unknown): target is StreamingAnchorInspectorData {
if (typeof target !== 'object' || target === null) return false;
const obj = target as Record<string, unknown>;
return 'entityId' in obj && 'component' in obj &&
obj.component !== null &&
typeof obj.component === 'object' &&
'weight' in (obj.component as Record<string, unknown>) &&
'bEnablePrefetch' in (obj.component as Record<string, unknown>) &&
'velocityX' in (obj.component as Record<string, unknown>);
}
render(data: StreamingAnchorInspectorData, _context: InspectorContext): React.ReactElement {
const { component } = data;
const velocity = Math.sqrt(
component.velocityX * component.velocityX +
component.velocityY * component.velocityY
);
return (
<div className="entity-inspector">
<div className="inspector-section">
<div className="section-title">
<Anchor size={14} style={{ marginRight: '6px' }} />
Streaming Anchor
</div>
<div className="property-row">
<label>Weight</label>
<span>{component.weight.toFixed(2)}</span>
</div>
<div className="property-row">
<label>Prefetch</label>
<span>{component.bEnablePrefetch ? 'Enabled' : 'Disabled'}</span>
</div>
</div>
<div className="inspector-section">
<div className="section-title">
<Navigation size={14} style={{ marginRight: '6px' }} />
Movement (Runtime)
</div>
<div className="property-row">
<label>Velocity</label>
<span>{velocity.toFixed(1)} u/s</span>
</div>
<div className="property-row">
<label>Direction</label>
<span>
({component.velocityX.toFixed(1)}, {component.velocityY.toFixed(1)})
</span>
</div>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,139 @@
.chunk-visualizer {
position: absolute;
pointer-events: none;
z-index: 100;
}
.chunk-visualizer-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
.chunk-grid-cell {
position: absolute;
box-sizing: border-box;
border: 1px solid rgba(100, 149, 237, 0.3);
transition: background-color 0.2s ease;
}
.chunk-grid-cell.loaded {
background-color: rgba(100, 149, 237, 0.1);
border-color: rgba(100, 149, 237, 0.5);
}
.chunk-grid-cell.loading {
background-color: rgba(255, 193, 7, 0.2);
border-color: rgba(255, 193, 7, 0.6);
animation: chunk-loading-pulse 1s ease-in-out infinite;
}
.chunk-grid-cell.unloading {
background-color: rgba(255, 87, 34, 0.2);
border-color: rgba(255, 87, 34, 0.5);
}
.chunk-grid-cell.failed {
background-color: rgba(244, 67, 54, 0.2);
border-color: rgba(244, 67, 54, 0.6);
}
.chunk-grid-cell.anchor-chunk {
border-color: rgba(76, 175, 80, 0.8);
border-width: 2px;
}
@keyframes chunk-loading-pulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
.chunk-coord-label {
position: absolute;
top: 2px;
left: 2px;
font-size: 10px;
color: rgba(255, 255, 255, 0.6);
font-family: monospace;
pointer-events: none;
}
.anchor-marker {
position: absolute;
width: 12px;
height: 12px;
border-radius: 50%;
background-color: rgba(76, 175, 80, 0.8);
border: 2px solid #4caf50;
transform: translate(-50%, -50%);
z-index: 101;
}
.anchor-marker::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 4px;
height: 4px;
background-color: white;
border-radius: 50%;
transform: translate(-50%, -50%);
}
.load-radius-indicator {
position: absolute;
border: 2px dashed rgba(76, 175, 80, 0.5);
border-radius: 4px;
pointer-events: none;
}
.unload-radius-indicator {
position: absolute;
border: 1px dashed rgba(255, 87, 34, 0.3);
border-radius: 4px;
pointer-events: none;
}
.chunk-stats-panel {
position: absolute;
top: 8px;
right: 8px;
background: rgba(30, 30, 30, 0.9);
border: 1px solid #3e3e42;
border-radius: 4px;
padding: 8px 12px;
font-size: 11px;
color: #cccccc;
pointer-events: auto;
min-width: 140px;
}
.chunk-stats-panel h4 {
margin: 0 0 6px 0;
font-size: 12px;
color: #ffffff;
border-bottom: 1px solid #3e3e42;
padding-bottom: 4px;
}
.chunk-stats-row {
display: flex;
justify-content: space-between;
margin: 2px 0;
}
.chunk-stats-row .label {
color: #888888;
}
.chunk-stats-row .value {
font-family: monospace;
}
.chunk-stats-row .value.loaded { color: #64b5f6; }
.chunk-stats-row .value.loading { color: #ffc107; }
.chunk-stats-row .value.pending { color: #ff9800; }

View File

@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"moduleResolution": "bundler",
"lib": ["ES2020", "DOM"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"jsx": "react-jsx",
"resolveJsonModule": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"jsx": "react-jsx"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'tsup';
import { editorOnlyPreset } from '../build-config/src/presets/plugin-tsup';
export default defineConfig({
...editorOnlyPreset(),
tsconfig: 'tsconfig.build.json'
});