276 lines
14 KiB
Python
276 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate the React/Worker module routing table from the parity matrix."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
MATRIX = ROOT / "docs/web-parity-matrix.json"
|
|
SRC = ROOT / "web/src"
|
|
OUT = SRC / "module-registry.generated.ts"
|
|
|
|
COMMAND_RE = re.compile(r'export\s+const\s+\w*COMMAND\s*=\s*["\']([^"\']+)["\']')
|
|
NATIVE_DESKTOP_MACRO_RE = re.compile(r'FC_WEB_DESKTOP_COMMAND\("([^"]+)"\s*,')
|
|
NATIVE_DESKTOP_LIST_RE = re.compile(r'(?:desktopCommands|family\s*==\s*QStringLiteral\("desktop-command"\).*?QStringList\s+\w+)\s*=\s*\{(.*?)\};', re.DOTALL)
|
|
QSTRING_RE = re.compile(r'QStringLiteral\("([^"]+)"\)')
|
|
PROBE_ROOT = ROOT / "freecad-source/src/WebAssembly/FreeCADCoreProbe"
|
|
PRIMARY_WORKERS = {
|
|
"Part": "part.worker.ts",
|
|
"PartDesign": "partdesign.worker.ts",
|
|
"Sketcher": "sketcher.worker.ts",
|
|
"TechDraw": "techdraw-native.worker.ts",
|
|
"Import": "import.worker.ts",
|
|
}
|
|
COMMAND_WORKERS = {
|
|
("Part", "part.viewCommand"): "part.worker.ts",
|
|
("Part", "part.documentViewCommand"): "part-save.worker.ts",
|
|
("Part", "part.document"): "part-save.worker.ts",
|
|
("Web", "web.statefulFamilyBatch"): "stateful-family.worker.ts",
|
|
("Part", "part.saveReopen"): "part-save.worker.ts",
|
|
("TechDraw", "techdraw.nativeSaveReopen"): "techdraw-python.worker.ts",
|
|
("TechDraw", "techdraw.desktopCommand"): "techdraw-python.worker.ts",
|
|
("Web", "web.dockOverlay"): "web-dock-overlay.worker.ts",
|
|
("TechDraw", "techdraw.pythonMethod"): "techdraw-python.worker.ts",
|
|
("Spreadsheet", "spreadsheet.saveReopen"): "spreadsheet-save.worker.ts",
|
|
("PartDesign", "partdesign.pythonApi"): "partdesign-python.worker.ts",
|
|
("PartDesign", "partdesign.pythonFamilyBatch"): "partdesign-python.worker.ts",
|
|
("PartDesign", "partdesign.geometryFamily"): "partdesign.worker.ts",
|
|
("Points", "points.pythonApi"): "points.worker.ts",
|
|
("Plot", "plot.pythonApi"): "plot-python.worker.ts",
|
|
("ReverseEngineering", "reverseengineering.pythonApi"): "reverseengineering-python.worker.ts",
|
|
("ReverseEngineering", "reverseengineering.pythonFamilyBatch"): "reverseengineering-python.worker.ts",
|
|
("Robot", "robot.pythonApi"): "robot-python.worker.ts",
|
|
("Sandbox", "sandbox.pythonApi"): "sandbox-python.worker.ts",
|
|
("Show", "show.pythonApi"): "show-python.worker.ts",
|
|
("Sketcher", "sketcher.pythonApi"): "sketcher-python.worker.ts",
|
|
("Sketcher", "sketcher.pythonFamilyBatch"): "sketcher-python.worker.ts",
|
|
("Sketcher", "sketcher.pythonMethod"): "sketcher-python.worker.ts",
|
|
("Spreadsheet", "spreadsheet.pythonApi"): "spreadsheet-python.worker.ts",
|
|
("Spreadsheet", "spreadsheet.pythonFamilyBatch"): "spreadsheet-python.worker.ts",
|
|
("Start", "start.pythonApi"): "start-python.worker.ts",
|
|
("Surface", "surface.pythonApi"): "surface-python.worker.ts",
|
|
("Surface", "surface.pythonFamilyBatch"): "surface-python.worker.ts",
|
|
("TechDraw", "techdraw.pythonApi"): "techdraw-python.worker.ts",
|
|
("TechDraw", "techdraw.pythonFamilyBatch"): "techdraw-python.worker.ts",
|
|
("TemplatePyMod", "templatepymod.pythonApi"): "templatepymod-python.worker.ts",
|
|
("PartDesign", "partdesign.inspectSourceInventory"): "partdesign-inspect.worker.ts",
|
|
("PartDesign", "partdesign.saveReopen"): "partdesign.worker.ts",
|
|
("Sketcher", "sketcher.inspectSourceInventory"): "sketcher-inspect.worker.ts",
|
|
("Sketcher", "sketcher.saveReopen"): "sketcher-persistence.worker.ts",
|
|
("TechDraw", "techdraw.createRecompute"): "techdraw-python.worker.ts",
|
|
("TechDraw", "techdraw.nativeInventory"): "techdraw-inspect.worker.ts",
|
|
("Points", "points.inspectSource"): "points.worker.ts",
|
|
("Points", "points.saveReopen"): "points-persistence.worker.ts",
|
|
("Surface", "surface.createRecompute"): "surface.worker.ts",
|
|
("Surface", "surface.inspectSourceInventory"): "surface-inspect.worker.ts",
|
|
("Surface", "surface.pythonApi"): "surface-python.worker.ts",
|
|
("Surface", "surface.saveReopen"): "surface-save.worker.ts",
|
|
("Spreadsheet", "spreadsheet.createRecompute"): "spreadsheet.worker.ts",
|
|
("Spreadsheet", "spreadsheet.inspectSourceInventory"): "spreadsheet-inspect.worker.ts",
|
|
("Spreadsheet", "spreadsheet.pythonApi"): "spreadsheet-python.worker.ts",
|
|
("Spreadsheet", "spreadsheet.saveReopen"): "spreadsheet-save.worker.ts",
|
|
("Robot", "robot.createRecompute"): "robot-create.worker.ts",
|
|
("Robot", "robot.inspectSourceInventory"): "robot-inspect.worker.ts",
|
|
("Robot", "robot.saveReopen"): "robot-save.worker.ts",
|
|
("Sandbox", "sandbox.createRecompute"): "sandbox.worker.ts",
|
|
("Sandbox", "sandbox.inspectSourceInventory"): "sandbox-inspect.worker.ts",
|
|
("Sandbox", "sandbox.pythonApi"): "sandbox-python.worker.ts",
|
|
("Sandbox", "sandbox.saveReopen"): "sandbox.worker.ts",
|
|
("ReverseEngineering", "reverseengineering.createRecompute"): "reverseengineering-create.worker.ts",
|
|
("ReverseEngineering", "reverseengineering.inspectSourceInventory"): "reverseengineering-inspect.worker.ts",
|
|
("ReverseEngineering", "reverseengineering.pythonApi"): "reverseengineering-python.worker.ts",
|
|
("ReverseEngineering", "reverseengineering.saveReopen"): "reverseengineering-save.worker.ts",
|
|
("Start", "start.inspectSourceInventory"): "start-inventory.worker.ts",
|
|
("Start", "start.pythonApi"): "start-python.worker.ts",
|
|
("Start", "start.moduleImport"): "start.worker.ts",
|
|
("Start", "start.importSvg"): "start.worker.ts",
|
|
("Show", "show.inspectSourceInventory"): "show-inspect.worker.ts",
|
|
("Show", "show.pythonApi"): "show-python.worker.ts",
|
|
("Assembly", "assembly.saveReopen"): "assembly.worker.ts",
|
|
("Assembly", "assembly.nativeTypeProbe"): "assembly.worker.ts",
|
|
("BIM", "bim.createAxis"): "bim.worker.ts",
|
|
("BIM", "bim.extrusionData"): "bim.worker.ts",
|
|
("CAM", "cam.saveReopen"): "cam.worker.ts",
|
|
("CAM", "cam.pythonFamilyBatch"): "cam.worker.ts",
|
|
("Cloud", "cloud.saveReopen"): "cloud.worker.ts",
|
|
("Cloud", "cloud.pythonFamilyBatch"): "cloud.worker.ts",
|
|
("Draft", "draft.pythonFamilyBatch"): "draft.worker.ts",
|
|
("Fem", "fem.pythonFamilyBatch"): "fem.worker.ts",
|
|
("Import", "import.pythonFamilyBatch"): "import.worker.ts",
|
|
("Inspection", "inspection.pythonFamilyBatch"): "inspection.worker.ts",
|
|
("Import", "import.saveReopen"): "import.worker.ts",
|
|
("Inspection", "inspection.saveReopen"): "inspection.worker.ts",
|
|
("JtReader", "jtreader.saveReopen"): "jtreader.worker.ts",
|
|
("Material", "material.saveReopen"): "material.worker.ts",
|
|
("Material", "material.pythonApi"): "material.worker.ts",
|
|
("Material", "material.pythonFamilyBatch"): "material.worker.ts",
|
|
("Measure", "measure.pythonFamilyBatch"): "measure.worker.ts",
|
|
("Mesh", "mesh.pythonFamilyBatch"): "mesh.worker.ts",
|
|
("MeshPart", "meshpart.pythonFamilyBatch"): "meshpart.worker.ts",
|
|
("Measure", "measure.saveReopen"): "measure.worker.ts",
|
|
("Mesh", "mesh.saveReopen"): "mesh.worker.ts",
|
|
("Part", "part.saveReopen"): "part-save.worker.ts",
|
|
("Points", "points.saveReopen"): "points-persistence.worker.ts",
|
|
("ReverseEngineering", "reverseengineering.saveReopen"): "reverseengineering-save.worker.ts",
|
|
("Robot", "robot.saveReopen"): "robot-save.worker.ts",
|
|
("Sandbox", "sandbox.saveReopen"): "sandbox.worker.ts",
|
|
("Sketcher", "sketcher.saveReopen"): "sketcher-persistence.worker.ts",
|
|
("Surface", "surface.saveReopen"): "surface-save.worker.ts",
|
|
("TechDraw", "techdraw.nativeSaveReopen"): "techdraw-python.worker.ts",
|
|
("Web", "web.saveReopenBoundary"): "web-persistence.worker.ts",
|
|
}
|
|
|
|
|
|
def source_stem(module: str) -> str:
|
|
return re.sub(r"[^a-z0-9]", "", module.lower())
|
|
|
|
|
|
def module_files(module: str) -> list[str]:
|
|
stem = source_stem(module)
|
|
return sorted(
|
|
path.name
|
|
for path in SRC.glob(f"{stem}*.worker.ts")
|
|
if path.name.startswith(f"{stem}.") or path.name.startswith(f"{stem}-")
|
|
if path.name != "core.worker.ts"
|
|
)
|
|
|
|
|
|
def native_desktop_commands(module: str) -> set[str]:
|
|
commands: set[str] = set()
|
|
paths = list(PROBE_ROOT.glob(f"{module}*Probe.cpp"))
|
|
desktop = PROBE_ROOT / f"{module}DesktopCommands.cpp"
|
|
if desktop.is_file():
|
|
paths.append(desktop)
|
|
for path in dict.fromkeys(paths):
|
|
text = path.read_text(encoding="utf-8", errors="replace")
|
|
commands.update(NATIVE_DESKTOP_MACRO_RE.findall(text))
|
|
for block in NATIVE_DESKTOP_LIST_RE.findall(text):
|
|
commands.update(QSTRING_RE.findall(block))
|
|
return commands
|
|
|
|
|
|
def direct_desktop_worker(module: str) -> str | None:
|
|
for worker in module_files(module):
|
|
text = (SRC / worker).read_text(encoding="utf-8", errors="replace")
|
|
if "DesktopRequest" in text and re.search(r"is[A-Za-z0-9]+DesktopRequest", text):
|
|
return worker
|
|
return None
|
|
|
|
|
|
def module_commands(module: str) -> list[str]:
|
|
stem = source_stem(module)
|
|
commands: set[str] = set()
|
|
for path in sorted(SRC.glob(f"{stem}*-facade.ts")):
|
|
if not (path.name.startswith(f"{stem}-") or path.name == f"{stem}-facade.ts"):
|
|
continue
|
|
commands.update(COMMAND_RE.findall(path.read_text(encoding="utf-8")))
|
|
return sorted(commands)
|
|
|
|
|
|
def ts_string(value: str) -> str:
|
|
return json.dumps(value, ensure_ascii=False)
|
|
|
|
|
|
rows = json.loads(MATRIX.read_text(encoding="utf-8"))["rows"]
|
|
entries: list[str] = []
|
|
generated_command_workers = dict(COMMAND_WORKERS)
|
|
for row in rows:
|
|
module = row["module"]
|
|
workers = module_files(module)
|
|
commands = module_commands(module)
|
|
desktop_worker = direct_desktop_worker(module)
|
|
if desktop_worker:
|
|
desktop_commands = native_desktop_commands(module)
|
|
commands = sorted(set(commands) | desktop_commands)
|
|
generated_command_workers.update({(module, command): desktop_worker for command in desktop_commands})
|
|
if module == "Web":
|
|
workers = sorted(set(workers + ["stateful-family.worker.ts"]))
|
|
commands = sorted(set(commands + ["web.statefulFamilyBatch"]))
|
|
primary_worker = PRIMARY_WORKERS.get(module) or (workers[0] if workers else None)
|
|
entries.append(
|
|
" { "
|
|
f"module: {ts_string(module)}, "
|
|
f"workers: {json.dumps(workers, ensure_ascii=False)}, "
|
|
f"primaryWorker: {ts_string(primary_worker) if primary_worker else 'null'}, "
|
|
f"commands: {json.dumps(commands, ensure_ascii=False)}, "
|
|
f"objectTypeCount: {row['object_type_count']}, "
|
|
f"pythonApiCount: {row['python_api_count']}, "
|
|
f"guiDependency: {str(bool(row['gui_dependency'])).lower()}, "
|
|
f"formatExtensions: {json.dumps(row['format_extensions'], ensure_ascii=False)} "
|
|
"},"
|
|
)
|
|
|
|
worker_assets = sorted({worker for row in rows for worker in module_files(row["module"])})
|
|
worker_identifiers = {worker: f"workerEntry{index}" for index, worker in enumerate(worker_assets)}
|
|
worker_imports = "\n".join(f'import {worker_identifiers[worker]} from "./{worker}?worker&url";' for worker in worker_assets)
|
|
|
|
declarations = """export const WEB_MODULE_REGISTRY_SCHEMA = "freecad-web-module-registry/v1" as const;
|
|
|
|
export interface WebModuleRoute {
|
|
readonly module: string;
|
|
readonly workers: readonly string[];
|
|
readonly primaryWorker: string | null;
|
|
readonly commands: readonly string[];
|
|
readonly objectTypeCount: number;
|
|
readonly pythonApiCount: number;
|
|
readonly guiDependency: boolean;
|
|
readonly formatExtensions: readonly string[];
|
|
}
|
|
|
|
export const WEB_MODULE_REGISTRY = [
|
|
""" + "\n".join(entries) + """
|
|
] as const satisfies readonly WebModuleRoute[];
|
|
|
|
const ROUTES: ReadonlyMap<string, WebModuleRoute> = new Map<string, WebModuleRoute>(WEB_MODULE_REGISTRY.map((route) => [route.module, route]));
|
|
|
|
export function getWebModuleRoute(module: string): WebModuleRoute | undefined {
|
|
return ROUTES.get(module);
|
|
}
|
|
|
|
export function listWebModuleCommands(module?: string): string[] {
|
|
if (module) return [...(ROUTES.get(module)?.commands ?? [])];
|
|
return WEB_MODULE_REGISTRY.flatMap((route) => route.commands);
|
|
}
|
|
"""
|
|
|
|
content = """/* Generated by tools/generate_web_module_registry.py. */
|
|
import { artifactForWorker, createDynamicModuleWorker, type WebModuleArtifact } from "./module-loader.js";
|
|
""" + worker_imports + "\n\n" + declarations + """
|
|
export function createWebModuleWorker(module: string, workerFile?: string, artifact?: WebModuleArtifact): Worker | undefined {
|
|
const route = ROUTES.get(module);
|
|
const selectedWorker = workerFile ?? route?.primaryWorker;
|
|
if (!route || !selectedWorker) return undefined;
|
|
const entryUrl = (() => { switch (`${module}:${selectedWorker}`) {
|
|
""" + "\n".join(
|
|
f' case {ts_string(row["module"] + ":" + worker)}: return new URL({worker_identifiers[worker]}, import.meta.url);'
|
|
for row in rows
|
|
for worker in module_files(row["module"])
|
|
) + """
|
|
default: return undefined;
|
|
} })();
|
|
return entryUrl ? createDynamicModuleWorker(entryUrl, artifactForWorker(artifact, selectedWorker)) : undefined;
|
|
}
|
|
|
|
export function getWebModuleWorkerForCommand(module: string, command: string): string | undefined {
|
|
const route = ROUTES.get(module);
|
|
if (!route) return undefined;
|
|
switch (`${module}:${command}`) {
|
|
""" + "\n".join(
|
|
f' case {ts_string(module + ":" + command)}: return {ts_string(worker)};'
|
|
for (module, command), worker in sorted(generated_command_workers.items())
|
|
) + """
|
|
default: return route.primaryWorker ?? undefined;
|
|
}
|
|
}
|
|
"""
|
|
changed = not OUT.is_file() or OUT.read_text(encoding="utf-8") != content
|
|
if changed:
|
|
OUT.write_text(content, encoding="utf-8")
|
|
metadata_path = SRC / "module-metadata.generated.ts"
|
|
metadata_content = "/* Generated by tools/generate_web_module_registry.py; no Worker factories. */\n" + declarations
|
|
if not metadata_path.is_file() or metadata_path.read_text(encoding="utf-8") != metadata_content:
|
|
metadata_path.write_text(metadata_content, encoding="utf-8")
|
|
print(json.dumps({"output": str(OUT.relative_to(ROOT)), "modules": len(entries), "bytes": OUT.stat().st_size, "changed": changed}, ensure_ascii=False))
|