#!/usr/bin/env node
// One table. Every list table in the panel renders through ds/DataTable and
// looks exactly like `.sk-dtable` inside `.sk-dtable-wrap`
// (styles/components/_design-system.scss). Fails when a page:
//
// - restyles table elements (th/td/tr/thead/tbody, nth-child widths) in its
// own SCSS instead of sizing columns through the column `width` prop,
// - renders a raw
instead of DataTable,
// - passes a `tableClassName` re-skin to DataTable.
//
// The Databases studio is the one exemption (its grids are an editor, not a
// list). Counts may only go down; lower a ceiling when a migration lands.
// Extension copies under src/plugins/ are out of scope (their source repos
// adopt the SDK DataTable instead).
import { readFileSync, readdirSync, statSync } from 'node:fs';
import { join, relative, sep } from 'node:path';
const SRC = new URL('../src', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1');
// Files that own table markup or styling.
const OWNERS = new Set([
'components/ui/table.jsx',
'components/ds/DataTable.jsx',
'components/ds/DataTableFooter.jsx',
'styles/components/_design-system.scss',
'styles/components/_ui.scss',
'styles/components/_datagrid.scss',
'styles/components/_tables.scss',
// Living documentation of the primitives.
'pages/StyleGuide.jsx',
// Dashboard table widget: a widget can be three rows tall, so it keeps
// compact rows + a sticky head; its styling otherwise follows .sk-dtable.
'components/dashboard/widgets/renderers.jsx',
'styles/components/_dashboard-widgets.scss',
]);
// The Databases studio's own stylesheets (its tuner + insights grids).
const EXEMPT = /^(components\/databases\/|styles\/pages\/_(databases|db-tuner)\.scss$|plugins\/)/;
// Remaining legacy sites: the DNS drawer's fixed-layout table (`.ddp__table`
// in _domains.scss + its tableClassName) and the now-unused `.sk-dlog__table`
// rules in _notification-center.scss.
const CEILINGS = {
scss: 1,
rawTable: 0,
tableClassName: 1,
};
function* walk(dir) {
for (const name of readdirSync(dir)) {
const p = join(dir, name);
if (statSync(p).isDirectory()) {
if (name === 'node_modules' || name === '__tests__') continue;
yield* walk(p);
} else if (/\.(jsx?|scss)$/.test(name)) {
yield p;
}
}
}
function stripComments(src) {
return src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1');
}
// A selector line (ends in `{` or `,`, or opens a one-line rule
// `sel { … }`) that targets a table element.
const TABLE_SELECTOR = /(^|[\s>+~,(&])(th|td|tr|thead|tbody|table)(?![\w-])[^{;]*(\{|,\s*$)/;
const found = { scss: [], rawTable: [], tableClassName: [] };
for (const file of walk(SRC)) {
const rel = relative(SRC, file).split(sep).join('/');
if (OWNERS.has(rel) || EXEMPT.test(rel)) continue;
const lines = stripComments(readFileSync(file, 'utf8')).split('\n');
lines.forEach((line, i) => {
const where = `${rel}:${i + 1}`;
if (rel.endsWith('.scss')) {
if (TABLE_SELECTOR.test(line)) found.scss.push(`${where} ${line.trim()}`);
} else {
if (/ (use DataTable)',
tableClassName: 'tableClassName re-skins (DataTable already is the look)',
};
const report = process.argv.includes('--report');
let failed = false;
for (const key of Object.keys(CEILINGS)) {
const n = found[key].length;
if (report || n > CEILINGS[key]) {
console.log(`${n > CEILINGS[key] ? '✗' : '·'} ${LABEL[key]}: ${n} (ceiling ${CEILINGS[key]})`);
for (const f of found[key]) console.log(` ${f}`);
}
if (n > CEILINGS[key]) failed = true;
}
if (failed) {
console.error('\nOne table: render lists through DataTable and leave its look to .sk-dtable.');
process.exit(1);
}
const total = Object.values(found).reduce((a, b) => a + b.length, 0);
console.log(`✓ tables: one look — ${total} legacy site(s) remain ratcheted (scss ${found.scss.length}/${CEILINGS.scss}, raw ${found.rawTable.length}/${CEILINGS.rawTable}, tableClassName ${found.tableClassName.length}/${CEILINGS.tableClassName}).`);