fix: SpireTab store props, mana regen display, skill cost deduction, grimoire cost format, unequip store, add test suite
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m34s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m34s
This commit is contained in:
@@ -471,6 +471,19 @@ Runs after merging branches:
|
||||
5. **Not updating modular stores**: Check all stores in `stores/` directory for related state
|
||||
6. **Bypassing crafting-actions**: Use the modular actions in `crafting-actions/` for new crafting features
|
||||
|
||||
## Testing
|
||||
|
||||
Run `npm run test` before every commit. Tests must pass.
|
||||
|
||||
When fixing a bug, write a test that would have caught it first.
|
||||
Test files live in src/lib/game/stores/__tests__/.
|
||||
|
||||
Critical paths that must always have tests:
|
||||
- Any store action that modifies rawMana
|
||||
- Any store action that modifies equippedInstances
|
||||
- computeRegen, computeMaxMana, calcDamage
|
||||
- canAffordSpellCost, spendRawMana, deductSpellCost
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
- Run `npm run lint` after changes
|
||||
|
||||
@@ -383,6 +383,11 @@ Mana-Loop/
|
||||
│ │ │ │ │ ├── spell-definitions.test.ts
|
||||
│ │ │ │ │ └── study-speed.test.ts
|
||||
│ │ │ │ ├── ui-store-tests/
|
||||
│ │ │ │ ├── equipment.test.ts
|
||||
│ │ │ │ ├── mana.test.ts
|
||||
│ │ │ │ ├── regen.test.ts
|
||||
│ │ │ │ ├── skill.test.ts
|
||||
│ │ │ │ ├── spell-cost.test.ts
|
||||
│ │ │ │ ├── store-methods.test.ts
|
||||
│ │ │ │ └── stores.test.ts
|
||||
│ │ │ ├── attunementStore.ts
|
||||
|
||||
@@ -7,9 +7,10 @@ import { ManaDisplay } from '@/components/game';
|
||||
import { ActionButtons } from '@/components/game';
|
||||
import { CalendarDisplay } from '@/components/game';
|
||||
import { DebugName } from '@/lib/game/debug-context';
|
||||
import { useGameStore, useManaStore, useSkillStore, useCombatStore, useCraftingStore } from '@/lib/game/stores';
|
||||
import { useGameStore, useManaStore, useSkillStore, useCombatStore, useCraftingStore, usePrestigeStore } from '@/lib/game/stores';
|
||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
||||
import { computeMaxMana, computeRegen, computeClickMana, getMeditationBonus, getIncursionStrength } from '@/lib/game/stores';
|
||||
import { getMeditationBonus, getIncursionStrength } from '@/lib/game/stores';
|
||||
import { computeTotalMaxMana, computeTotalRegen, computeTotalClickMana } from '@/lib/game/effects';
|
||||
|
||||
export function LeftPanel() {
|
||||
const [isGathering, setIsGathering] = useState(false);
|
||||
@@ -23,6 +24,8 @@ export function LeftPanel() {
|
||||
const skillTiers = useSkillStore((s) => s.skillTiers);
|
||||
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
|
||||
|
||||
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
|
||||
|
||||
const equippedInstances = useCraftingStore((s) => s.equippedInstances);
|
||||
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
|
||||
|
||||
@@ -77,19 +80,18 @@ export function LeftPanel() {
|
||||
equipmentInstances,
|
||||
});
|
||||
|
||||
const maxMana = computeMaxMana(
|
||||
{ skills, skillTiers, skillUpgrades },
|
||||
const maxMana = computeTotalMaxMana(
|
||||
{ skills, prestigeUpgrades, skillUpgrades, skillTiers, equippedInstances, equipmentInstances },
|
||||
upgradeEffects
|
||||
);
|
||||
const baseRegen = computeRegen(
|
||||
{ skills, skillTiers, skillUpgrades },
|
||||
const baseRegen = computeTotalRegen(
|
||||
{ skills, prestigeUpgrades, skillUpgrades, skillTiers, equippedInstances, equipmentInstances },
|
||||
upgradeEffects
|
||||
);
|
||||
const clickMana = computeTotalClickMana(
|
||||
{ skills, skillUpgrades, skillTiers, equippedInstances, equipmentInstances },
|
||||
upgradeEffects
|
||||
);
|
||||
const clickMana = computeClickMana({
|
||||
skills,
|
||||
skillTiers,
|
||||
skillUpgrades,
|
||||
});
|
||||
const meditationMultiplier = getMeditationBonus(meditateTicks, skills, upgradeEffects.meditationEfficiency);
|
||||
const incursionStrength = getIncursionStrength(day, hour);
|
||||
const effectiveRegen = baseRegen * (1 - incursionStrength) * meditationMultiplier;
|
||||
@@ -127,7 +129,7 @@ export function LeftPanel() {
|
||||
<DebugName name="ActionButtons">
|
||||
<ActionButtons
|
||||
currentAction={currentAction}
|
||||
currentStudyTarget={currentStudyTarget}
|
||||
currentStudyTarget={currentStudyTarget as any}
|
||||
designProgress={designProgress}
|
||||
designProgress2={designProgress2}
|
||||
preparationProgress={preparationProgress}
|
||||
|
||||
+5
-1
@@ -112,7 +112,11 @@ function GrimoireTab() {
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 mb-3">{spell.desc}</p>
|
||||
<div className="text-xs text-gray-500 space-y-1">
|
||||
<div>Cost: {(spell.cost as any[]).map((c: any) => `${c.amount} ${c.type}`).join(', ')}</div>
|
||||
<div>Cost: {spell.cost.amount} {
|
||||
spell.cost.type === 'element'
|
||||
? spell.cost.element
|
||||
: 'raw mana'
|
||||
}</div>
|
||||
<div>Power: {spell.power}</div>
|
||||
{spell.effect && <div>Effect: {spell.effect}</div>}
|
||||
</div>
|
||||
|
||||
@@ -7,10 +7,12 @@ import { Zap, Shield, ShieldCheck, Wind, Heart, Mountain, BookOpen } from 'lucid
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
import { GOLEMS_DEF, getGolemDamage, getGolemAttackSpeed } from '@/lib/game/data/golems';
|
||||
import type { CombatStatsPanelProps } from '@/lib/game/types';
|
||||
import { useCombatStore } from '@/lib/game/stores';
|
||||
import { useSkillStore } from '@/lib/game/stores';
|
||||
import { usePrestigeStore } from '@/lib/game/stores';
|
||||
|
||||
export function CombatStatsPanel({
|
||||
activeEquipmentSpells,
|
||||
store,
|
||||
totalDPS,
|
||||
calcDamage,
|
||||
formatSpellCost,
|
||||
@@ -21,7 +23,11 @@ export function CombatStatsPanel({
|
||||
studySpeedMult,
|
||||
storeCurrentAction,
|
||||
}: CombatStatsPanelProps) {
|
||||
const activeGolems = store.golemancy.summonedGolems;
|
||||
const golemancy = useCombatStore((s) => s.golemancy);
|
||||
const equipmentSpellStates = useCombatStore((s) => s.equipmentSpellStates);
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const signedPacts = usePrestigeStore((s) => s.signedPacts);
|
||||
const activeGolems = golemancy.summonedGolems;
|
||||
|
||||
return (
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
@@ -39,7 +45,7 @@ export function CombatStatsPanel({
|
||||
{activeEquipmentSpells.map(({ spellId, equipmentId }) => {
|
||||
const spellDef = SPELLS_DEF[spellId];
|
||||
if (!spellDef) return null;
|
||||
const spellState = store.equipmentSpellStates?.find(
|
||||
const spellState = equipmentSpellStates?.find(
|
||||
s => s.spellId === spellId && s.sourceEquipment === equipmentId
|
||||
);
|
||||
const progress = spellState?.castProgress || 0;
|
||||
@@ -58,11 +64,11 @@ export function CombatStatsPanel({
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mb-1">
|
||||
⚔️ {fmt(calcDamage(store, spellId))} dmg • {' '}
|
||||
⚔️ {fmt(calcDamage({ skills, signedPacts }, spellId))} dmg • {' '}
|
||||
<span style={{ color: getSpellCostColor(spellDef.cost) }}>
|
||||
{formatSpellCost(spellDef.cost)}
|
||||
</span>
|
||||
{' '}• ⚡ {fmt(Math.floor(calcDamage(store, spellId) * (spellDef.castSpeed || 1)))} dmg/hr
|
||||
{' '}• ⚡ {fmt(Math.floor(calcDamage({ skills, signedPacts }, spellId) * (spellDef.castSpeed || 1)))} dmg/hr
|
||||
</div>
|
||||
{storeCurrentAction === 'climb' && (
|
||||
<div className="space-y-0.5">
|
||||
@@ -97,8 +103,8 @@ export function CombatStatsPanel({
|
||||
const golemDef = GOLEMS_DEF[summoned.golemId];
|
||||
if (!golemDef) return null;
|
||||
const elemColor = ELEMENTS[golemDef.baseManaType]?.color || '#888';
|
||||
const damage = getGolemDamage(summoned.golemId, store.skills);
|
||||
const attackSpeed = getGolemAttackSpeed(summoned.golemId, store.skills);
|
||||
const damage = getGolemDamage(summoned.golemId, skills);
|
||||
const attackSpeed = getGolemAttackSpeed(summoned.golemId, skills);
|
||||
|
||||
return (
|
||||
<div key={summoned.golemId} className="p-2 bg-gray-800/50 rounded border border-gray-700">
|
||||
|
||||
@@ -160,7 +160,7 @@ export function EquipmentTab() {
|
||||
const handleUnequip = (slot: EquipmentSlot) => {
|
||||
const instanceId = equippedInstances[slot];
|
||||
const instance = instanceId ? equipmentInstances[instanceId] : null;
|
||||
unequipItem(slot, useCombatStore.getState, (fn) => useCombatStore.setState(fn));
|
||||
unequipItem(slot, (fn) => useCraftingStore.setState(fn as any));
|
||||
showToast('success', 'Item Unequipped', `${instance?.name || 'Item'} removed from ${SLOT_NAMES[slot]}`);
|
||||
};
|
||||
|
||||
@@ -224,7 +224,7 @@ export function EquipmentTab() {
|
||||
|
||||
const confirmDelete = () => {
|
||||
if (deleteConfirm) {
|
||||
deleteEquipmentInstance(deleteConfirm.instanceId, useCombatStore.getState, (fn) => useCombatStore.setState(fn));
|
||||
deleteEquipmentInstance(deleteConfirm.instanceId, useCraftingStore.getState, (fn) => useCraftingStore.setState(fn));
|
||||
showToast('success', 'Item Discarded', `${deleteConfirm.name} has been removed from inventory`);
|
||||
setDeleteConfirm(null);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { useCraftingStore } from '@/lib/game/stores';
|
||||
import { initialCraftingState } from '@/lib/game/stores/craftingStore';
|
||||
|
||||
describe('useCraftingStore - Equipment Actions', () => {
|
||||
beforeEach(() => {
|
||||
useCraftingStore.setState(initialCraftingState);
|
||||
});
|
||||
|
||||
it('equipItem sets equippedInstances[slot] to instanceId', () => {
|
||||
const instanceId = 'test-instance-1';
|
||||
const slot = 'mainHand';
|
||||
|
||||
// First, add the instance to equipmentInstances
|
||||
useCraftingStore.setState((state) => ({
|
||||
equipmentInstances: {
|
||||
...state.equipmentInstances,
|
||||
[instanceId]: {
|
||||
id: instanceId,
|
||||
equipmentId: 'test-equip',
|
||||
name: 'Test Sword',
|
||||
rarity: 'common',
|
||||
level: 1,
|
||||
upgrades: [],
|
||||
createdAt: Date.now(),
|
||||
} as any,
|
||||
},
|
||||
}));
|
||||
|
||||
// Equip the item
|
||||
useCraftingStore.getState().equipItem(slot, instanceId);
|
||||
|
||||
expect(useCraftingStore.getState().equippedInstances[slot]).toBe(instanceId);
|
||||
});
|
||||
|
||||
it('unequipItem sets equippedInstances[slot] to null', () => {
|
||||
const instanceId = 'test-instance-1';
|
||||
const slot = 'mainHand';
|
||||
|
||||
// First equip the item
|
||||
useCraftingStore.setState((state) => ({
|
||||
equipmentInstances: {
|
||||
...state.equipmentInstances,
|
||||
[instanceId]: {
|
||||
id: instanceId,
|
||||
equipmentId: 'test-equip',
|
||||
name: 'Test Sword',
|
||||
rarity: 'common',
|
||||
level: 1,
|
||||
upgrades: [],
|
||||
createdAt: Date.now(),
|
||||
} as any,
|
||||
},
|
||||
equippedInstances: {
|
||||
...state.equippedInstances,
|
||||
[slot]: instanceId,
|
||||
},
|
||||
}));
|
||||
|
||||
// Unequip the item
|
||||
useCraftingStore.getState().unequipItem(slot);
|
||||
|
||||
expect(useCraftingStore.getState().equippedInstances[slot]).toBeNull();
|
||||
});
|
||||
|
||||
it('deleteEquipmentInstance removes from both equippedInstances and equipmentInstances', () => {
|
||||
const instanceId = 'test-instance-1';
|
||||
const slot = 'mainHand';
|
||||
|
||||
// Add and equip the item
|
||||
useCraftingStore.setState((state) => ({
|
||||
equipmentInstances: {
|
||||
...state.equipmentInstances,
|
||||
[instanceId]: {
|
||||
id: instanceId,
|
||||
equipmentId: 'test-equip',
|
||||
name: 'Test Sword',
|
||||
rarity: 'common',
|
||||
level: 1,
|
||||
upgrades: [],
|
||||
createdAt: Date.now(),
|
||||
} as any,
|
||||
},
|
||||
equippedInstances: {
|
||||
...state.equippedInstances,
|
||||
[slot]: instanceId,
|
||||
},
|
||||
}));
|
||||
|
||||
// Delete the item
|
||||
useCraftingStore.getState().deleteEquipmentInstance(instanceId);
|
||||
|
||||
const state = useCraftingStore.getState();
|
||||
expect(state.equipmentInstances[instanceId]).toBeUndefined();
|
||||
expect(state.equippedInstances[slot]).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { useManaStore } from '@/lib/game/stores';
|
||||
import { initialManaState } from '@/lib/game/stores/manaStore';
|
||||
|
||||
describe('useManaStore', () => {
|
||||
beforeEach(() => {
|
||||
useManaStore.setState(initialManaState);
|
||||
});
|
||||
|
||||
it('spendRawMana reduces rawMana correctly', () => {
|
||||
useManaStore.setState({ rawMana: 100 });
|
||||
const result = useManaStore.getState().spendRawMana(30);
|
||||
expect(result).toBe(true);
|
||||
expect(useManaStore.getState().rawMana).toBe(70);
|
||||
});
|
||||
|
||||
it('spendRawMana returns false if insufficient mana', () => {
|
||||
useManaStore.setState({ rawMana: 20 });
|
||||
const result = useManaStore.getState().spendRawMana(50);
|
||||
expect(result).toBe(false);
|
||||
expect(useManaStore.getState().rawMana).toBe(20); // unchanged
|
||||
});
|
||||
|
||||
it('rawMana never goes below 0', () => {
|
||||
useManaStore.setState({ rawMana: 10 });
|
||||
const result = useManaStore.getState().spendRawMana(20);
|
||||
expect(result).toBe(false);
|
||||
expect(useManaStore.getState().rawMana).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { computeRegen } from '@/lib/game/store-modules/computed-stats';
|
||||
import { getIncursionStrength } from '@/lib/game/store-modules/computed-stats';
|
||||
import { useSkillStore } from '@/lib/game/stores';
|
||||
import { initialSkillState } from '@/lib/game/stores/skillStore';
|
||||
|
||||
describe('computeRegen', () => {
|
||||
beforeEach(() => {
|
||||
useSkillStore.setState(initialSkillState);
|
||||
});
|
||||
|
||||
it('Returns 0 when no skills', () => {
|
||||
const result = computeRegen({
|
||||
skills: {},
|
||||
skillTiers: {},
|
||||
skillUpgrades: {},
|
||||
});
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('Increases with manaWell skill level', () => {
|
||||
const base = computeRegen({
|
||||
skills: { manaWell: 0 },
|
||||
skillTiers: {},
|
||||
skillUpgrades: {},
|
||||
});
|
||||
|
||||
const withSkill = computeRegen({
|
||||
skills: { manaWell: 5 },
|
||||
skillTiers: {},
|
||||
skillUpgrades: {},
|
||||
});
|
||||
|
||||
expect(withSkill).toBeGreaterThan(base);
|
||||
});
|
||||
});
|
||||
|
||||
describe('effectiveRegen', () => {
|
||||
it('effectiveRegen = baseRegen * (1 - incursionStrength)', () => {
|
||||
const baseRegen = 10;
|
||||
const day = 20; // After incursion start
|
||||
const hour = 12;
|
||||
const incursionStrength = getIncursionStrength(day, hour);
|
||||
|
||||
const effectiveRegen = baseRegen * (1 - incursionStrength);
|
||||
|
||||
expect(effectiveRegen).toBeLessThan(baseRegen);
|
||||
expect(effectiveRegen).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { useSkillStore } from '@/lib/game/stores';
|
||||
import { useManaStore } from '@/lib/game/stores';
|
||||
import { initialSkillState } from '@/lib/game/stores/skillStore';
|
||||
import { initialManaState } from '@/lib/game/stores/manaStore';
|
||||
|
||||
describe('useSkillStore', () => {
|
||||
beforeEach(() => {
|
||||
useSkillStore.setState(initialSkillState);
|
||||
useManaStore.setState(initialManaState);
|
||||
});
|
||||
|
||||
it('startStudyingSkill returns { started: false } if rawMana < cost', () => {
|
||||
// Set rawMana to 0
|
||||
useManaStore.setState({ rawMana: 0 });
|
||||
const result = useSkillStore.getState().startStudyingSkill('manaWell', false);
|
||||
expect(result.started).toBe(false);
|
||||
});
|
||||
|
||||
it('startStudyingSkill deducts mana via manaStore.spendRawMana when started', () => {
|
||||
// Set rawMana to 100, skill cost is maybe 50? We need to know the cost.
|
||||
// Let's mock spendRawMana to track calls
|
||||
const spendRawManaSpy = vi.spyOn(useManaStore.getState(), 'spendRawMana');
|
||||
useManaStore.setState({ rawMana: 100 });
|
||||
|
||||
const result = useSkillStore.getState().startStudyingSkill('manaWell', false);
|
||||
|
||||
if (result.started) {
|
||||
expect(spendRawManaSpy).toHaveBeenCalledWith(result.cost);
|
||||
}
|
||||
});
|
||||
|
||||
it('startStudyingSkill does NOT deduct if isAlreadyPaid', () => {
|
||||
const spendRawManaSpy = vi.spyOn(useManaStore.getState(), 'spendRawMana');
|
||||
useManaStore.setState({ rawMana: 100 });
|
||||
|
||||
const result = useSkillStore.getState().startStudyingSkill('manaWell', true);
|
||||
|
||||
if (result.started) {
|
||||
expect(spendRawManaSpy).not.toHaveBeenCalled();
|
||||
expect(result.cost).toBe(0); // cost should be 0 if already paid
|
||||
}
|
||||
});
|
||||
|
||||
it('cancelStudy clears currentStudyTarget', () => {
|
||||
// First start studying
|
||||
useManaStore.setState({ rawMana: 100 });
|
||||
useSkillStore.getState().startStudyingSkill('manaWell', false);
|
||||
expect(useSkillStore.getState().currentStudyTarget).not.toBeNull();
|
||||
|
||||
// Cancel study
|
||||
useSkillStore.getState().cancelStudy();
|
||||
expect(useSkillStore.getState().currentStudyTarget).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { canAffordSpellCost } from '@/lib/game/stores';
|
||||
import { formatSpellCost } from '@/lib/game/formatting';
|
||||
import { useManaStore } from '@/lib/game/stores';
|
||||
import { initialManaState } from '@/lib/game/stores/manaStore';
|
||||
import type { SpellCost } from '@/lib/game/types';
|
||||
|
||||
describe('canAffordSpellCost', () => {
|
||||
beforeEach(() => {
|
||||
useManaStore.setState(initialManaState);
|
||||
});
|
||||
|
||||
it('returns true when rawMana >= cost.amount (type: raw)', () => {
|
||||
useManaStore.setState({ rawMana: 100 });
|
||||
const cost: SpellCost = { type: 'raw', amount: 50 };
|
||||
expect(canAffordSpellCost(cost)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when insufficient rawMana', () => {
|
||||
useManaStore.setState({ rawMana: 20 });
|
||||
const cost: SpellCost = { type: 'raw', amount: 50 };
|
||||
expect(canAffordSpellCost(cost)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when has enough element mana', () => {
|
||||
// Set initial state first
|
||||
useManaStore.setState(initialManaState);
|
||||
// Then update the Fire element
|
||||
useManaStore.setState((state) => ({
|
||||
elements: {
|
||||
...state.elements,
|
||||
Fire: { amount: 100, max: 200, rate: 0 },
|
||||
},
|
||||
}));
|
||||
const cost: SpellCost = { type: 'element', element: 'Fire', amount: 50 };
|
||||
expect(canAffordSpellCost(cost)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatSpellCost', () => {
|
||||
it('returns a non-empty string for raw cost', () => {
|
||||
const cost: SpellCost = { type: 'raw', amount: 50 };
|
||||
const result = formatSpellCost(cost);
|
||||
expect(result).toBeTruthy();
|
||||
expect(typeof result).toBe('string');
|
||||
});
|
||||
|
||||
it('returns a non-empty string for element cost', () => {
|
||||
const cost: SpellCost = { type: 'element', element: 'Fire', amount: 30 };
|
||||
const result = formatSpellCost(cost);
|
||||
expect(result).toBeTruthy();
|
||||
expect(typeof result).toBe('string');
|
||||
});
|
||||
|
||||
it('does NOT throw when cost.type is element', () => {
|
||||
const cost: SpellCost = { type: 'element', element: 'Water', amount: 25 };
|
||||
expect(() => formatSpellCost(cost)).not.toThrow();
|
||||
});
|
||||
|
||||
it('handles edge case: zero amount', () => {
|
||||
const cost: SpellCost = { type: 'raw', amount: 0 };
|
||||
expect(() => formatSpellCost(cost)).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
@@ -14,4 +15,9 @@ export default defineConfig({
|
||||
],
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user