Dialogo con Gemini.
Nell’articolo abbiamo creato il framework utilizzando python come compilatore, potresti creare il framework equivalente ma Node.js puro?
posso ricostruire e reimpostare l’intera architettura da zero qui sotto, eliminando completamente Python e unificando l’intero stack al 100% in Node.js. In questo modo avrai la guida e il codice completi, pronti anche per essere copiati in una nuova sessione.
Perché la scelta “Full Node.js” è architetturalmente vincente
Sostituire Python con Node.js per il compilatore e l’introspezione del database risolve alla radice il problema del doppio runtime:
- Unica toolchain (
package.json): Nessun virtualenv, nessun interprete Python da installare sui server o nei container. Tutto si gestisce connpm installopnpm. - Condivisione di tipi e logica: Funzioni di validazione, formattatori di dati e costanti possono essere condivisi direttamente tra il compilatore, il backend e il frontend.
- Pipeline unificata: Build, watch (
node --watch), introspezione ed esecuzione del server convivono nei normali script npm (npm run introspect,npm run build,npm start).
Struttura del Progetto (Pure Node.js)
Plaintext
framework-node/
│
├── package.json
├── scaffold.js # Introspezione PostgreSQL ed emissione YAML
├── compiler.js # Compilatore AOT (YAML -> Fastify + HTML/JS)
│
├── pages/ # Schemi dichiarativi delle viste
│ └── magazzino.yaml
├── components/ # Componenti generati o referenziati
│ └── grid_movimenti.yaml
│
├── templates/ # Template EJS per la generazione
│ ├── backend_route.ejs
│ ├── frontend_view.ejs
│ └── controller.ejs
│
├── generated/ # Sola lettura: codice generato dal compilatore
│ ├── magazzino.routes.js
│ └── magazzino.html
│
└── controllers/ # Codice utente (Code-Behind, mai sovrascritto)
└── magazzino.controller.js
1. Configurazione e Dipendenze (package.json)
Usiamo librerie leggere e standard:
yaml: parser moderno e veloce per leggere e scrivere gli schemi.ejs: motore di templating simile a Jinja2, nativo per JavaScript.pg: client PostgreSQL ufficiale per connettersi, ispezionare i cataloghi ed eseguire query.fastify: server HTTP ad alte prestazioni per il runtime backend.
JSON
{
"name": "metadata-driven-framework",
"version": "1.0.0",
"type": "module",
"scripts": {
"scaffold": "node scaffold.js",
"build": "node compiler.js",
"watch": "node --watch compiler.js",
"start": "node server.js"
},
"dependencies": {
"fastify": "^4.26.0",
"pg": "^8.11.3"
},
"devDependencies": {
"ejs": "^3.1.9",
"yaml": "^2.4.1"
}
}
2. Introspezione di PostgreSQL in Node.js (scaffold.js)
Questo script interroga information_schema e i commenti di catalogo (pg_catalog.pg_description) di PostgreSQL, estraendo i metadati per costruire automaticamente il file YAML del componente griglia.
JavaScript
import pg from 'pg';
import YAML from 'yaml';
import fs from 'node:fs/promises';
import path from 'node:path';
const { Pool } = pg;
const pool = new Pool({
connectionString: process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/gestionall'
});
/**
* Mappa i tipi PostgreSQL nelle proprietà visive della DataGrid.
*/
function inferColumnConfig(colName, dataType, charLen, comment) {
const label = comment || colName.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
const config = {
field: colName,
label: label,
width: 150,
align: 'left'
};
if (['integer', 'bigint', 'smallint'].includes(dataType)) {
Object.assign(config, { width: 90, align: 'right', type: 'number', format: '0' });
} else if (['numeric', 'decimal', 'real', 'double precision'].includes(dataType)) {
Object.assign(config, { width: 120, align: 'right', type: 'number', format: '0.00' });
} else if (dataType.includes('timestamp') || dataType === 'date') {
Object.assign(config, { width: 160, align: 'center', type: 'date' });
} else if (dataType === 'boolean') {
Object.assign(config, { width: 80, align: 'center', type: 'boolean' });
} else if (charLen && charLen > 100) {
Object.assign(config, { width: 300 });
}
return config;
}
export async function scaffoldTableGrid(tableName, outputFileName) {
const client = await pool.connect();
try {
const query = `
SELECT
c.column_name,
c.data_type,
c.character_maximum_length,
pgd.description AS column_comment
FROM information_schema.columns c
LEFT JOIN pg_catalog.pg_statio_all_tables st
ON c.table_schema = st.schemaname AND c.table_name = st.relname
LEFT JOIN pg_catalog.pg_description pgd
ON pgd.objoid = st.relid AND pgd.objsubid = c.ordinal_position
WHERE c.table_name = $1 AND c.table_schema = 'public'
ORDER BY c.ordinal_position;
`;
const { rows } = await client.query(query, [tableName]);
if (rows.length === 0) {
throw new Error(`Tabella "${tableName}" non trovata o vuota nello schema public.`);
}
const columns = rows.map(r =>
inferColumnConfig(r.column_name, r.data_type, r.character_maximum_length, r.column_comment)
);
const componentDef = {
component: {
type: 'dataGrid',
name: `grid_${tableName}`,
dataSourceTable: tableName,
rowHeight: 36,
columns: columns
}
};
const targetPath = path.join('components', outputFileName || `grid_${tableName}.yaml`);
await fs.mkdir(path.dirname(targetPath), { recursive: true });
await fs.writeFile(targetPath, YAML.stringify(componentDef), 'utf-8');
console.log(`[✔] Componente generato con successo: ${targetPath} (${columns.length} colonne rilevate)`);
} finally {
client.release();
}
}
// Esempio d'uso diretto da CLI: node scaffold.js movimenti_magazzino
const targetTable = process.argv[2] || 'movimenti_magazzino';
scaffoldTableGrid(targetTable).then(() => pool.end());
3. La Specifica della Vista (pages/magazzino.yaml)
Grazie al supporto ai riferimenti (ref), la pagina si limita a orchestrare layout, bottoni, pannelli e il componente generato dal comando di introspezione.
YAML
page:
name: magazzino
title: "Anagrafica Movimenti di Magazzino"
dataSource:
table: "movimenti_magazzino"
primaryKey: "id"
pageSize: 100
components:
toolbar:
buttons:
- id: "btnNuovo"
label: "Nuovo Movimento"
action: "onNuovoClick"
- id: "btnEsporta"
label: "Esporta Dati"
action: "onEsportaClick"
# Riferimento modulare al componente griglia introspezionato dal DB:
gridRef: "components/grid_movimenti_magazzino.yaml"
form:
id: "formDettaglio"
title: "Dettaglio Selezione"
fields:
- field: "codice_articolo"
label: "Codice Articolo"
type: "text"
- field: "quantita"
label: "Quantità Effettiva"
type: "number"
- field: "note"
label: "Note Aggiuntive"
type: "textarea"
4. Il Compilatore AOT in Node.js (compiler.js)
Il compilatore:
- Legge il file YAML della pagina.
- Risolve eventuali riferimenti esterni (
gridRef). - Renderizza i template EJS generando il backend Fastify e la vista HTML.
- Applica il pattern Base & Extension: se il controller Code-Behind (
controllers/<name>.controller.js) non esiste, ne crea uno scaffold; se esiste già, non lo sovrascrive mai.
JavaScript
import fs from 'node:fs/promises';
import path from 'node:path';
import YAML from 'yaml';
import ejs from 'ejs';
const PAGES_DIR = './pages';
const TEMPLATES_DIR = './templates';
const GEN_DIR = './generated';
const CTRL_DIR = './controllers';
async function compilePage(yamlFile) {
const pagePath = path.join(PAGES_DIR, yamlFile);
const rawContent = await fs.readFile(pagePath, 'utf-8');
const spec = YAML.parse(rawContent);
// Risoluzione modulare dei componenti collegati (ref)
if (spec.components.gridRef) {
const gridRefPath = spec.components.gridRef;
const gridRaw = await fs.readFile(gridRefPath, 'utf-8');
const gridData = YAML.parse(gridRaw);
spec.components.grid = gridData.component;
}
const pageName = spec.page.name;
console.log(`[*] Compilazione: ${pageName} (${yamlFile})...`);
// Caricamento template EJS
const tplBackend = await fs.readFile(path.join(TEMPLATES_DIR, 'backend_route.ejs'), 'utf-8');
const tplFrontend = await fs.readFile(path.join(TEMPLATES_DIR, 'frontend_view.ejs'), 'utf-8');
const tplController = await fs.readFile(path.join(TEMPLATES_DIR, 'controller.ejs'), 'utf-8');
// 1. Generazione Backend Route (Fastify)
const backendCode = ejs.render(tplBackend, { spec });
await fs.writeFile(path.join(GEN_DIR, `${pageName}.routes.js`), backendCode, 'utf-8');
// 2. Generazione Frontend HTML / Virtual Grid
const frontendCode = ejs.render(tplFrontend, { spec });
await fs.writeFile(path.join(GEN_DIR, `${pageName}.html`), frontendCode, 'utf-8');
// 3. Generazione Controller Code-Behind (solo al primo passaggio)
const ctrlPath = path.join(CTRL_DIR, `${pageName}.controller.js`);
try {
await fs.access(ctrlPath);
console.log(` [-] Controller esistente preservato: ${ctrlPath}`);
} catch {
const controllerCode = ejs.render(tplController, { spec });
await fs.writeFile(ctrlPath, controllerCode, 'utf-8');
console.log(` [+] Generato nuovo controller utente: ${ctrlPath}`);
}
}
async function run() {
await fs.mkdir(GEN_DIR, { recursive: true });
await fs.mkdir(CTRL_DIR, { recursive: true });
const files = await fs.readdir(PAGES_DIR);
for (const file of files.filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))) {
await compilePage(file);
}
console.log('[✔] Build completata.');
}
run().catch(console.error);
5. I Template del Framework (EJS)
5.1 Template Backend: templates/backend_route.ejs
Implementa la paginazione basata su chiavi (Keyset Pagination $O(\log N)$ con indice B-Tree) e stima istantanea delle righe via catalogo (pg_class.reltuples), prevenendo qualsiasi blocco o latenza tipica di OFFSET e COUNT(*).
JavaScript
/**
* AUTO-GENERATO DA NODE COMPILER. NON MODIFICARE DIRETTAMENTE.
* Vista: <%= spec.page.name %>
*/
export default async function registerRoutes(fastify, dbPool) {
const TABLE_NAME = '<%= spec.dataSource.table %>';
const PRIMARY_KEY = '<%= spec.dataSource.primaryKey %>';
const DEFAULT_PAGE_SIZE = <%= spec.dataSource.pageSize || 100 %>;
// Whitelist colonne ammesse per evitare SQL Injection
const ALLOWED_COLUMNS = [
<% spec.components.grid.columns.forEach(col => { %>
'<%= col.field %>',
<% }) %>
];
// 1. Endpoint Stream Dati con Keyset Pagination
fastify.get('/api/<%= spec.page.name %>/data', async (request, reply) => {
const cursor = request.query.cursor ? Number(request.query.cursor) : null;
const limit = Math.min(Number(request.query.limit) || DEFAULT_PAGE_SIZE, 500);
const values = [];
let whereClause = '';
if (cursor !== null && !isNaN(cursor)) {
values.push(cursor);
whereClause = `WHERE ${PRIMARY_KEY} > $${values.length}`;
}
values.push(limit);
const sql = `
SELECT ${ALLOWED_COLUMNS.join(', ')}
FROM ${TABLE_NAME}
${whereClause}
ORDER BY ${PRIMARY_KEY} ASC
LIMIT $${values.length};
`;
const client = await dbPool.connect();
try {
const { rows } = await client.query(sql, values);
// Stima rapida totale righe in O(1)
const countRes = await client.query(
'SELECT reltuples::bigint AS estimate FROM pg_class WHERE relname = $1;',
[TABLE_NAME]
);
const totalEstimate = Number(countRes.rows[0]?.estimate || rows.length);
const nextCursor = rows.length === limit ? rows[rows.length - 1][PRIMARY_KEY] : null;
return {
data: rows,
nextCursor,
totalEstimate
};
} finally {
client.release();
}
});
// 2. Endpoint Record Singolo (Binding Form)
fastify.get('/api/<%= spec.page.name %>/:id', async (request, reply) => {
const { id } = request.params;
const client = await dbPool.connect();
try {
const { rows } = await client.query(
`SELECT * FROM ${TABLE_NAME} WHERE ${PRIMARY_KEY} = $1 LIMIT 1;`,
[id]
); if (rows.length === 0) return reply.status(404).send({ error: ‘Record non trovato’ }); return rows[0]; } finally { client.release(); } }); }
5.2 Template Frontend: templates/frontend_view.ejs
Virtual DataGrid autonoma: calcola le posizioni matematiche e crea solo le righe visibili nel viewport, spostando il container via accelerazione hardware GPU (transform: translateY).
HTML
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<title><%= spec.page.title %></title>
<style>
:root {
--border-color: #d1d5db;
--header-bg: #f3f4f6;
--row-hover: #f9fafb;
--row-selected: #e0f2fe;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
margin: 0; padding: 16px; display: flex; flex-direction: column;
height: 100vh; box-sizing: border-box; background: #f8fafc;
}
.toolbar {
padding: 8px 12px; background: white; border: 1px solid var(--border-color);
border-radius: 6px; margin-bottom: 12px; display: flex; gap: 8px;
}
button {
padding: 6px 14px; background: #0284c7; color: white; border: none;
border-radius: 4px; cursor: pointer; font-size: 13px; font-weight: 500;
}
button:hover { background: #0369a1; }
.workspace { display: flex; flex: 1; gap: 16px; min-height: 0; }
/* Griglia Virtuale */
.grid-container {
flex: 3; display: flex; flex-direction: column; background: white;
border: 1px solid var(--border-color); border-radius: 6px; overflow: hidden;
}
.grid-header {
display: flex; background: var(--header-bg); border-bottom: 1px solid var(--border-color);
font-size: 12px; font-weight: 600; color: #374151;
}
.grid-header-cell { padding: 10px 8px; box-sizing: border-box; }
.grid-viewport { flex: 1; overflow-y: auto; position: relative; will-change: transform; }
.grid-phantom { position: absolute; left: 0; top: 0; width: 1px; }
.grid-content { position: absolute; left: 0; top: 0; right: 0; }
.grid-row {
display: flex; align-items: center; border-bottom: 1px solid #f3f4f6;
box-sizing: border-box; cursor: pointer; font-size: 13px;
}
.grid-row:hover { background: var(--row-hover); }
.grid-row.selected { background: var(--row-selected); font-weight: 500; }
.grid-cell { padding: 0 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Form Laterale */
.form-panel {
flex: 1; background: white; border: 1px solid var(--border-color);
border-radius: 6px; padding: 16px; display: flex; flex-direction: column; gap: 12px;
}
.form-group { display: flex; flex-direction: column; gap: 4px; }
.form-group label { font-size: 12px; font-weight: 600; color: #4b5563; }
.form-group input, .form-group textarea {
padding: 6px 8px; border: 1px solid var(--border-color); border-radius: 4px; font-size: 13px;
}
</style>
</head>
<body>
<div class="toolbar">
<% spec.components.toolbar.buttons.forEach(btn => { %>
<button id="<%= btn.id %>"><%= btn.label %></button>
<% }) %>
</div>
<div class="workspace">
<div class="grid-container">
<div class="grid-header">
<% spec.components.grid.columns.forEach(col => { %>
<div class="grid-header-cell" style="width: <%= col.width %>px; text-align: <%= col.align || 'left' %>;">
<%= col.label %>
</div>
<% }) %>
</div>
<div class="grid-viewport" id="viewport">
<div class="grid-phantom" id="phantom"></div>
<div class="grid-content" id="content"></div>
</div>
</div>
<% if (spec.components.form) { %>
<div class="form-panel">
<h3><%= spec.components.form.title %></h3>
<% spec.components.form.fields.forEach(fld => { %>
<div class="form-group">
<label><%= fld.label %></label>
<% if (fld.type === 'textarea') { %>
<textarea id="form_<%= fld.field %>" rows="3"></textarea>
<% } else { %>
<input id="form_<%= fld.field %>" type="<%= fld.type || 'text' %>">
<% } %>
</div>
<% }) %>
</div>
<% } %>
</div>
<script type="module">
import Controller from '../controllers/<%= spec.page.name %>.controller.js';
class VirtualGrid {
constructor(config) {
this.viewport = document.getElementById('viewport');
this.phantom = document.getElementById('phantom');
this.content = document.getElementById('content');
this.rowHeight = config.rowHeight || 36;
this.columns = config.columns;
this.records = [];
this.nextCursor = null;
this.isLoading = false;
this.selectedRow = null;
this.onRowSelect = config.onRowSelect;
this.viewport.addEventListener('scroll', () => this.render());
this.loadMore();
}
async loadMore() {
if (this.isLoading) return;
this.isLoading = true;
const url = `/api/<%= spec.page.name %>/data?cursor=${this.nextCursor || ''}&limit=100`;
const res = await fetch(url).then(r => r.json());
this.records.push(...res.data);
this.nextCursor = res.nextCursor;
this.totalEstimate = res.totalEstimate;
this.phantom.style.height = `${Math.max(this.records.length, this.totalEstimate) * this.rowHeight}px`;
this.isLoading = false;
this.render();
}
render() {
const scrollTop = this.viewport.scrollTop;
const viewportHeight = this.viewport.clientHeight;
const startIndex = Math.max(0, Math.floor(scrollTop / this.rowHeight) - 5);
const endIndex = Math.min(this.records.length, Math.ceil((scrollTop + viewportHeight) / this.rowHeight) + 5);
if (endIndex >= this.records.length - 20 && this.nextCursor) {
this.loadMore();
}
this.content.style.transform = `translateY(${startIndex * this.rowHeight}px)`;
let html = '';
for (let i = startIndex; i < endIndex; i++) {
const row = this.records[i];
const isSelected = this.selectedRow === row ? 'selected' : '';
html += `<div class="grid-row ${isSelected}" data-index="${i}" style="height: ${this.rowHeight}px;">`;
for (const col of this.columns) {
html += `<div class="grid-cell" style="width: ${col.width}px; text-align: ${col.align || 'left'};">
${row[col.field] ?? ''}
</div>`;
}
html += `</div>`;
}
this.content.innerHTML = html;
this.content.querySelectorAll('.grid-row').forEach(el => {
el.addEventListener('click', () => {
this.selectedRow = this.records[el.dataset.index];
this.render();
if (this.onRowSelect) this.onRowSelect(this.selectedRow);
});
});
}
}
const controller = new Controller();
const grid = new VirtualGrid({
rowHeight: <%= spec.components.grid.rowHeight || 36 %>,
columns: <%- JSON.stringify(spec.components.grid.columns) %>,
onRowSelect: (row) => {
<% if (spec.components.form) { %>
<% spec.components.form.fields.forEach(fld => { %>
const el_<%= fld.field %> = document.getElementById('form_<%= fld.field %>');
if (el_<%= fld.field %>) el_<%= fld.field %>.value = row['<%= fld.field %>'] ?? '';
<% }) %>
<% } %>
controller.onRowSelect(row);
}
});
<% spec.components.toolbar.buttons.forEach(btn => { %>
document.getElementById('<%= btn.id %>')?.addEventListener('click', () => {
controller.<%= btn.action %>(grid);
});
<% }) %>
</script>
</body>
</html>
5.3 Template Code-Behind: templates/controller.ejs
JavaScript
/**
* Controller Utente (Code-Behind): <%= spec.page.name %>
* Questo file NON viene sovrascritto dal compilatore se già presente su disco.
*/
export default class <%= spec.page.name.charAt(0).toUpperCase() + spec.page.name.slice(1) %>Controller {
onRowSelect(record) {
console.log('[Code-Behind] Record selezionato:', record);
}
<% spec.components.toolbar.buttons.forEach(btn => { %>
<%= btn.action %>(gridInstance) {
console.log('[Code-Behind] Azione pulsante: <%= btn.action %>');
}
<% }) %>
}
6. Flusso Operativo Finale
Con questo impianto, lo sviluppo di una nuova entità gestionale complessa richiede solo tre passaggi:
- Introspezione (estrazione metadati dal DB):Bash
node scaffold.js ordini_clienti - Definizione o modifica dello schema di pagina:Si crea
pages/ordini.yamlreferenziandocomponents/grid_ordini_clienti.yaml. - Compilazione AOT continua:Bash
npm run build # oppure in ascolto automatico di modifiche: npm run watch
Il risultato è un’architettura completamente omogenea in Node.js, con zero runtime sovraccarico sul client, footprint di memoria minimo e velocità operativa pari a quella dei migliori ambienti RAD tradizionali.
Ottimo, grazie. Potresti elencare i vantaggi di usare Python?
Anche se la soluzione Full Node.js ha il pregio innegabile dell’omogeneità dello stack (un solo runtime, un solo package.json), l’uso di Python per il livello di compilazione, introspezione e generazione del codice (AOT) presenta vantaggi ingegneristici e concettuali notevoli.
La scrittura di un compilatore source-to-source (o generatore di codice) è un problema di manipolazione simbolica, analisi sintattica e trasformazione di alberi (AST). In questo dominio specifico, Python offre strumenti e caratteristiche di linguaggio storicamente superiori all’ecosistema JavaScript:
1. Jinja2: controllo millimetrico sul codice generato
Nel templating finalizzato alla generazione di sorgenti (code generation), i dettagli contano: spazi, indentazioni, righe vuote superflue e leggibilità del codice emesso.
- Whitespace Control chirurgico: Con Jinja2 (
{%- ... -%}) è possibile eliminare selettivamente newline e spazi di template senza intaccare l’indentazione del codice sorgente target (Fastify o JavaScript). Motori JS come EJS o Handlebars tendono a produrre codice generato sporco o richiedono tool di post-processing (come Prettier) per risultare leggibili. - Macro e Template Inheritance: Jinja2 supporta la composizione modulare avanzata (
{% extends %},{% block %},{% macro %}), permettendo di definire una gerarchia di componenti generati riutilizzabili esattamente come funzioni con parametri e valori di default.
2. Validazione rigorosa e fail-fast con Pydantic
Se un file di testo/YAML contiene un errore semantico (es. una proprietà sconosciuta, un tipo colonna incoerente, un allineamento invalido), l’errore deve emergere istantaneamente in fase di build:
- Con Pydantic in Python, lo schema della UI non è un dizionario generico, ma un modello a oggetti tipizzato e autodocumentante con validatori custom e messaggi di errore chiari e puntuali.
- Sebbene in TypeScript esista Zod, in un ambiente Node.js non compilato (plain JS) la validazione richiede librerie esterne e configurazioni più verbose; in Python la validazione dei dati è nativa, dichiarativa e integrata nel sistema di tipi.
3. Evoluzione naturale verso un DSL proprietario (Lark, textX)
Oggi la specifica è in YAML, ma se il framework dovesse evolvere verso un DSL proprietario (una sintassi dedicata più espressiva e concisa, sul modello dei file .dfm di Delphi, QML di Qt o XAML di Microsoft), Python è l’ambiente di riferimento:
- Librerie come Lark o textX consentono di definire una grammatica formale EBNF in poche righe, gestendo automaticamente parser LALR/Earley, costruzione dell’albero sintattico (AST) e gestione degli errori di sintassi.
- In JavaScript, l’equivalente (come ANTLR4 o PEG.js/Nearley) è spesso più farraginoso da configurare, meno manutenibile e con documentazione meno orientata alla progettazione di compilatori.
4. Introspezione avanzata del Database (SQLAlchemy & Psycopg)
L’introspezione di PostgreSQL non si limita a leggere i nomi delle colonne, ma riguarda la comprensione semantica del modello relazionale:
- Reflection nativa: Con motori come SQLAlchemy o il layer di introspezione di Psycopg, è possibile “riflettere” (
autoload_with) intere tabelle leggendo automaticamente chiavi primarie composite, vincoli di foreign key (utilissimi per dedurre al volo se un campo deve essere una lookup a tendina anziché un input di testo), tipiENUM, array nativi di PostgreSQL e commenti di tabella e colonna, senza dover scrivere a mano query complesse suinformation_schemaepg_catalog. - La gestione dei tipi ricchi di Postgres (JSONB, intervalli, UUID, timestamp con time zone) è gestita con conversioni a tipi nativi Python senza perdite di precisione.
5. Pattern Matching e Visitor Pattern (Python 3.10+)
Un generatore di codice deve percorrere l’albero dei componenti (Grid, Toolbar, FormPanel, Button) e trasformarlo in costrutti target.
- Il costrutto
match / case(structural pattern matching) introdotto in Python 3.10 rende l’ispezione dei nodi dell’albero estremamente elegante:Pythonmatch component: case {"type": "grid", "dataSource": ds, "columns": cols}: return render_grid(ds, cols) case {"type": "form", "fields": fields}: return render_form(fields) case _: raise ValueError(f"Componente non riconosciuto: {component}") - L’implementazione del pattern Visitor per attraversare e trasformare l’AST risulta pulita, concisa e priva del boilerplate tipico di JavaScript.
6. Netta separazione concettuale: Build-Time vs. Run-Time
C’è un beneficio metodologico sottile ma rilevante:
- Quando lo strumento di build è scritto nello stesso linguaggio del runtime (JS/Node), il rischio comune è quello di iniziare a mescolare le responsabilità: importare moduli applicativi nel compilatore, riutilizzare codice a runtime con side-effect non voluti o tentare interpretazioni dinamiche (
eval,Function). - Avere il compilatore in Python e il target in Node.js/HTML forza una separazione architetturale netta: il compilatore è un processo a sé stante, deterministico e matematico (funzione pura che prende testo/metadati e produce codice sorgente); non può “sporcarsi” con il ciclo di vita dell’applicazione server né condividere lo stato di memoria con essa.
Sintesi
| Aspetto | Approccio Node.js puro | Approccio Compilatore Python |
|---|---|---|
| Omogeneità stack | Massima (un solo runtime per build e run) | Meno lineare (richiede Python a build-time) |
| Templating sorgenti | EJS/Handlebars (gestione spazi/whitespace più grezza) | Jinja2 (controllo chirurgico del codice emesso) |
| Parsing & DSL | Più macchinoso (Nearley/PEG.js) | Lark / textX (ideali per progettare linguaggi formali) |
| Validazione Schema | Zod / Joi | Pydantic (rigore e messaggi d’errore eccellenti) |
| Introspezione DB | Query manuali su pg_catalog con pg | Reflection nativa (SQLAlchemy / Psycopg) |
| Struttura Compilatore | Funzioni JS standard / switch-case | Pattern matching strutturale e Visitor pattern |
In breve: se l’obiettivo prioritario è la semplicità di distribuzione e l’assenza di dipendenze extra, Node.js vince; se l’obiettivo è la robustezza dello strumento linguistico, la precisione del codice generato e l’estensibilità verso un DSL avanzato, Python offre le fondamenta concettuali e pratiche più solide per un compilatore.