feat(library-manifest): per-board manifests + autocomplete
Library manifests are now PER-BOARD (each board carries its own velxio.json), so two boards in one project can use different (even conflicting) libraries without clashing — the multi-board extension of the no-clash guarantee. - board.libraries on BoardInstance + serialisableBoard: rides in boards_json, so it round-trips, dirty-checks, autosaves and restores natively. This also removes the load-restore hacks (useLibraryManifestStore + applyProjectManifest deleted): the manifest is plain board state. - loadProjectState now restores per-board boardOptions/spiffsFiles/libraries (it previously dropped them). - EditorToolbar single + compile-all send the COMPILING board's libraries. - Backend compile.py prefers the client's per-board request.libraries; the project-level libraries_json (now the union of all boards) is the fallback. - buildLoadPayload migrates pre-per-board projects: seed each board with the project union so they keep compiling scoped. - Library Manager 'In project' tab edits the ACTIVE board's velxio.json (shows the board name) and the add field is now an autocomplete (installed libs + index search) so users pick from a list instead of typing names. Deletes useLibraryManifestStore.ts + applyProjectManifest.ts.
This commit is contained in:
parent
c93924541c
commit
8617d3b224
|
|
@ -215,17 +215,20 @@ async def _run_compile(
|
|||
[f.model_dump() for f in request.spiffs_files]
|
||||
if request.spiffs_files else None
|
||||
)
|
||||
# Library manifest = ESP-IDF resolution SCOPE. Prefer the manifest SAVED
|
||||
# with the project (authoritative, read server-side from the project
|
||||
# record) so a reloaded project always scopes to its own libraries
|
||||
# regardless of what the client sends; fall back to the manifest the
|
||||
# client sent (unsaved examples). None/empty → legacy scan-all.
|
||||
# Library manifest = ESP-IDF resolution SCOPE. Manifests are now
|
||||
# PER-BOARD (each board carries its own velxio.json), and the client
|
||||
# sends the COMPILING board's manifest in request.libraries — so it
|
||||
# takes precedence: two boards in one project can scope to different
|
||||
# libraries. Fall back to the project-level manifest (the union of all
|
||||
# boards, read server-side) only when the client sends none — e.g. an
|
||||
# anonymous compile or an old client. None/empty → legacy scan-all.
|
||||
allowed_libraries = None
|
||||
project_libs = await get_project_libraries(request.project_id)
|
||||
if project_libs:
|
||||
allowed_libraries = set(project_libs)
|
||||
elif request.libraries:
|
||||
if request.libraries:
|
||||
allowed_libraries = set(request.libraries)
|
||||
else:
|
||||
project_libs = await get_project_libraries(request.project_id)
|
||||
if project_libs:
|
||||
allowed_libraries = set(project_libs)
|
||||
result = await espidf_compiler.compile(
|
||||
files, request.board_fqbn,
|
||||
progress_callback=progress_callback,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import { clearChipDrives } from '../../simulation/customChips/chipPinDrives';
|
|||
import { requestElectricalResolve } from '../../simulation/spice/electricalResolveHook';
|
||||
import { reportRunEvent } from '../../services/metricsService';
|
||||
import { useProjectStore } from '../../store/useProjectStore';
|
||||
import { useLibraryManifestStore } from '../../store/useLibraryManifestStore';
|
||||
import { LibraryManagerModal } from '../simulator/LibraryManagerModal';
|
||||
import { InstallLibrariesModal } from '../simulator/InstallLibrariesModal';
|
||||
import { parseCompileResult } from '../../utils/compilationLogger';
|
||||
|
|
@ -171,8 +170,6 @@ export const EditorToolbar = ({
|
|||
|
||||
const activeBoard = boards.find((b) => b.id === activeBoardId) ?? boards[0];
|
||||
const currentProject = useProjectStore((s) => s.currentProject);
|
||||
// P2.3 — declared library manifest of the loaded example/project (compile scope).
|
||||
const activeLibraries = useLibraryManifestStore((s) => s.libraries);
|
||||
|
||||
// Board-less mode: digital / analog SPICE-only circuits. The Run / Stop
|
||||
// buttons toggle the SPICE solver's `paused` flag — pausing freezes every
|
||||
|
|
@ -520,7 +517,9 @@ export const EditorToolbar = ({
|
|||
{
|
||||
boardOptions: activeBoard?.boardOptions,
|
||||
spiffsFiles: activeBoard?.spiffsFiles,
|
||||
libraries: activeLibraries,
|
||||
// P2.4 — THIS board's declared manifest (compile scope). Per-board so
|
||||
// two boards can use different libraries without clashing.
|
||||
libraries: activeBoard?.libraries?.length ? activeBoard.libraries : null,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -993,7 +992,7 @@ export const EditorToolbar = ({
|
|||
})),
|
||||
]);
|
||||
},
|
||||
{ boardOptions: board.boardOptions, spiffsFiles: board.spiffsFiles, libraries: activeLibraries },
|
||||
{ boardOptions: board.boardOptions, spiffsFiles: board.spiffsFiles, libraries: board.libraries?.length ? board.libraries : null },
|
||||
);
|
||||
|
||||
const resultLogs = parseCompileResult(result, label, boardTarget);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { useEditorStore, chipFileGroupId } from '../../store/useEditorStore';
|
||||
import { useSimulatorStore } from '../../store/useSimulatorStore';
|
||||
import { useLibraryManifestStore } from '../../store/useLibraryManifestStore';
|
||||
import {
|
||||
isProgrammableChip,
|
||||
targetForChip,
|
||||
|
|
@ -300,9 +299,10 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
|
|||
renameFile,
|
||||
setActiveGroup,
|
||||
} = useEditorStore();
|
||||
const manifestLibs = useLibraryManifestStore((s) => s.libraries);
|
||||
const boards = useSimulatorStore((s) => s.boards);
|
||||
const activeBoardId = useSimulatorStore((s) => s.activeBoardId);
|
||||
// P2.4 — velxio.json is per-board: show the ACTIVE board's declared manifest.
|
||||
const manifestLibs = boards.find((b) => b.id === activeBoardId)?.libraries ?? null;
|
||||
const setActiveBoardId = useSimulatorStore((s) => s.setActiveBoardId);
|
||||
const updateBoard = useSimulatorStore((s) => s.updateBoard);
|
||||
const updateComponent = useSimulatorStore((s) => s.updateComponent);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import {
|
|||
} from '../../services/libraryService';
|
||||
import type { ArduinoLibrary, InstalledLibrary } from '../../services/libraryService';
|
||||
import { trackInstallLibrary } from '../../utils/analytics';
|
||||
import { useLibraryManifestStore } from '../../store/useLibraryManifestStore';
|
||||
import { useSimulatorStore } from '../../store/useSimulatorStore';
|
||||
import { boardDisplayName } from '../../types/board';
|
||||
import './LibraryManagerModal.css';
|
||||
|
||||
interface LibraryManagerModalProps {
|
||||
|
|
@ -27,13 +28,27 @@ const normLib = (s: string): string =>
|
|||
export const LibraryManagerModal: React.FC<LibraryManagerModalProps> = ({ isOpen, onClose }) => {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState<Tab>('search');
|
||||
// P2.4 — the project's declared library manifest (compile scope) lives here.
|
||||
const manifestLibs = useLibraryManifestStore((s) => s.libraries);
|
||||
const setLibraries = useLibraryManifestStore((s) => s.setLibraries);
|
||||
// P2.4 — manifests are PER-BOARD: the velxio.json edited here belongs to the
|
||||
// ACTIVE board, so two boards in one project can scope to different libraries.
|
||||
const boards = useSimulatorStore((s) => s.boards);
|
||||
const activeBoardId = useSimulatorStore((s) => s.activeBoardId);
|
||||
const updateBoard = useSimulatorStore((s) => s.updateBoard);
|
||||
const activeBoard = boards.find((b) => b.id === activeBoardId) ?? boards[0];
|
||||
const manifestLibs = activeBoard?.libraries ?? null;
|
||||
const setLibraries = useCallback(
|
||||
(libs: string[] | null) => {
|
||||
if (!activeBoard) return;
|
||||
updateBoard(activeBoard.id, { libraries: libs && libs.length ? libs : undefined });
|
||||
},
|
||||
[activeBoard, updateBoard],
|
||||
);
|
||||
// Raw velxio.json editor draft + parse error (the Wokwi-style view).
|
||||
const [jsonDraft, setJsonDraft] = useState('');
|
||||
const [jsonError, setJsonError] = useState<string | null>(null);
|
||||
const [newLibName, setNewLibName] = useState('');
|
||||
// Autocomplete suggestions for the "add library" field (index search).
|
||||
const [addSuggestions, setAddSuggestions] = useState<string[]>([]);
|
||||
const addDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchResults, setSearchResults] = useState<ArduinoLibrary[]>([]);
|
||||
const [installedLibraries, setInstalledLibraries] = useState<InstalledLibrary[]>([]);
|
||||
|
|
@ -163,22 +178,62 @@ export const LibraryManagerModal: React.FC<LibraryManagerModalProps> = ({ isOpen
|
|||
(name: string) => {
|
||||
const clean = name.trim();
|
||||
if (!clean) return;
|
||||
const cur = useLibraryManifestStore.getState().libraries ?? [];
|
||||
const cur = manifestLibs ?? [];
|
||||
if (cur.some((l) => normLib(l) === normLib(clean))) return;
|
||||
setLibraries([...cur, clean]);
|
||||
},
|
||||
[setLibraries],
|
||||
[manifestLibs, setLibraries],
|
||||
);
|
||||
|
||||
const removeFromManifest = useCallback(
|
||||
(name: string) => {
|
||||
const cur = useLibraryManifestStore.getState().libraries ?? [];
|
||||
const cur = manifestLibs ?? [];
|
||||
const next = cur.filter((l) => normLib(l) !== normLib(name));
|
||||
setLibraries(next.length ? next : null);
|
||||
},
|
||||
[setLibraries],
|
||||
[manifestLibs, setLibraries],
|
||||
);
|
||||
|
||||
// Autocomplete: debounced index search for the "add library" field. Combined
|
||||
// with instant matches from the installed list in `addOptions` below.
|
||||
useEffect(() => {
|
||||
if (addDebounceRef.current) clearTimeout(addDebounceRef.current);
|
||||
const q = newLibName.trim();
|
||||
if (q.length < 2) {
|
||||
setAddSuggestions([]);
|
||||
return;
|
||||
}
|
||||
addDebounceRef.current = setTimeout(async () => {
|
||||
try {
|
||||
const results = await searchLibraries(q);
|
||||
setAddSuggestions(results.map((r) => r.name).filter(Boolean));
|
||||
} catch {
|
||||
setAddSuggestions([]);
|
||||
}
|
||||
}, 300);
|
||||
return () => {
|
||||
if (addDebounceRef.current) clearTimeout(addDebounceRef.current);
|
||||
};
|
||||
}, [newLibName]);
|
||||
|
||||
// Merged, de-duped suggestions: installed libs first (instant), then index
|
||||
// results, excluding what's already declared. Capped for a tidy dropdown.
|
||||
const addOptions = (() => {
|
||||
const q = normLib(newLibName);
|
||||
if (!q) return [];
|
||||
const installedNames = installedLibraries
|
||||
.map((il) => il.library?.name || il.name || '')
|
||||
.filter(Boolean);
|
||||
const merged: string[] = [];
|
||||
for (const n of [...installedNames, ...addSuggestions]) {
|
||||
if (!normLib(n).includes(q)) continue;
|
||||
if (declared.some((d) => normLib(d) === normLib(n))) continue;
|
||||
if (merged.some((m) => normLib(m) === normLib(n))) continue;
|
||||
merged.push(n);
|
||||
}
|
||||
return merged.slice(0, 8);
|
||||
})();
|
||||
|
||||
const applyJsonDraft = useCallback(() => {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonDraft || '{}');
|
||||
|
|
@ -334,10 +389,14 @@ export const LibraryManagerModal: React.FC<LibraryManagerModalProps> = ({ isOpen
|
|||
{activeTab === 'project' && (
|
||||
<div className="lib-content">
|
||||
<div style={{ padding: '10px 14px', color: '#9d9d9d', fontSize: 12, lineHeight: 1.5 }}>
|
||||
Libraries this project uses (its <strong style={{ color: '#ffd60a' }}>velxio.json</strong>).
|
||||
They become the compile scope so libraries never clash between
|
||||
projects. Installing a library from the Search tab adds it here
|
||||
automatically; you can also add or remove them below.
|
||||
Libraries used by{' '}
|
||||
<strong style={{ color: '#a5d6a7' }}>
|
||||
{activeBoard ? boardDisplayName(activeBoard) : 'this board'}
|
||||
</strong>{' '}
|
||||
(its <strong style={{ color: '#ffd60a' }}>velxio.json</strong>). Each
|
||||
board has its own list, so two boards can use different libraries
|
||||
without clashing. Installing a library adds it here automatically;
|
||||
start typing below to add more.
|
||||
</div>
|
||||
|
||||
{/* Declared libraries as removable rows */}
|
||||
|
|
@ -371,30 +430,73 @@ export const LibraryManagerModal: React.FC<LibraryManagerModalProps> = ({ isOpen
|
|||
))}
|
||||
</div>
|
||||
|
||||
{/* Quick add by name */}
|
||||
<div className="lib-search-bar" style={{ marginTop: 8 }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Add a library by name (e.g. Adafruit GFX Library)"
|
||||
value={newLibName}
|
||||
onChange={(e) => setNewLibName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && newLibName.trim()) {
|
||||
addToManifest(newLibName);
|
||||
setNewLibName('');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className="lib-install-btn"
|
||||
disabled={!newLibName.trim()}
|
||||
onClick={() => {
|
||||
addToManifest(newLibName);
|
||||
setNewLibName('');
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
{/* Add a library — autocomplete (installed + index search) so the
|
||||
user picks from a list instead of typing the exact name. */}
|
||||
<div style={{ position: 'relative', marginTop: 8 }}>
|
||||
<div className="lib-search-bar" style={{ margin: 0 }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Add a library — start typing to search…"
|
||||
value={newLibName}
|
||||
onChange={(e) => setNewLibName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const pick = addOptions[0] ?? newLibName;
|
||||
if (pick.trim()) {
|
||||
addToManifest(pick);
|
||||
setNewLibName('');
|
||||
setAddSuggestions([]);
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
setNewLibName('');
|
||||
setAddSuggestions([]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{addOptions.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 5,
|
||||
background: '#252526',
|
||||
border: '1px solid #3c3c3c',
|
||||
borderRadius: 4,
|
||||
marginTop: 2,
|
||||
maxHeight: 200,
|
||||
overflowY: 'auto',
|
||||
boxShadow: '0 6px 16px rgba(0,0,0,0.4)',
|
||||
}}
|
||||
>
|
||||
{addOptions.map((opt) => (
|
||||
<button
|
||||
key={opt}
|
||||
onClick={() => {
|
||||
addToManifest(opt);
|
||||
setNewLibName('');
|
||||
setAddSuggestions([]);
|
||||
}}
|
||||
style={{
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
textAlign: 'left',
|
||||
padding: '7px 12px',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: '#d4d4d4',
|
||||
fontSize: 13,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.background = '#094771')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
|
||||
>
|
||||
{opt}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Raw velxio.json editor (Wokwi-style) for power users */}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { useParams, useNavigate } from 'react-router-dom';
|
|||
import { getProjectById } from '../services/projectService';
|
||||
import { useSimulatorStore } from '../store/useSimulatorStore';
|
||||
import { useProjectStore } from '../store/useProjectStore';
|
||||
import { applyProjectManifest } from '../utils/applyProjectManifest';
|
||||
import { useSEO } from '../utils/useSEO';
|
||||
import { EditorPage } from './EditorPage';
|
||||
import type { BoardInstance, BoardKind } from '../types/board';
|
||||
|
|
@ -56,12 +55,10 @@ export const ProjectByIdPage: React.FC = () => {
|
|||
getProjectById(id)
|
||||
.then((project) => {
|
||||
const payload = buildLoadPayload(project);
|
||||
// Per-board manifests ride in boards_json (buildLoadPayload migrates
|
||||
// pre-per-board projects), so loadProjectState restores each board's
|
||||
// compile scope directly.
|
||||
loadProjectState(payload);
|
||||
// P2.4 — restore the declared library manifest (compile scope) so the
|
||||
// editor, toolbar, Library Manager and velxio.json reflect it. Done via
|
||||
// a dedicated side-effecting util (not inside buildLoadPayload) so the
|
||||
// store write survives esbuild's DCE — see applyProjectManifest.
|
||||
applyProjectManifest(project.libraries_json);
|
||||
setCurrentProject({
|
||||
id: project.id,
|
||||
slug: project.slug,
|
||||
|
|
@ -174,6 +171,8 @@ export function buildLoadPayload(project: RawProject) {
|
|||
// pre-feature projects; the compiler falls back to its defaults.
|
||||
boardOptions: b.boardOptions,
|
||||
spiffsFiles: b.spiffsFiles,
|
||||
// P2.4 — this board's declared manifest (per-board compile scope).
|
||||
libraries: b.libraries,
|
||||
}));
|
||||
}
|
||||
} catch {
|
||||
|
|
@ -199,6 +198,20 @@ export function buildLoadPayload(project: RawProject) {
|
|||
];
|
||||
}
|
||||
|
||||
// P2.4 migration — projects saved before per-board manifests stored a single
|
||||
// project-level manifest (libraries_json). If no board carries its own, seed
|
||||
// every board with the project union so it keeps compiling scoped.
|
||||
if (!boards.some((b) => b.libraries && b.libraries.length)) {
|
||||
try {
|
||||
const union = JSON.parse(project.libraries_json || '[]');
|
||||
if (Array.isArray(union) && union.length) {
|
||||
for (const b of boards) b.libraries = union as string[];
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// File groups
|
||||
const fileGroups: Record<string, { name: string; content: string }[]> = {};
|
||||
if (project.file_groups && project.file_groups.length > 0) {
|
||||
|
|
@ -237,9 +250,9 @@ export function buildLoadPayload(project: RawProject) {
|
|||
wires = [];
|
||||
}
|
||||
|
||||
// NB: the project's declared library manifest (project.libraries_json) is
|
||||
// NOT restored here — esbuild DCE'd the store write when it lived in this
|
||||
// value-producer helper. The caller applies it via applyProjectManifest().
|
||||
// Per-board library manifests ride inside each board (boards_json) and were
|
||||
// migrated above for pre-per-board projects, so loadProjectState restores the
|
||||
// compile scope along with the boards — no separate step needed.
|
||||
return {
|
||||
boards,
|
||||
fileGroups,
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
import { create } from 'zustand';
|
||||
|
||||
/**
|
||||
* Library manifest for the currently-loaded example/project (P2.3).
|
||||
*
|
||||
* The declared library set is sent to the backend compiler as the resolution
|
||||
* SCOPE: ESP-IDF then merges only these libraries (plus the core), so a sketch
|
||||
* picks the declared lib and never an unrelated one from the shared dir.
|
||||
*
|
||||
* `null` = no manifest -> legacy scan-all (unchanged behaviour). Set by
|
||||
* `loadExample` to the example's declared `libraries` (or null for core-only
|
||||
* examples). Switching between examples updates it; loading a non-example
|
||||
* workspace leaves the previous value, but that only degrades to scan-all via
|
||||
* the backend's graceful fallback — never an incorrect build.
|
||||
*/
|
||||
interface LibraryManifestState {
|
||||
libraries: string[] | null;
|
||||
setLibraries: (libs: string[] | null | undefined) => void;
|
||||
}
|
||||
|
||||
export const useLibraryManifestStore = create<LibraryManifestState>((set) => ({
|
||||
libraries: null,
|
||||
setLibraries: (libs) => set({ libraries: libs && libs.length ? libs : null }),
|
||||
}));
|
||||
|
|
@ -1280,6 +1280,10 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
const patch: Partial<BoardInstance> = {};
|
||||
if (b.languageMode && b.languageMode !== 'arduino') patch.languageMode = b.languageMode;
|
||||
if (b.name && b.name.trim()) patch.name = b.name;
|
||||
// P2.4 — restore per-board persisted fields that ride in boards_json.
|
||||
if (b.boardOptions) patch.boardOptions = b.boardOptions;
|
||||
if (b.spiffsFiles) patch.spiffsFiles = b.spiffsFiles;
|
||||
if (b.libraries && b.libraries.length) patch.libraries = b.libraries;
|
||||
if (Object.keys(patch).length > 0) {
|
||||
set((s) => ({
|
||||
boards: s.boards.map((bb) => (bb.id === b.id ? { ...bb, ...patch } : bb)),
|
||||
|
|
|
|||
|
|
@ -98,6 +98,11 @@ export interface BoardInstance {
|
|||
// Types live in `./boardOptions` to avoid a circular import.
|
||||
boardOptions?: import('./boardOptions').ESP32BoardOptions;
|
||||
spiffsFiles?: import('./boardOptions').SpiffsFile[];
|
||||
// P2.4 — this board's declared library manifest (its velxio.json). The ESP32
|
||||
// compile scope: each board resolves ONLY its own declared libraries, so two
|
||||
// boards in the same project can use different (even conflicting) libraries
|
||||
// without clashing. Undefined for pre-feature boards (-> legacy scan-all).
|
||||
libraries?: string[];
|
||||
}
|
||||
|
||||
export const BOARD_KIND_LABELS: Record<BoardKind, string> = {
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
import { useLibraryManifestStore } from '../store/useLibraryManifestStore';
|
||||
|
||||
/**
|
||||
* Apply a saved project's declared library manifest (the compile scope) to the
|
||||
* manifest store on load, so the editor, toolbar, Library Manager and
|
||||
* velxio.json all reflect it. `null`/empty -> no scope (legacy scan-all).
|
||||
*
|
||||
* Lives in its own util so both the OSS and the pro-overlay ProjectByIdPage
|
||||
* (the one actually routed on velxio.dev) restore the manifest identically.
|
||||
*/
|
||||
export function applyProjectManifest(librariesJson: string | undefined | null): void {
|
||||
try {
|
||||
const parsed = JSON.parse(librariesJson || '[]');
|
||||
const libs = Array.isArray(parsed) && parsed.length ? (parsed as string[]) : null;
|
||||
useLibraryManifestStore.getState().setLibraries(libs);
|
||||
} catch {
|
||||
useLibraryManifestStore.getState().setLibraries(null);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@ import { useSimulatorStore, DEFAULT_BOARD_POSITION } from '../store/useSimulator
|
|||
import { useElectricalStore } from '../store/useElectricalStore';
|
||||
import { useProjectStore } from '../store/useProjectStore';
|
||||
import { useVfsStore } from '../store/useVfsStore';
|
||||
import { useLibraryManifestStore } from '../store/useLibraryManifestStore';
|
||||
import { isBoardComponent } from './boardPinMapping';
|
||||
import { getInstalledLibraries, installLibrary } from '../services/libraryService';
|
||||
import { trackOpenExample } from './analytics';
|
||||
|
|
@ -116,10 +115,8 @@ export async function loadExample(
|
|||
// so no PUT goes out.
|
||||
useProjectStore.getState().clearCurrentProject();
|
||||
|
||||
// P2.3 — record this example's declared library manifest as the compile
|
||||
// scope (null for core-only examples). EditorToolbar sends it with every
|
||||
// compile so ESP-IDF resolution merges exactly these libraries.
|
||||
useLibraryManifestStore.getState().setLibraries(example.libraries ?? null);
|
||||
// P2.4 — this example's declared manifest (compile scope) is assigned to each
|
||||
// board it creates at the END of this function (the boards don't exist yet).
|
||||
|
||||
// Loading a new example always starts unpaused — otherwise the canvas
|
||||
// would open with every LED frozen at the previous example's state.
|
||||
|
|
@ -377,4 +374,14 @@ export async function loadExample(
|
|||
);
|
||||
recalculateAllWirePositions();
|
||||
}
|
||||
|
||||
// P2.4 — assign this example's declared manifest to every board it created
|
||||
// (the per-board compile scope). Examples declare one library set today, so
|
||||
// each board gets it; the user can refine per board via velxio.json.
|
||||
{
|
||||
const sim = useSimulatorStore.getState();
|
||||
const libs =
|
||||
example.libraries && example.libraries.length ? example.libraries : undefined;
|
||||
for (const b of sim.boards) sim.updateBoard(b.id, { libraries: libs });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import type { BoardInstance } from '../types/board';
|
|||
import type { Wire } from '../types/wire';
|
||||
import { useEditorStore, chipFileGroupId } from '../store/useEditorStore';
|
||||
import { useSimulatorStore } from '../store/useSimulatorStore';
|
||||
import { useLibraryManifestStore } from '../store/useLibraryManifestStore';
|
||||
|
||||
/**
|
||||
* Editor groups owned by programmable custom-chips on the canvas (those whose
|
||||
|
|
@ -45,9 +44,29 @@ function serialisableBoard(b: BoardInstance) {
|
|||
// inside boards_json so there's no DB migration.
|
||||
boardOptions: b.boardOptions,
|
||||
spiffsFiles: b.spiffsFiles,
|
||||
// P2.4 — this board's declared library manifest (compile scope). Rides in
|
||||
// boards_json so it round-trips, dirty-checks and autosaves for free.
|
||||
libraries: b.libraries,
|
||||
};
|
||||
}
|
||||
|
||||
/** Union of every board's declared library manifest, sorted + de-duped.
|
||||
* Persisted as the project-level `libraries_json` for backward-compat readers
|
||||
* and as the backend's fallback scope when a client sends no per-board list. */
|
||||
function unionBoardLibraries(boards: BoardInstance[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
const out: string[] = [];
|
||||
for (const b of boards) {
|
||||
for (const lib of b.libraries ?? []) {
|
||||
if (!seen.has(lib)) {
|
||||
seen.add(lib);
|
||||
out.push(lib);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out.sort();
|
||||
}
|
||||
|
||||
interface SnapshotInputs {
|
||||
name?: string;
|
||||
description?: string;
|
||||
|
|
@ -97,12 +116,12 @@ export function buildSavePayload(meta: SnapshotInputs = {}): ProjectSaveData {
|
|||
}));
|
||||
const fileGroups = [...boardGroups, ...chipGroups];
|
||||
|
||||
// P2.4 — declared library manifest (compile scope). Persist it ONLY when it
|
||||
// is explicitly known (non-null). null means "unknown" — e.g. a reloaded
|
||||
// project whose manifest wasn't restored into the store — so we OMIT the
|
||||
// field and the backend preserves the saved manifest instead of clobbering
|
||||
// it to []. The compiler reads the saved manifest server-side regardless.
|
||||
const manifestLibs = useLibraryManifestStore.getState().libraries;
|
||||
// P2.4 — per-board manifests live inside boards_json (serialisableBoard).
|
||||
// The project-level libraries_json is their UNION: kept for backward-compat
|
||||
// readers and as the backend's fallback compile scope when a client sends no
|
||||
// per-board list. Always sent (no clobber risk: boards_json is the source of
|
||||
// truth and round-trips natively, so the union is always recomputable).
|
||||
const unionLibs = unionBoardLibraries(sim.boards);
|
||||
|
||||
return {
|
||||
name: meta.name ?? '',
|
||||
|
|
@ -115,7 +134,7 @@ export function buildSavePayload(meta: SnapshotInputs = {}): ProjectSaveData {
|
|||
components_json: JSON.stringify(sim.components),
|
||||
wires_json: JSON.stringify(sim.wires),
|
||||
boards_json: JSON.stringify(sim.boards.map(serialisableBoard)),
|
||||
...(manifestLibs !== null ? { libraries_json: JSON.stringify(manifestLibs) } : {}),
|
||||
libraries_json: JSON.stringify(unionLibs),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -152,15 +171,13 @@ export function computeProjectStateHash(): string {
|
|||
}));
|
||||
|
||||
const payload = {
|
||||
// boards carry their per-board `libraries` via serialisableBoard, so
|
||||
// declaring/removing a library marks the project dirty and autosaves.
|
||||
boards: sim.boards.map(serialisableBoard),
|
||||
activeId: sim.activeBoardId,
|
||||
components: sim.components,
|
||||
wires: wiresHash,
|
||||
groups: groupsForHash,
|
||||
// P2.4 — include the declared library manifest so adding/removing a
|
||||
// library (e.g. via the Library Manager / velxio.json) marks the project
|
||||
// dirty and gets persisted by the auto-save hook, even with no code change.
|
||||
libraries: useLibraryManifestStore.getState().libraries,
|
||||
};
|
||||
return JSON.stringify(payload);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue