From 39512dcd924fc489a503134059c161d1a9dce7c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Gallego?= Date: Fri, 23 May 2025 15:18:51 +0900 Subject: [PATCH] Experiment with parse toml --- README.md | 16 +- bin/generate-globals.js | 48 +++++ bin/generate-metaobject-types.js | 294 ++++++++++++++++++++++++++ cli/generate-globals.ts | 63 ++++++ cli/generate-metaobject-types.ts | 349 +++++++++++++++++++++++++++++++ package-lock.json | 21 +- package.json | 6 + src/global.d.ts | 7 + 8 files changed, 793 insertions(+), 11 deletions(-) create mode 100755 bin/generate-globals.js create mode 100755 bin/generate-metaobject-types.js create mode 100644 cli/generate-globals.ts create mode 100644 cli/generate-metaobject-types.ts create mode 100644 src/global.d.ts diff --git a/README.md b/README.md index 6f39718..d467026 100644 --- a/README.md +++ b/README.md @@ -132,4 +132,18 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { * Adding a `bulkUpsert` method to upsert a high number of objects using a long standing job. * Adding an `export` method to the repository to export up to 250 metaobjects. * Adding a `bulkExport` method to export any number of metaobjects, using the bulk API. -* Adding a `syncFromSchema` method on the definition manager to sync definitions. \ No newline at end of file +* Adding a `syncFromSchema` method on the definition manager to sync definitions. + + + +IDEA: + +we should generate the following metadata: + +__METAOBJECT_METADATA__ = { + '$app:type' = { + fields: { + foo: product_reference + } + } +} \ No newline at end of file diff --git a/bin/generate-globals.js b/bin/generate-globals.js new file mode 100755 index 0000000..44e13dc --- /dev/null +++ b/bin/generate-globals.js @@ -0,0 +1,48 @@ +#!/usr/bin/env node +"use strict"; +var _a; +Object.defineProperty(exports, "__esModule", { value: true }); +var fs = require("fs"); +var path = require("path"); +var toml_1 = require("@iarna/toml"); +var lodash_1 = require("lodash"); +var commander_1 = require("commander"); +var program = new commander_1.Command() + .requiredOption('-i, --input ', 'path to your definitions.toml') + .requiredOption('-o, --output ', 'where to emit the runtime .ts') + .option('-d, --dts ', 'where to emit the ambient .d.ts', function (val, _) { return val; }, undefined) + .parse(process.argv); +var _b = program.opts(), input = _b.input, output = _b.output, dts = _b.dts; +var tomlSrc = fs.readFileSync(path.resolve(input), 'utf8'); +var data = (0, toml_1.parse)(tomlSrc); +var appDefs = (_a = data.metaobjects) === null || _a === void 0 ? void 0 : _a.app; +if (!appDefs) { + console.error('❌ no [metaobjects.app] block found in', input); + process.exit(1); +} +// build the metadata payload +var metadata = {}; +for (var _i = 0, _c = Object.entries(appDefs); _i < _c.length; _i++) { + var _d = _c[_i], typeKey = _d[0], def = _d[1]; + if (typeof def !== 'object' || !('fields' in def)) + continue; + var fields = {}; + for (var _e = 0, _f = Object.entries(def.fields); _e < _f.length; _e++) { + var _g = _f[_e], fk = _g[0], fd = _g[1]; + var t = fd.type; + if (typeof t === 'string') + fields[(0, lodash_1.camelCase)(fk)] = t; + } + metadata["$app:".concat(typeKey)] = { fields: fields }; +} +// 1) emit the runtime file +var payload = JSON.stringify(metadata, null, 2); +var tsOut = "// generated \u2014 do not edit\nexport {}\ndeclare global {\n var __METAOBJECTS_METADATA__: Record }>\n}\nglobalThis.__METAOBJECTS_METADATA__ = ".concat(payload, " as any\n"); +fs.mkdirSync(path.dirname(output), { recursive: true }); +fs.writeFileSync(output, tsOut, 'utf8'); +console.log('✔ wrote runtime to', output); +// 2) emit an ambient .d.ts (either your -d value or same basename) +var dtsPath = dts !== null && dts !== void 0 ? dts : output.replace(/\.tsx?$/, '') + '.d.ts'; +var dtsOut = "// generated \u2014 do not edit\nexport {}\ndeclare global {\n var __METAOBJECTS_METADATA__: Record }>\n}\n"; +fs.writeFileSync(dtsPath, dtsOut, 'utf8'); +console.log('✔ wrote types to', dtsPath); diff --git a/bin/generate-metaobject-types.js b/bin/generate-metaobject-types.js new file mode 100755 index 0000000..d294a83 --- /dev/null +++ b/bin/generate-metaobject-types.js @@ -0,0 +1,294 @@ +#!/usr/bin/env node +"use strict"; +var _a, _b; +Object.defineProperty(exports, "__esModule", { value: true }); +var fs = require("fs"); +var path = require("path"); +var toml_1 = require("@iarna/toml"); +var lodash_1 = require("lodash"); +// --- Helper Type Definitions (move to shared types if desired) +/*type PickedFile = Pick; +type PickedMediaImage = Pick; +type PickedVideo = Pick; +type PickedGenericFile = Pick;*/ +var defaultTypes = "\ntype Weight = {\n unit: 'oz' | 'lb' | 'g' | 'kg';\n value: number;\n}\n\ntype Volume = {\n unit: 'ml' | 'cl' | 'l' | 'm3' | 'us_fl_oz' | 'us_pt' | 'us_qt' | 'us_gal' | 'imp_fl_oz' | 'imp_pt' | 'impt_qt' | 'imp_gal';\n value: number;\n}\n\ntype Dimension = {\n unit: 'in' | 'ft' | 'yd' | 'mm' | 'cm' | 'm';\n value: number;\n}\n\ntype Link = {\n text: string;\n url: string;\n}\n\ntype Money = {\n amount: string;\n currencyCode: string;\n}\n\ntype Rating = {\n value: string;\n scaleMin: string;\n scaleMax: string;\n}\n"; +// --- Base mapping for simple and list types +var baseTypeMapping = { + boolean: 'boolean', + color: 'string', + date: 'string', + date_time: 'string', + dimension: 'Dimension', + id: 'string', + link: 'Link', + money: 'Money', + multi_line_text_field: 'string', + number_decimal: 'number', + number_integer: 'number', + rating: 'Rating', + rich_text_field: 'string', + single_line_text_field: 'string', + url: 'string', + volume: 'Volume', + weight: 'Weight', + json: 'object', + collection_reference: 'string', + customer_reference: 'string', + company_reference: 'string', + file_reference: 'string', + metaobject_reference: 'string', + mixed_reference: 'any', + page_reference: 'string', + product_reference: 'string', + product_taxonomy_value_reference: 'string', + variant_reference: 'string', +}; +// --- Static PopulatedMap entries +var staticPopulatedMapping = { + boolean: 'boolean', + color: 'string', + date: 'string', + date_time: 'string', + dimension: 'Dimension', + id: 'string', + link: 'Link', + money: 'Money', + multi_line_text_field: 'string', + number_decimal: 'number', + number_integer: 'number', + rating: 'Rating', + rich_text_field: 'string', + single_line_text_field: 'string', + url: 'string', + volume: 'Volume', + weight: 'Weight', + json: 'object', + collection_reference: "Pick", + customer_reference: "Pick", + company_reference: "Pick", + file_reference: 'PickedFile|PickedMediaImage|PickedVideo|PickedGenericFile', + page_reference: "Pick", + product_reference: "Pick", + product_taxonomy_value_reference: "Pick", + variant_reference: "Pick", +}; +// --- Map a raw TOML type key to TS type (handles list variants) +function mapFieldType(raw) { + if (raw.startsWith('list.')) { + return mapSingleType(raw.slice(5)) + '[]'; + } + return mapSingleType(raw); +} +function mapSingleType(key) { + var _a; + return (_a = baseTypeMapping[key]) !== null && _a !== void 0 ? _a : 'any'; +} +// --- Recursively collect JSON schemas including nested ones +function collectSchemas(name, schema, collected) { + if (schema.type === 'object' && schema.properties) { + var props = schema.properties; + // Process nested object properties + for (var _i = 0, _a = Object.entries(props); _i < _a.length; _i++) { + var _b = _a[_i], propKey = _b[0], propSchemaRaw = _b[1]; + var propSchema = propSchemaRaw; + if (propSchema.type === 'object' && + propSchema.properties) { + var nestedName = "".concat(name).concat((0, lodash_1.upperFirst)((0, lodash_1.camelCase)(propKey))); + collectSchemas(nestedName, propSchema, collected); + // Replace nested schema with reference + schema.properties[propKey] = { $ref: nestedName }; + } + else if (propSchema.type === 'array' && + propSchema.items) { + var itemSchema = propSchema.items; + if (itemSchema.type === 'object' && + itemSchema.properties) { + var nestedItemName = "".concat(name).concat((0, lodash_1.upperFirst)((0, lodash_1.camelCase)(propKey)), "Item"); + collectSchemas(nestedItemName, itemSchema, collected); + // Replace items with reference + schema.properties[propKey] = { type: 'array', items: { $ref: nestedItemName } }; + } + } + } + collected.push({ name: name, schema: schema }); + } + else if (schema.type === 'array' && schema.items) { + var itemSchema = schema.items; + var nestedName = "".concat(name, "Item"); + if (itemSchema.type === 'object' && itemSchema.properties) { + collectSchemas(nestedName, itemSchema, collected); + // After nested, keep collection schema + collected.push({ name: name, schema: { type: 'array', items: { $ref: nestedName } } }); + } + else { + // Array of primitives + collected.push({ name: name, schema: schema }); + } + } + else { + // Primitives or fallback + collected.push({ name: name, schema: schema }); + } +} +// --- Map JSON schema entry to TS type (primitive, $ref, array) +function mapJsonType(schema) { + if (schema.$ref) { + return schema.$ref; + } + if (schema.type === 'array' && schema.items) { + return "".concat(mapJsonType(schema.items), "[]"); + } + return mapJsonSchemaType(schema); +} +// --- Generate TS from flattened JSON schema definitions +function generateTypesFromJsonSchemas(schemas) { + return schemas + .map(function (_a) { + var name = _a.name, schema = _a.schema; + if (schema.type === 'object' && schema.properties) { + var required_1 = new Set(schema.required || []); + var props = Object.entries(schema.properties) + .map(function (_a) { + var k = _a[0], v = _a[1]; + var propName = (0, lodash_1.camelCase)(k); + var opt = required_1.has(k) ? '' : '?'; + var tsType = mapJsonType(v); + return " ".concat(propName).concat(opt, ": ").concat(tsType, ";"); + }) + .join('\n'); + return "export interface ".concat(name, " {\n").concat(props, "\n}"); + } + if (schema.type === 'array' && schema.items) { + var tsType = mapJsonType(schema); + return "export type ".concat(name, " = ").concat(tsType, ";"); + } + return "export type ".concat(name, " = ").concat(mapJsonType(schema), ";"); + }) + .join('\n\n'); +} +// --- Map individual JSON schema property to TS type (primitive fallback) +function mapJsonSchemaType(prop) { + var t = prop.type; + if (Array.isArray(t)) { + return t.map(function (x) { return mapJsonSchemaType({ type: x }); }).join(' | '); + } + switch (t) { + case 'string': + return 'string'; + case 'number': + case 'integer': + return 'number'; + case 'boolean': + return 'boolean'; + case 'object': + // Should have been replaced or collected + return 'object'; + case 'array': + return prop.items ? mapJsonSchemaType(prop.items) + '[]' : 'any[]'; + default: + return 'any'; + } +} +// --- Parse populated types for metaobject and mixed references +function parsePopulatedType(raw, metaNames) { + var _a; + var m = raw.match(/^metaobject_reference<\$app:([^>]+)>$/); + if (m) { + var nm = m[1]; + if (metaNames.includes(nm)) + return (0, lodash_1.upperFirst)((0, lodash_1.camelCase)(nm)); + } + var mm = raw.match(/^mixed_reference<([^>]+)>$/); + if (mm) { + return mm[1] + .split(',') + .map(function (s) { return (0, lodash_1.upperFirst)((0, lodash_1.camelCase)(s.replace(/\$app:/, ''))); }) + .join(' | '); + } + return (_a = staticPopulatedMapping[raw]) !== null && _a !== void 0 ? _a : 'any'; +} +// --- Main CLI +var _c = process.argv, inputFile = _c[2], outputFile = _c[3]; +if (!inputFile || !outputFile) { + console.error('Usage: generate-metaobject-types '); + process.exit(1); +} +var tomlContent = fs.readFileSync(path.resolve(inputFile), 'utf-8'); +var parsed = (0, toml_1.parse)(tomlContent); +var appMeta = (_a = parsed.metaobjects) === null || _a === void 0 ? void 0 : _a.app; +if (!appMeta) { + console.error('Missing [metaobjects.app]'); + process.exit(1); +} +// 1) Collect JSON schemas and map to subtype names +var jsonSchemas = []; +var jsonFieldMap = {}; +for (var _i = 0, _d = Object.entries(appMeta); _i < _d.length; _i++) { + var _e = _d[_i], metaKey = _e[0], def = _e[1]; + var typeName = (0, lodash_1.upperFirst)((0, lodash_1.camelCase)(metaKey)); + var fields = def.fields || {}; + for (var _f = 0, _g = Object.entries(fields); _f < _g.length; _f++) { + var _h = _g[_f], fieldKey = _h[0], cfg = _h[1]; + if (cfg.type === 'json' && ((_b = cfg.validations) === null || _b === void 0 ? void 0 : _b.schema)) { + var subName = "".concat(typeName).concat((0, lodash_1.upperFirst)((0, lodash_1.camelCase)(fieldKey))); + var schemaObj = JSON.parse(cfg.validations.schema); + jsonSchemas.push({ name: subName, schema: schemaObj }); + jsonFieldMap["".concat(metaKey, ".").concat(fieldKey)] = subName; + } + } +} +// 2) Generate TS interfaces for JSON schemas +var jsonTypeDefs = generateTypesFromJsonSchemas(jsonSchemas); +// 3) Collect raw field types for dynamic PopulateMap +var rawTypesSet = new Set(); +for (var _j = 0, _k = Object.values(appMeta); _j < _k.length; _j++) { + var defObj = _k[_j]; + var fields = defObj.fields || {}; + for (var _l = 0, _m = Object.values(fields); _l < _m.length; _l++) { + var cfg = _m[_l]; + rawTypesSet.add(cfg.type); + } +} +var metaNames = Object.keys(appMeta); +// 4) Generate TS types for each metaobject +var interfaces = Object.entries(appMeta) + .map(function (_a) { + var metaKey = _a[0], def = _a[1]; + var typeName = (0, lodash_1.upperFirst)((0, lodash_1.camelCase)(metaKey)); + var fields = def.fields || {}; + var props = Object.entries(fields) + .map(function (_a) { + var fieldKey = _a[0], cfg = _a[1]; + var mapKey = "".concat(metaKey, ".").concat(fieldKey); + var propName = (0, lodash_1.camelCase)(fieldKey); + var optional = cfg.required ? '' : '?'; + var tsType = jsonFieldMap[mapKey] + ? jsonFieldMap[mapKey] + : mapFieldType(cfg.type); + return " ".concat(propName).concat(optional, ": ").concat(tsType, ";"); + }) + .join('\n'); + return "export type ".concat(typeName, " = {\n").concat(props, "\n};"); +}) + .join('\n\n'); +// 5) Generate PopulatedMap entries +var populatedEntries = []; +for (var _o = 0, _p = Object.entries(staticPopulatedMapping); _o < _p.length; _o++) { + var _q = _p[_o], k = _q[0], v = _q[1]; + populatedEntries.push(" '".concat(k, "': ").concat(v, ";")); +} +for (var _r = 0, _s = Array.from(rawTypesSet); _r < _s.length; _r++) { + var raw = _s[_r]; + if (/^(metaobject_reference|mixed_reference)<.*>$/.test(raw)) { + var mapped = parsePopulatedType(raw, metaNames); + populatedEntries.push(" '".concat(raw, "': ").concat(mapped, ";")); + } +} +var populatedMap = "export type PopulatedMap = {\n".concat(populatedEntries.join('\n'), "\n};"); +// 6) Populate utility\ +var populateHelper = "/**\n * Populate replaces string IDs\n * for Keys with actual types from PopulatedMap.\n */\nexport type Populate<\n T,\n Keys extends readonly (keyof T)[]\n> = { [P in keyof T]:\n P extends Keys[number]\n ? T[P] extends string\n ? PopulatedMap[T[P] & keyof PopulatedMap]\n : T[P]\n : T[P]\n};"; +// 7) Compose output +var header = "/** AUTO-GENERATED: do NOT edit */"; +var output = [header, defaultTypes, jsonTypeDefs, interfaces, populatedMap, populateHelper].join('\n\n'); +fs.writeFileSync(path.resolve(outputFile), output, 'utf-8'); +console.log("\u2728 Generated types to ".concat(outputFile)); diff --git a/cli/generate-globals.ts b/cli/generate-globals.ts new file mode 100644 index 0000000..68cafb5 --- /dev/null +++ b/cli/generate-globals.ts @@ -0,0 +1,63 @@ +#!/usr/bin/env node + +import * as fs from 'fs' +import * as path from 'path' +import { parse as parseToml } from '@iarna/toml'; +import { camelCase } from 'lodash'; +import { Command } from 'commander' + +const program = new Command() + .requiredOption('-i, --input ', 'path to your definitions.toml') + .requiredOption('-o, --output ', 'where to emit the runtime .ts') + .option ('-d, --dts ', 'where to emit the ambient .d.ts', + (val, _) => val, + undefined) + .parse(process.argv) + +const { input, output, dts } = program.opts() +const tomlSrc = fs.readFileSync(path.resolve(input), 'utf8') +const data = parseToml(tomlSrc) as any + +const appDefs = data.metaobjects?.app +if (!appDefs) { + console.error('❌ no [metaobjects.app] block found in', input) + process.exit(1) +} + +// build the metadata payload +const metadata: Record }> = {} +for (const [typeKey, def] of Object.entries(appDefs)) { + if (typeof def !== 'object' || !('fields' in def)) continue + const fields: Record = {} + for (const [fk, fd] of Object.entries((def as any).fields)) { + const t = (fd as any).type + if (typeof t === 'string') fields[camelCase(fk)] = t + } + metadata[`$app:${typeKey}`] = { fields } +} + +// 1) emit the runtime file +const payload = JSON.stringify(metadata, null, 2) +const tsOut = `// generated — do not edit +export {} +declare global { + var __METAOBJECTS_METADATA__: Record }> +} +globalThis.__METAOBJECTS_METADATA__ = ${payload} as any +` +fs.mkdirSync(path.dirname(output), { recursive: true }) +fs.writeFileSync(output, tsOut, 'utf8') +console.log('✔ wrote runtime to', output) + +// 2) emit an ambient .d.ts (either your -d value or same basename) +const dtsPath = dts + ?? output.replace(/\.tsx?$/,'') + '.d.ts' + +const dtsOut = `// generated — do not edit +export {} +declare global { + var __METAOBJECTS_METADATA__: Record }> +} +` +fs.writeFileSync(dtsPath, dtsOut, 'utf8') +console.log('✔ wrote types to', dtsPath) diff --git a/cli/generate-metaobject-types.ts b/cli/generate-metaobject-types.ts new file mode 100644 index 0000000..241c115 --- /dev/null +++ b/cli/generate-metaobject-types.ts @@ -0,0 +1,349 @@ +#!/usr/bin/env node + +import * as fs from 'fs'; +import * as path from 'path'; +import { parse as parseToml } from '@iarna/toml'; +import { camelCase, upperFirst } from 'lodash'; + +// --- Helper Type Definitions (move to shared types if desired) +/*type PickedFile = Pick; +type PickedMediaImage = Pick; +type PickedVideo = Pick; +type PickedGenericFile = Pick;*/ + +const defaultTypes = ` +type Weight = { + unit: 'oz' | 'lb' | 'g' | 'kg'; + value: number; +} + +type Volume = { + unit: 'ml' | 'cl' | 'l' | 'm3' | 'us_fl_oz' | 'us_pt' | 'us_qt' | 'us_gal' | 'imp_fl_oz' | 'imp_pt' | 'impt_qt' | 'imp_gal'; + value: number; +} + +type Dimension = { + unit: 'in' | 'ft' | 'yd' | 'mm' | 'cm' | 'm'; + value: number; +} + +type Link = { + text: string; + url: string; +} + +type Money = { + amount: string; + currencyCode: string; +} + +type Rating = { + value: string; + scaleMin: string; + scaleMax: string; +} +` + +// --- Base mapping for simple and list types +const baseTypeMapping: Record = { + boolean: 'boolean', + color: 'string', + date: 'string', + date_time: 'string', + dimension: 'Dimension', + id: 'string', + link: 'Link', + money: 'Money', + multi_line_text_field: 'string', + number_decimal: 'number', + number_integer: 'number', + rating: 'Rating', + rich_text_field: 'string', + single_line_text_field: 'string', + url: 'string', + volume: 'Volume', + weight: 'Weight', + json: 'object', + collection_reference: 'string', + customer_reference: 'string', + company_reference: 'string', + file_reference: 'string', + metaobject_reference: 'string', + mixed_reference: 'any', + page_reference: 'string', + product_reference: 'string', + product_taxonomy_value_reference: 'string', + variant_reference: 'string', +}; + +// --- Static PopulatedMap entries +const staticPopulatedMapping: Record = { + boolean: 'boolean', + color: 'string', + date: 'string', + date_time: 'string', + dimension: 'Dimension', + id: 'string', + link: 'Link', + money: 'Money', + multi_line_text_field: 'string', + number_decimal: 'number', + number_integer: 'number', + rating: 'Rating', + rich_text_field: 'string', + single_line_text_field: 'string', + url: 'string', + volume: 'Volume', + weight: 'Weight', + json: 'object', + collection_reference: "Pick", + customer_reference: "Pick", + company_reference: "Pick", + file_reference: 'PickedFile|PickedMediaImage|PickedVideo|PickedGenericFile', + page_reference: "Pick", + product_reference: "Pick", + product_taxonomy_value_reference: "Pick", + variant_reference: "Pick", +}; + +// --- Map a raw TOML type key to TS type (handles list variants) +function mapFieldType(raw: string): string { + if (raw.startsWith('list.')) { + return mapSingleType(raw.slice(5)) + '[]'; + } + return mapSingleType(raw); +} +function mapSingleType(key: string): string { + return baseTypeMapping[key] ?? 'any'; +} + +// --- Recursively collect JSON schemas including nested ones +function collectSchemas( + name: string, + schema: any, + collected: Array<{ name: string; schema: any }> +) { + if (schema.type === 'object' && schema.properties) { + const props = schema.properties as Record; + // Process nested object properties + for (const [propKey, propSchemaRaw] of Object.entries(props)) { + const propSchema = propSchemaRaw as any; + if ( + propSchema.type === 'object' && + propSchema.properties + ) { + const nestedName = `${name}${upperFirst(camelCase(propKey))}`; + collectSchemas(nestedName, propSchema, collected); + // Replace nested schema with reference + schema.properties[propKey] = { $ref: nestedName }; + } else if ( + propSchema.type === 'array' && + propSchema.items + ) { + const itemSchema = propSchema.items as any; + if ( + itemSchema.type === 'object' && + itemSchema.properties + ) { + const nestedItemName = `${name}${upperFirst(camelCase(propKey))}Item`; + collectSchemas(nestedItemName, itemSchema, collected); + // Replace items with reference + schema.properties[propKey] = { type: 'array', items: { $ref: nestedItemName } }; + } + } + } + collected.push({ name, schema }); + } else if (schema.type === 'array' && schema.items) { + const itemSchema = (schema.items as any); + const nestedName = `${name}Item`; + if (itemSchema.type === 'object' && itemSchema.properties) { + collectSchemas(nestedName, itemSchema, collected); + // After nested, keep collection schema + collected.push({ name, schema: { type: 'array', items: { $ref: nestedName } } }); + } else { + // Array of primitives + collected.push({ name, schema }); + } + } else { + // Primitives or fallback + collected.push({ name, schema }); + } +} + +// --- Map JSON schema entry to TS type (primitive, $ref, array) +function mapJsonType(schema: any): string { + if (schema.$ref) { + return schema.$ref; + } + if (schema.type === 'array' && schema.items) { + return `${mapJsonType(schema.items)}[]`; + } + return mapJsonSchemaType(schema); +} + +// --- Generate TS from flattened JSON schema definitions +function generateTypesFromJsonSchemas( + schemas: Array<{ name: string; schema: any }> +): string { + return schemas + .map(({ name, schema }) => { + if (schema.type === 'object' && schema.properties) { + const required = new Set(schema.required || []); + const props = Object.entries(schema.properties) + .map(([k, v]) => { + const propName = camelCase(k); + const opt = required.has(k) ? '' : '?'; + const tsType = mapJsonType(v); + return ` ${propName}${opt}: ${tsType};`; + }) + .join('\n'); + return `export interface ${name} {\n${props}\n}`; + } + if (schema.type === 'array' && schema.items) { + const tsType = mapJsonType(schema); + return `export type ${name} = ${tsType};`; + } + return `export type ${name} = ${mapJsonType(schema)};`; + }) + .join('\n\n'); +} + +// --- Map individual JSON schema property to TS type (primitive fallback) +function mapJsonSchemaType(prop: any): string { + const t = prop.type; + if (Array.isArray(t)) { + return t.map((x: any) => mapJsonSchemaType({ type: x })).join(' | '); + } + switch (t) { + case 'string': + return 'string'; + case 'number': + case 'integer': + return 'number'; + case 'boolean': + return 'boolean'; + case 'object': + // Should have been replaced or collected + return 'object'; + case 'array': + return prop.items ? mapJsonSchemaType(prop.items) + '[]' : 'any[]'; + default: + return 'any'; + } +} + +// --- Parse populated types for metaobject and mixed references +function parsePopulatedType(raw: string, metaNames: string[]): string { + const m = raw.match(/^metaobject_reference<\$app:([^>]+)>$/); + if (m) { + const nm = m[1]; + if (metaNames.includes(nm)) return upperFirst(camelCase(nm)); + } + const mm = raw.match(/^mixed_reference<([^>]+)>$/); + if (mm) { + return mm[1] + .split(',') + .map(s => upperFirst(camelCase(s.replace(/\$app:/, '')))) + .join(' | '); + } + return staticPopulatedMapping[raw] ?? 'any'; +} + +// --- Main CLI +const [,, inputFile, outputFile] = process.argv; +if (!inputFile || !outputFile) { + console.error('Usage: generate-metaobject-types '); + process.exit(1); +} + +const tomlContent = fs.readFileSync(path.resolve(inputFile), 'utf-8'); +const parsed: any = parseToml(tomlContent); +const appMeta: any = parsed.metaobjects?.app; +if (!appMeta) { + console.error('Missing [metaobjects.app]'); + process.exit(1); +} + +// 1) Collect JSON schemas and map to subtype names +const jsonSchemas: Array<{name: string; schema: any}> = []; +const jsonFieldMap: Record = {}; +for (const [metaKey, def] of Object.entries(appMeta as any)) { + const typeName = upperFirst(camelCase(metaKey)); + const fields = (def as any).fields || {}; + for (const [fieldKey, cfg] of Object.entries(fields as any)) { + if ((cfg as any).type === 'json' && (cfg as any).validations?.schema) { + const subName = `${typeName}${upperFirst(camelCase(fieldKey))}`; + const schemaObj = JSON.parse((cfg as any).validations.schema); + jsonSchemas.push({ name: subName, schema: schemaObj }); + jsonFieldMap[`${metaKey}.${fieldKey}`] = subName; + } + } +} + +// 2) Generate TS interfaces for JSON schemas +const jsonTypeDefs = generateTypesFromJsonSchemas(jsonSchemas); + +// 3) Collect raw field types for dynamic PopulateMap +const rawTypesSet = new Set(); +for (const defObj of Object.values(appMeta as any)) { + const fields = (defObj as any).fields || {}; + for (const cfg of Object.values(fields as any)) { + rawTypesSet.add((cfg as any).type); + } +} +const metaNames = Object.keys(appMeta); + +// 4) Generate TS types for each metaobject +const interfaces = Object.entries(appMeta as any) + .map(([metaKey, def]: [string, any]) => { + const typeName = upperFirst(camelCase(metaKey)); + const fields = (def as any).fields || {}; + const props = Object.entries(fields as any) + .map(([fieldKey, cfg]: [string, any]) => { + const mapKey = `${metaKey}.${fieldKey}`; + const propName = camelCase(fieldKey); + const optional = cfg.required ? '' : '?'; + const tsType = jsonFieldMap[mapKey] + ? jsonFieldMap[mapKey] + : mapFieldType(cfg.type); + return ` ${propName}${optional}: ${tsType};`; + }) + .join('\n'); + return `export type ${typeName} = {\n${props}\n};`; + }) + .join('\n\n'); + +// 5) Generate PopulatedMap entries +const populatedEntries: string[] = []; +for (const [k, v] of Object.entries(staticPopulatedMapping)) { + populatedEntries.push(` '${k}': ${v};`); +} +for (const raw of Array.from(rawTypesSet)) { + if (/^(metaobject_reference|mixed_reference)<.*>$/.test(raw)) { + const mapped = parsePopulatedType(raw, metaNames); + populatedEntries.push(` '${raw}': ${mapped};`); + } +} +const populatedMap = `export type PopulatedMap = {\n${populatedEntries.join('\n')}\n};`; + +// 6) Populate utility\ +const populateHelper = `/** + * Populate replaces string IDs + * for Keys with actual types from PopulatedMap. + */ +export type Populate< + T, + Keys extends readonly (keyof T)[] +> = { [P in keyof T]: + P extends Keys[number] + ? T[P] extends string + ? PopulatedMap[T[P] & keyof PopulatedMap] + : T[P] + : T[P] +};`; + +// 7) Compose output +const header = `/** AUTO-GENERATED: do NOT edit */`; +const output = [header, defaultTypes, jsonTypeDefs, interfaces, populatedMap, populateHelper].join('\n\n'); +fs.writeFileSync(path.resolve(outputFile), output, 'utf-8'); +console.log(`✨ Generated types to ${outputFile}`); diff --git a/package-lock.json b/package-lock.json index 1d78982..867b20d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,20 @@ { "name": "metaobject-repository", - "version": "0.17.1", + "version": "0.18.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "metaobject-repository", - "version": "0.17.1", + "version": "0.18.0", "license": "ISC", "dependencies": { - "@thebeyondgroup/shopify-rich-text-renderer": "^2.1.0", + "lodash": "^4.17.21", "raku-ql": "^1.4.0", "snake-camel": "^1.0.9" }, "devDependencies": { + "@iarna/toml": "2.2.5", "@shopify/api-codegen-preset": "^1.1.7", "@shopify/shopify-app-remix": "^3.8.2", "json-schema-to-ts": "^3.1.1", @@ -1728,6 +1729,13 @@ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, + "node_modules/@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", + "dev": true, + "license": "ISC" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2748,12 +2756,6 @@ "@shopify/graphql-client": "^1.3.2" } }, - "node_modules/@thebeyondgroup/shopify-rich-text-renderer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@thebeyondgroup/shopify-rich-text-renderer/-/shopify-rich-text-renderer-2.1.0.tgz", - "integrity": "sha512-T6mM3rmMSgYOIuH5TSfVrdMEHfDXMSnHDM7F4nU4HshgMrd4/9v9JmRIrc2uNjTXBsb92lxBfzjj3ZrMt3v3jQ==", - "license": "MIT" - }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", @@ -4979,7 +4981,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, "license": "MIT" }, "node_modules/lodash.includes": { diff --git a/package.json b/package.json index 794e2af..c7b67c7 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,14 @@ ], "scripts": { "build": "tsup", + "build:cli": "tsc --outDir bin/ cli/generate-globals.ts", "test": "vitest run", "dev": "vite", "graphql-codegen": "graphql-codegen" }, + "bin": { + "generate-globals": "./bin/generate-globals" + }, "repository": { "type": "git", "url": "git+https://github.com/maestrooo/metaobject-repository.git" @@ -35,6 +39,8 @@ "devDependencies": { "@shopify/api-codegen-preset": "^1.1.7", "@shopify/shopify-app-remix": "^3.8.2", + "@iarna/toml": "2.2.5", + "lodash": "^4.17.21", "json-schema-to-ts": "^3.1.1", "tsup": "^8.4.0", "typescript": "^5.8.3", diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 0000000..1d907c2 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,7 @@ +export {} + +declare global { + var __METAOBJECTS_METADATA__: Record + }> +} \ No newline at end of file