Compilare la UI: un generatore AOT in Node.js per Node.js, Virtual Grid e PostgreSQL

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:

  1. Unica toolchain (package.json): Nessun virtualenv, nessun interprete Python da installare sui server o nei container. Tutto si gestisce con npm install o pnpm.
  2. 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.
  3. 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:

  1. Legge il file YAML della pagina.
  2. Risolve eventuali riferimenti esterni (gridRef).
  3. Renderizza i template EJS generando il backend Fastify e la vista HTML.
  4. 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:

  1. Introspezione (estrazione metadati dal DB):Bashnode scaffold.js ordini_clienti
  2. Definizione o modifica dello schema di pagina:Si crea pages/ordini.yaml referenziando components/grid_ordini_clienti.yaml.
  3. Compilazione AOT continua:Bashnpm 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), tipi ENUM, array nativi di PostgreSQL e commenti di tabella e colonna, senza dover scrivere a mano query complesse su information_schema e pg_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

AspettoApproccio Node.js puroApproccio Compilatore Python
Omogeneità stackMassima (un solo runtime per build e run)Meno lineare (richiede Python a build-time)
Templating sorgentiEJS/Handlebars (gestione spazi/whitespace più grezza)Jinja2 (controllo chirurgico del codice emesso)
Parsing & DSLPiù macchinoso (Nearley/PEG.js)Lark / textX (ideali per progettare linguaggi formali)
Validazione SchemaZod / JoiPydantic (rigore e messaggi d’errore eccellenti)
Introspezione DBQuery manuali su pg_catalog con pgReflection nativa (SQLAlchemy / Psycopg)
Struttura CompilatoreFunzioni JS standard / switch-casePattern 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.