fix(assets): serve furniture icons from the gamedata tree, add stack-height SQL generator
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 30s
CI / tests-integration (push) Successful in 1m52s
CI / tests-unit (push) Successful in 2m3s
CI / tests-ui (push) Successful in 2m40s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 3m47s
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 30s
CI / tests-integration (push) Successful in 1m52s
CI / tests-unit (push) Successful in 2m3s
CI / tests-ui (push) Successful in 2m40s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 3m47s
Commit the two changes that were live in the working tree but never recorded. The nginx change adds a location for /swf/dcr/hof_furni/icons/ that reads /var/www/Gamedata/icons/ off disk and falls back to the CMS bundle for anything missing. The CMS built its icon URLs from that path, but the icons actually live in the gamedata tree: of the 16,263 classnames in items_base, 15,338 are present there under the safe name, against 11,267 under public/swf/dcr/hof_furni/icons. The swf tree also keeps colour variants behind a literal "*" in the filename, so every request using the safe name missed, and hof.furni.url already points the Nitro client at the gamedata tree. A miss is a no-store 404, never a cached one — same reasoning as @gamedata_missing, since add_header without "always" says nothing about a 404 and Cloudflare would otherwise apply the zone TTL. scripts/generate-stack-height-sql.ts emits a SQL file that repairs items_base dimensions and interaction columns from the logic inside each bundle, which is where they actually live rather than in furnidata. It writes a file and touches no database, so the result is reviewed before it is run.
This commit is contained in:
1 parent
43742e8d99
commit
2978483ea9
2 files changed
+489
No files matched your search
@@ -78,6 +78,23 @@ map "$cms_cc_class|$cms_skip_cache" $cms_public_cc {
|
||||
"~^3\|0" "public, max-age=300, s-maxage=300, stale-while-revalidate=600";
|
||||
}
|
||||
|
||||
# Bestandsnaam van een furni-icon, opgehaald uit de URL. De locatie voor
|
||||
# /swf/dcr/hof_furni/icons/ heeft die nodig om hetzelfde bestand bij de
|
||||
# gamedata-boom op te kunnen vragen. Leeg laten voor elke andere URL, zodat
|
||||
# niets anders deze waarde per ongeluk gebruikt.
|
||||
map $uri $furni_icon_file {
|
||||
~^/swf/dcr/hof_furni/icons/(?<icon_file>.+)$ $icon_file;
|
||||
default "";
|
||||
}
|
||||
|
||||
# Cache-Control per status voor het CMS-valrimpeltje. Een 404 mag nooit
|
||||
# gecacht worden (zie @gamedata_missing hieronder), dus die krijgt `no-store`
|
||||
# in plaats van de icon-TTL.
|
||||
map $upstream_status $furni_icon_cc {
|
||||
200 "public, max-age=604800, stale-while-revalidate=2592000";
|
||||
default "no-store";
|
||||
}
|
||||
|
||||
# ─── Mime fix ───
|
||||
types {
|
||||
application/json jsonc;
|
||||
@@ -376,6 +393,92 @@ server {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# ─── Furni-icons: gamedata-boom eerst, CMS-boom als valrimpeltje ───
|
||||
#
|
||||
# De CMS bouwde zijn icon-URL's altijd vanaf /swf/dcr/hof_furni/icons/,
|
||||
# maar de catalogus komt uit /var/www/Gamedata/icons:
|
||||
# - van de 16.263 classnames in items_base staat er 15.338 hier onder de
|
||||
# "veilige" naam (`highscore_perteam_1_icon.png`), tegen 11.267 in
|
||||
# public/swf/dcr/hof_furni/icons;
|
||||
# - de swf-boom houdt kleurvarianten bovendien vast met een letterlijke
|
||||
# `*` in de bestandsnaam (`highscore_perteam*1_icon.png`), dus elke
|
||||
# aanvraag met de veilige naam mist;
|
||||
# - en `hof.furni.url` wijst de Nitro-client al naar deze boom.
|
||||
# Resultaat was een 404 voor `highscore_perteam_1_icon.png` en duizenden
|
||||
# andere, terwijl `/gamedata/icons/` ze wél heeft.
|
||||
#
|
||||
# Deze locatie leest de gamedata-boom rechtstreeks van schijf — nginx heeft
|
||||
# er leesrechten op en het is de boom die de client ook gebruikt. Wat daar
|
||||
# niet staat wordt doorgestuurd naar de CMS, die uit public/ serveert,
|
||||
# zodat niets wat nu al laadde stopt met laden.
|
||||
#
|
||||
# Een echte miss is een no-store 404, nooit een gecachte: `add_header`
|
||||
# zonder `always` zegt niets over een 404, en zonder de expliciete handler
|
||||
# hieronder neemt Cloudflare de zone-TTL (1 jaar) over. Zelfde les als bij
|
||||
# @gamedata_missing hierboven.
|
||||
location ^~ /swf/dcr/hof_furni/icons/ {
|
||||
alias /var/www/Gamedata/icons/;
|
||||
error_page 404 = @furni_icon_from_cms;
|
||||
# 403 = het pad valt op een map, dus `alias` eindigt op een directory.
|
||||
# Dat is geen icon, dus dezelfde route als een miss.
|
||||
error_page 403 = @furni_icon_missing;
|
||||
|
||||
# Geen limit_req: zelfde reden als /gamedata/icons/. Een kamerladen
|
||||
# vuurt honderden icons in één burst af.
|
||||
add_header Cache-Control "public, max-age=604800, stale-while-revalidate=2592000";
|
||||
add_header Cache-Tag "cms-furni-icons";
|
||||
access_log off;
|
||||
|
||||
add_header Access-Control-Allow-Origin $http_origin always;
|
||||
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
|
||||
if ($cors_headers) {
|
||||
add_header Access-Control-Max-Age 1728000;
|
||||
add_header Content-Type "text/plain; charset=utf-8";
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
|
||||
# Niet in de gamedata-boom: val terug op public/swf/dcr/hof_furni/icons/,
|
||||
# waar de CMS naartoe importeert. Het pad wordt hier opnieuw opgebouwd
|
||||
# omdat een named location geen prefix van `$uri` afstropt.
|
||||
#
|
||||
# De Cache-Control komt uit `$furni_icon_cc` (status-afhankelijk) in plaats
|
||||
# van uit een tweede `error_page`: nginx negeert `error_page` die binnen
|
||||
# een named location staat die zelf via `error_page` bereikt is, dus een
|
||||
# geketende 404-handler bestaat hier niet.
|
||||
location @furni_icon_from_cms {
|
||||
internal;
|
||||
proxy_pass http://cms_app/swf/dcr/hof_furni/icons/$furni_icon_file;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Connection "";
|
||||
proxy_intercept_errors on;
|
||||
|
||||
# `=404` (geen URI) vervangt de body door nginx' eigen foutpagina. Next
|
||||
# stuurt voor een miss een volledige HTML-pagina van ~300 KB mee, en een
|
||||
# gebroken icon hoeft geen 300 KB aan markup op te halen. Dit is geen
|
||||
# interne redirect, dus de `add_header` hieronder blijven gelden.
|
||||
error_page 404 =404;
|
||||
|
||||
# `proxy_hide_header` haalt de Cache-Control van de CMS weg, anders
|
||||
# staan er twee in één antwoord (Next stuurt voor /swf/** een
|
||||
# `public, max-age=604800`, `location /` elders nog een
|
||||
# `private, no-cache`). Eén header per antwoord.
|
||||
proxy_hide_header Cache-Control;
|
||||
add_header Cache-Control $furni_icon_cc always;
|
||||
add_header Cache-Tag "cms-furni-icons";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location @furni_icon_missing {
|
||||
add_header Cache-Control "no-store" always;
|
||||
add_header Cache-Tag "cms-furni-icons" always;
|
||||
add_header Access-Control-Allow-Origin $http_origin always;
|
||||
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
|
||||
access_log off;
|
||||
return 404;
|
||||
}
|
||||
|
||||
location /camera/ {
|
||||
alias /var/www/Camera/;
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
import "./load-env";
|
||||
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { db } from "@/lib/db";
|
||||
import {
|
||||
autoDetectInteraction,
|
||||
nitroAnimationStatesCount,
|
||||
} from "@/lib/furni/auto-interaction";
|
||||
import { parseNitroBundle } from "@/lib/services/swf/nitro-builder";
|
||||
|
||||
/**
|
||||
* Generate a plain SQL file that repairs every furniture row in `items_base`
|
||||
* from the visual logic inside each `.hab` / `.nitro` bundle:
|
||||
*
|
||||
* width / length / stack_height ← logic.model.dimensions x/y/z
|
||||
* allow_stack ← z > 0
|
||||
* allow_sit / allow_lay / allow_walk ← furnidata flags (cansiton/canlayon/canstandon)
|
||||
* interaction_modes_count ← real nitro animation states / mechanic
|
||||
* interaction_type ← logicType + classname mechanic (only fills 'default')
|
||||
*
|
||||
* The dimensions are NOT in furnidata — they live in the bundle JSON at
|
||||
* `logic.model.dimensions` (or top-level `dimensions` for official bundles).
|
||||
* The interaction columns mirror `verifyAndFixInteractionModesCount`
|
||||
* (src/lib/services/furni-import.ts:1341): real furnidata flags + .nitro states,
|
||||
* and `interaction_type` is only touched when it is still `default`.
|
||||
*
|
||||
* Usage:
|
||||
* pnpm exec tsx scripts/generate-stack-height-sql.ts [options]
|
||||
*
|
||||
* Options:
|
||||
* --dir=<path> bundle directory (default: /var/www/Gamedata/bundled/furniture)
|
||||
* --furnidata=<path> local FurnitureData.json (default: /var/www/Gamedata/config/FurnitureData.json)
|
||||
* --out=<path> SQL output file (default: stack-heights.sql)
|
||||
* --types=<list> items_base types (default: s,i → floor + wall furniture)
|
||||
* --all emit every matched row instead of only changed rows
|
||||
* --no-interaction dimensions/allow_stack only, skip interaction columns
|
||||
*
|
||||
* Nothing in the database is modified here; you run the generated file yourself.
|
||||
*/
|
||||
|
||||
const args = new Map<string, string | true>();
|
||||
for (const raw of process.argv.slice(2)) {
|
||||
const eq = raw.indexOf("=");
|
||||
if (eq === -1) args.set(raw.replace(/^--/, ""), true);
|
||||
else args.set(raw.slice(2, eq), raw.slice(eq + 1));
|
||||
}
|
||||
|
||||
const DIR = String(args.get("dir") ?? "/var/www/Gamedata/bundled/furniture");
|
||||
const FURNIDATA = String(
|
||||
args.get("furnidata") ?? "/var/www/Gamedata/config/FurnitureData.json",
|
||||
);
|
||||
const OUT = String(args.get("out") ?? "stack-heights.sql");
|
||||
const TYPES = String(args.get("types") ?? "s,i")
|
||||
.split(",")
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean);
|
||||
const EMIT_ALL = args.has("all");
|
||||
const WITH_INTERACTION = !args.has("no-interaction");
|
||||
|
||||
const EXTENSIONS = [".hab", ".nitro"] as const;
|
||||
|
||||
interface Dims {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
}
|
||||
|
||||
interface Flags {
|
||||
cansiton: boolean;
|
||||
canlayon: boolean;
|
||||
canstandon: boolean;
|
||||
}
|
||||
|
||||
interface BundleInfo {
|
||||
dims: Dims | null;
|
||||
animationStates: number | undefined;
|
||||
logicType: string | undefined;
|
||||
}
|
||||
|
||||
interface Row {
|
||||
item_name: string;
|
||||
public_name: string;
|
||||
width: number;
|
||||
length: number;
|
||||
stack_height: number;
|
||||
allow_stack: number | string;
|
||||
allow_sit: number | string;
|
||||
allow_lay: number | string;
|
||||
allow_walk: number | string;
|
||||
interaction_type: string;
|
||||
interaction_modes_count: number;
|
||||
}
|
||||
|
||||
/** Escape a value for a single-quoted MySQL string literal. */
|
||||
function q(value: string): string {
|
||||
return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "''")}'`;
|
||||
}
|
||||
|
||||
/** `enum('0','1')` values must be quoted — an unquoted 0 is read as index 0 (''). */
|
||||
function qbit(value: boolean): string {
|
||||
return value ? "'1'" : "'0'";
|
||||
}
|
||||
|
||||
function isTruthyBit(value: number | string): boolean {
|
||||
return Number(value) === 1;
|
||||
}
|
||||
|
||||
/** Pull dimensions out of a parsed bundle JSON, tolerating both layouts. */
|
||||
function extractDims(json: unknown): Dims | null {
|
||||
const root = json as Record<string, unknown> | null;
|
||||
if (!root) return null;
|
||||
const logic = root.logic as Record<string, unknown> | undefined;
|
||||
const model = logic?.model as Record<string, unknown> | undefined;
|
||||
const candidate =
|
||||
(model?.dimensions as Dims | undefined) ??
|
||||
(logic?.dimensions as Dims | undefined) ??
|
||||
(root.dimensions as Dims | undefined);
|
||||
if (!candidate) return null;
|
||||
const x = Number(candidate.x);
|
||||
const y = Number(candidate.y);
|
||||
const z = Number(candidate.z);
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) {
|
||||
return null;
|
||||
}
|
||||
return { x, y, z };
|
||||
}
|
||||
|
||||
/** Parse a bundle once and cache everything we need from it. */
|
||||
function readBundleInfo(path: string): BundleInfo {
|
||||
try {
|
||||
const json = parseNitroBundle(readFileSync(path)).json as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
return {
|
||||
dims: extractDims(json),
|
||||
animationStates: nitroAnimationStatesCount(json),
|
||||
logicType:
|
||||
typeof json.logicType === "string" ? json.logicType : undefined,
|
||||
};
|
||||
} catch {
|
||||
return { dims: null, animationStates: undefined, logicType: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the local FurnitureData.json into a classname → flags map. Duplicate
|
||||
* classnames are merged so a true flag in any entry wins (same rule as the
|
||||
* app's `lookupRealFurniFlags`).
|
||||
*/
|
||||
function loadFurniFlags(path: string): Map<string, Flags> {
|
||||
const map = new Map<string, Flags>();
|
||||
if (!existsSync(path)) return map;
|
||||
try {
|
||||
const json = JSON.parse(readFileSync(path, "utf-8")) as Record<
|
||||
string,
|
||||
{ furnitype?: Array<Record<string, unknown>> }
|
||||
>;
|
||||
for (const section of ["roomitemtypes", "wallitemtypes"] as const) {
|
||||
for (const item of json[section]?.furnitype ?? []) {
|
||||
const cn = item.classname;
|
||||
if (typeof cn !== "string" || !cn) continue;
|
||||
const candidate: Flags = {
|
||||
cansiton: !!item.cansiton,
|
||||
canlayon: !!item.canlayon,
|
||||
canstandon: !!item.canstandon,
|
||||
};
|
||||
const existing = map.get(cn);
|
||||
if (!existing) {
|
||||
map.set(cn, candidate);
|
||||
continue;
|
||||
}
|
||||
existing.cansiton = existing.cansiton || candidate.cansiton;
|
||||
existing.canlayon = existing.canlayon || candidate.canlayon;
|
||||
existing.canstandon = existing.canstandon || candidate.canstandon;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// unreadable furnidata → interaction columns are skipped
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const t0 = Date.now();
|
||||
const dir = resolve(DIR);
|
||||
const entries = new Set(readdirSync(dir));
|
||||
const infoCache = new Map<string, BundleInfo>();
|
||||
const flagsMap = WITH_INTERACTION
|
||||
? loadFurniFlags(resolve(FURNIDATA))
|
||||
: new Map<string, Flags>();
|
||||
|
||||
const [rows] = (await db.execute(
|
||||
sql`SELECT item_name, public_name, width, length, stack_height, allow_stack,
|
||||
allow_sit, allow_lay, allow_walk, interaction_type, interaction_modes_count
|
||||
FROM items_base
|
||||
WHERE type IN (${sql.join(
|
||||
TYPES.map((t) => sql`${t}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
)) as unknown as [Row[], unknown];
|
||||
|
||||
console.log(
|
||||
`[stack-sql] ${rows.length} furniture rows | ${entries.size} bundles | ${flagsMap.size} furnidata flags`,
|
||||
);
|
||||
|
||||
const updates: string[] = [];
|
||||
let matched = 0;
|
||||
let changed = 0;
|
||||
let resolvedBase = 0;
|
||||
const counts = {
|
||||
dims: 0,
|
||||
allowFlags: 0,
|
||||
modes: 0,
|
||||
interactionType: 0,
|
||||
};
|
||||
const missing: string[] = [];
|
||||
const unparsed: string[] = [];
|
||||
|
||||
for (const row of rows) {
|
||||
const name = row.item_name;
|
||||
const base = name.includes("*") ? name.slice(0, name.indexOf("*")) : name;
|
||||
const variants = [name, name.trim(), base, base.trim()];
|
||||
|
||||
let chosen: string | null = null;
|
||||
for (const variant of variants) {
|
||||
for (const ext of EXTENSIONS) {
|
||||
const candidate = `${variant}${ext}`;
|
||||
if (entries.has(candidate)) {
|
||||
chosen = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (chosen) break;
|
||||
}
|
||||
if (!chosen) {
|
||||
missing.push(name);
|
||||
continue;
|
||||
}
|
||||
if (!chosen.startsWith(`${name}.`)) resolvedBase++;
|
||||
|
||||
let info = infoCache.get(chosen);
|
||||
if (!info) {
|
||||
info = readBundleInfo(resolve(dir, chosen));
|
||||
infoCache.set(chosen, info);
|
||||
}
|
||||
if (!info.dims) {
|
||||
unparsed.push(`${name} (${chosen})`);
|
||||
continue;
|
||||
}
|
||||
matched++;
|
||||
|
||||
const width = Math.trunc(info.dims.x);
|
||||
const length = Math.trunc(info.dims.y);
|
||||
const stackHeight = Number(info.dims.z.toFixed(2));
|
||||
const allowStack = info.dims.z > 0;
|
||||
|
||||
const setParts: string[] = [];
|
||||
const dimsChanged =
|
||||
row.width !== width ||
|
||||
row.length !== length ||
|
||||
Number(row.stack_height) !== stackHeight ||
|
||||
isTruthyBit(row.allow_stack) !== allowStack;
|
||||
if (dimsChanged) counts.dims++;
|
||||
setParts.push(
|
||||
`width=${width}`,
|
||||
`length=${length}`,
|
||||
`stack_height=${stackHeight}`,
|
||||
`allow_stack=${qbit(allowStack)}`,
|
||||
);
|
||||
|
||||
// Interaction columns — only when the classname exists in furnidata
|
||||
// (mirrors the app sweep: authoritative flags, never keyword guesses).
|
||||
const flags =
|
||||
flagsMap.get(name) ??
|
||||
flagsMap.get(base) ??
|
||||
flagsMap.get(name.trim()) ??
|
||||
flagsMap.get(base.trim());
|
||||
if (WITH_INTERACTION && flags) {
|
||||
const auto = autoDetectInteraction(name, row.public_name || undefined, {
|
||||
cansiton: flags.cansiton,
|
||||
canlayon: flags.canlayon,
|
||||
canstandon: flags.canstandon,
|
||||
hasActionData: true,
|
||||
animationStates: info.animationStates,
|
||||
logicType: info.logicType,
|
||||
});
|
||||
|
||||
if (
|
||||
Number(row.allow_sit) !== 2 &&
|
||||
isTruthyBit(row.allow_sit) !== auto.canSit
|
||||
) {
|
||||
setParts.push(`allow_sit=${qbit(auto.canSit)}`);
|
||||
counts.allowFlags++;
|
||||
}
|
||||
if (
|
||||
Number(row.allow_lay) !== 2 &&
|
||||
isTruthyBit(row.allow_lay) !== auto.canLay
|
||||
) {
|
||||
setParts.push(`allow_lay=${qbit(auto.canLay)}`);
|
||||
counts.allowFlags++;
|
||||
}
|
||||
if (
|
||||
Number(row.allow_walk) !== 2 &&
|
||||
isTruthyBit(row.allow_walk) !== auto.canStand
|
||||
) {
|
||||
setParts.push(`allow_walk=${qbit(auto.canStand)}`);
|
||||
counts.allowFlags++;
|
||||
}
|
||||
if (
|
||||
Number(row.interaction_modes_count ?? 0) !== auto.interactionModesCount
|
||||
) {
|
||||
setParts.push(`interaction_modes_count=${auto.interactionModesCount}`);
|
||||
counts.modes++;
|
||||
}
|
||||
if (
|
||||
(row.interaction_type === "default" || !row.interaction_type) &&
|
||||
auto.interactionType !== "default"
|
||||
) {
|
||||
setParts.push(`interaction_type=${q(auto.interactionType)}`);
|
||||
counts.interactionType++;
|
||||
}
|
||||
}
|
||||
|
||||
const isChanged =
|
||||
dimsChanged ||
|
||||
setParts.some(
|
||||
(p) =>
|
||||
!p.startsWith("width=") &&
|
||||
!p.startsWith("length=") &&
|
||||
!p.startsWith("stack_height=") &&
|
||||
!p.startsWith("allow_stack="),
|
||||
);
|
||||
if (isChanged) changed++;
|
||||
if (!EMIT_ALL && !isChanged) continue;
|
||||
|
||||
updates.push(
|
||||
`UPDATE items_base SET ${setParts.join(", ")} WHERE item_name=${q(name)} AND type IN (${TYPES.map(q).join(", ")});`,
|
||||
);
|
||||
}
|
||||
|
||||
const header = [
|
||||
"-- Auto-generated by scripts/generate-stack-height-sql.ts",
|
||||
`-- Source bundles: ${dir}`,
|
||||
`-- Furnidata: ${WITH_INTERACTION ? resolve(FURNIDATA) : "(disabled)"}`,
|
||||
`-- Generated: ${new Date().toISOString()}`,
|
||||
`-- Furniture rows: ${rows.length} | matched: ${matched} | changed: ${changed} | missing bundle: ${missing.length} | unparsable: ${unparsed.length}`,
|
||||
`-- Changes: dimensions ${counts.dims} | allow_sit/lay/walk ${counts.allowFlags} | modes_count ${counts.modes} | interaction_type ${counts.interactionType}`,
|
||||
`-- Mode: ${EMIT_ALL ? "all matched rows" : "changed rows only"}`,
|
||||
"",
|
||||
"START TRANSACTION;",
|
||||
"",
|
||||
].join("\n");
|
||||
const footer = "\n\nCOMMIT;\n";
|
||||
|
||||
writeFileSync(
|
||||
resolve(OUT),
|
||||
`${header}${updates.join("\n")}${footer}`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
console.log(`[stack-sql] matched ${matched}, changed ${changed}`);
|
||||
console.log(
|
||||
`[stack-sql] changes -> dims ${counts.dims}, allow flags ${counts.allowFlags}, modes ${counts.modes}, interaction_type ${counts.interactionType}`,
|
||||
);
|
||||
if (resolvedBase > 0)
|
||||
console.log(`[stack-sql] resolved via base classname: ${resolvedBase}`);
|
||||
console.log(`[stack-sql] updates written: ${updates.length}`);
|
||||
if (missing.length) {
|
||||
console.log(
|
||||
`[stack-sql] no bundle (${missing.length}): ${missing.slice(0, 20).join(", ")}${missing.length > 20 ? ", …" : ""}`,
|
||||
);
|
||||
}
|
||||
if (unparsed.length) {
|
||||
console.log(
|
||||
`[stack-sql] unparsable (${unparsed.length}): ${unparsed.slice(0, 20).join(", ")}${unparsed.length > 20 ? ", …" : ""}`,
|
||||
);
|
||||
}
|
||||
console.log(`[stack-sql] wrote ${resolve(OUT)} in ${Date.now() - t0}ms`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in new issue
Block a user