From da2f6986d9ec05408f217a0f514bfbda0a957d70 Mon Sep 17 00:00:00 2001 From: David Cameron Date: Tue, 29 Apr 2025 11:44:37 -0400 Subject: [PATCH 1/2] Add robust API version update utilities - Add @shopify/toml-patch for TOML manipulation - Create utilities for API version management and schema updates - Add check-api-version.js to auto-update extension API versions - Add configure-extension-directories.js to manage app config - Add update-schemas.js script for schema updates with CLI - Update README with simplified instructions - Add yarn commands for API version maintenance - Add .gitignore entry for shopify.app.toml --- .gitignore | 1 + README.md | 14 +++ package.json | 8 +- util/check-api-version.js | 114 ++++++++++++++++++++++++ util/configure-extension-directories.js | 71 +++++++++++++++ util/update-schemas.js | 93 +++++++++++++++++++ 6 files changed, 300 insertions(+), 1 deletion(-) create mode 100644 util/check-api-version.js create mode 100644 util/configure-extension-directories.js create mode 100644 util/update-schemas.js diff --git a/.gitignore b/.gitignore index 2be74316..9fe87f95 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ sample-apps/*/public/build/* !sample-apps/**/shopify.extension.toml !sample-apps/**/locales/*.json dev.sqlite +shopify.app.toml diff --git a/README.md b/README.md index b02ecb8f..bb8cd753 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,20 @@ yarn expand-liquid vanilla-js yarn expand-liquid typescript ``` +### Update API Versions and Function Schemas + +To update API versions and function schemas automatically: + +```shell +# Step 1: Link to a Shopify app to create shopify.app.toml with client_id +shopify app config link + +# Step 2: Run the comprehensive update command +yarn update-all +``` + +This updates API versions across all extensions, configures extension directories, expands liquid templates, and updates function schemas in one command. + ### Run Tests ```shell diff --git a/package.json b/package.json index 3677e6ae..fe0cba00 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,8 @@ "type": "module", "devDependencies": { "@iarna/toml": "^2.2.5", + "@shopify/toml-patch": "^0.3.0", + "dayjs": "^1.11.11", "fast-glob": "^3.2.11", "liquidjs": "^9.37.0", "@graphql-codegen/cli": "^3.2.2", @@ -15,7 +17,11 @@ "expand-liquid": "node ./util/expand-liquid.js", "typegen": "yarn workspaces run graphql-code-generator --config package.json", "test-js": "yarn expand-liquid vanilla-js && yarn && yarn typegen && yarn workspaces run test run", - "test-ts": "yarn expand-liquid typescript && yarn && yarn typegen && yarn workspaces run test run" + "test-ts": "yarn expand-liquid typescript && yarn && yarn typegen && yarn workspaces run test run", + "update-api-version": "node ./util/check-api-version.js", + "configure-extension-directories": "node ./util/configure-extension-directories.js", + "update-schemas": "node ./util/update-schemas.js", + "update-all": "yarn update-api-version && yarn configure-extension-directories && yarn expand-liquid && yarn update-schemas" }, "private": true, "workspaces": [ diff --git a/util/check-api-version.js b/util/check-api-version.js new file mode 100644 index 00000000..5a0560dd --- /dev/null +++ b/util/check-api-version.js @@ -0,0 +1,114 @@ +import fs from 'fs/promises'; +import fastGlob from 'fast-glob'; +import dayjs from 'dayjs'; +import { updateTomlValues } from '@shopify/toml-patch'; + +const ROOT_DIR = '.'; +const FILE_PATTERN = '**/shopify.extension.toml.liquid'; +const LIQUID_PLACEHOLDER = 'LIQUID_PLACEHOLDER'; + +// Method to get the latest API version based on today's date +function getLatestApiVersion() { + const date = dayjs(); + const year = date.year(); + const month = date.month(); + const quarter = Math.floor(month / 3); + + const monthNum = quarter * 3 + 1; + const paddedMonth = String(monthNum).padStart(2, '0'); + + return `${year}-${paddedMonth}`; +} + +// Method to find all shopify.extension.toml.liquid files +async function findAllExtensionFiles() { + return fastGlob(FILE_PATTERN, { cwd: ROOT_DIR, absolute: true }); +} + +// Function to preprocess liquid syntax for TOML parsing +function preprocessLiquidSyntax(content) { + const liquidExpressions = []; + const placeholderContent = content.replace(/\{\{.*?\}\}|\{%\s.*?\s%\}/g, (match) => { + liquidExpressions.push(match); + return `{${LIQUID_PLACEHOLDER}:${liquidExpressions.length - 1}}`; + }); + return { placeholderContent, liquidExpressions }; +} + +// Function to restore liquid syntax after TOML manipulation +function restoreLiquidSyntax(content, liquidExpressions) { + return content.replace(new RegExp(`\\{${LIQUID_PLACEHOLDER}:(\\d+)\\}`, 'g'), (match, index) => { + return liquidExpressions[Number(index)]; + }); +} + +// Method to update the API version in the file using toml-patch +async function updateApiVersion(filePath, latestVersion) { + try { + const content = await fs.readFile(filePath, 'utf8'); + + // Handle liquid templates if needed + const isLiquidFile = filePath.endsWith('.liquid'); + let liquidExpressions = []; + let processedContent = content; + + if (isLiquidFile) { + const processed = preprocessLiquidSyntax(content); + processedContent = processed.placeholderContent; + liquidExpressions = processed.liquidExpressions; + } + + // Use toml-patch to update the API version + const updates = [ + [['api_version'], latestVersion] + ]; + + let updatedContent = updateTomlValues(processedContent, updates); + + // Restore liquid syntax if needed + if (isLiquidFile) { + updatedContent = restoreLiquidSyntax(updatedContent, liquidExpressions); + } + + await fs.writeFile(filePath, updatedContent, 'utf8'); + console.log(`Updated API version in ${filePath} to ${latestVersion}`); + + } catch (error) { + console.error(`Error updating API version in ${filePath}:`, error.message); + } +} + +// Main method to check and update API versions +async function checkAndUpdateApiVersions() { + const latestVersion = getLatestApiVersion(); + console.log(`Latest API version: ${latestVersion}`); + const extensionFiles = await findAllExtensionFiles(); + console.log(`Found ${extensionFiles.length} extension files to check`); + + for (const filePath of extensionFiles) { + try { + const content = await fs.readFile(filePath, 'utf8'); + const match = content.match(/api_version\s*=\s*"(\d{4}-\d{2})"/); + + if (match) { + const currentVersion = match[1]; + + if (currentVersion !== latestVersion) { + console.log(`Updating ${filePath} from ${currentVersion} to ${latestVersion}`); + await updateApiVersion(filePath, latestVersion); + } else { + console.log(`API version in ${filePath} is already up to date (${currentVersion}).`); + } + } else { + console.warn(`No API version found in ${filePath}`); + } + } catch (error) { + console.error(`Error processing ${filePath}:`, error.message); + } + } +} + +checkAndUpdateApiVersions().catch(error => { + console.error('Error checking and updating API versions:', error); + process.exit(1); +}); \ No newline at end of file diff --git a/util/configure-extension-directories.js b/util/configure-extension-directories.js new file mode 100644 index 00000000..04d55451 --- /dev/null +++ b/util/configure-extension-directories.js @@ -0,0 +1,71 @@ +import fs from 'fs/promises'; +import path from 'path'; +import fastGlob from 'fast-glob'; +import { existsSync } from 'fs'; +import { updateTomlValues } from '@shopify/toml-patch'; + +const ROOT_DIR = '.'; +const FILE_PATTERN = '**/shopify.extension.toml.liquid'; +const EXCLUDED_DIRS = ['samples', 'sample-apps', 'node_modules']; +const OUTPUT_FILE = 'shopify.app.toml'; + +// Method to find all shopify.extension.toml.liquid files excluding specified directories +async function findAllExtensionFiles() { + return fastGlob(FILE_PATTERN, { + cwd: ROOT_DIR, + absolute: true, + ignore: EXCLUDED_DIRS.map(dir => `${dir}/**`) + }); +} + +// Method to read existing shopify.app.toml if it exists +async function readExistingToml() { + try { + if (existsSync(OUTPUT_FILE)) { + return await fs.readFile(OUTPUT_FILE, 'utf8'); + } + return null; + } catch (error) { + console.error(`Error reading ${OUTPUT_FILE}:`, error); + return null; + } +} + +// Main method to update the extension directories in shopify.app.toml +async function configureExtensionDirectories() { + try { + const extensionFiles = await findAllExtensionFiles(); + + // Transform paths to be relative to root and exclude the filenames + const extensionDirectories = extensionFiles.map(filePath => path.relative(ROOT_DIR, path.dirname(filePath))); + + // Remove duplicates + const uniqueDirectories = [...new Set(extensionDirectories)]; + + // Read existing content + const existingContent = await readExistingToml(); + + // Require an existing shopify.app.toml file + if (!existingContent) { + throw new Error(`${OUTPUT_FILE} not found. Please run 'shopify app config link' first to create the file.`); + } + + // Use toml-patch to update the TOML content with extension directories + const updatedContent = updateTomlValues(existingContent, [ + [['extension_directories'], uniqueDirectories], + [['web_directories'], []] + ]); + + // Write the updated content to the file + await fs.writeFile(OUTPUT_FILE, updatedContent, 'utf8'); + console.log(`Updated ${OUTPUT_FILE} with ${uniqueDirectories.length} extension directories`); + } catch (error) { + console.error(`Error updating extension directories:`, error); + throw error; + } +} + +configureExtensionDirectories().catch(error => { + console.error('Error configuring extension directories:', error); + process.exit(1); +}); \ No newline at end of file diff --git a/util/update-schemas.js b/util/update-schemas.js new file mode 100644 index 00000000..6ede3359 --- /dev/null +++ b/util/update-schemas.js @@ -0,0 +1,93 @@ +import fs from 'fs/promises'; +import { exec } from 'child_process'; +import path from 'path'; +import util from 'util'; +import { existsSync } from 'fs'; +import toml from '@iarna/toml'; + +const execPromise = util.promisify(exec); +const APP_TOML_FILE = 'shopify.app.toml'; +const COMMAND_TEMPLATE = 'shopify app function schema'; + +// Method to read shopify.app.toml and extract needed configuration +async function getConfig() { + try { + if (!existsSync(APP_TOML_FILE)) { + throw new Error(`${APP_TOML_FILE} does not exist. Run 'shopify app config link' first to create the file.`); + } + + const content = await fs.readFile(APP_TOML_FILE, 'utf8'); + + // Parse the TOML content + const parsedToml = toml.parse(content); + + const config = { + clientId: '', + directories: [] + }; + + // Extract client_id if it exists + if (parsedToml.client_id) { + config.clientId = parsedToml.client_id; + } + + // Extract extension directories if they exist + if (parsedToml.extension_directories && Array.isArray(parsedToml.extension_directories)) { + // Filter the directories to ensure they exist + config.directories = parsedToml.extension_directories.filter(dir => { + const exists = existsSync(dir); + if (!exists) { + console.warn(`Directory specified in config does not exist: ${dir}`); + } + return exists; + }); + } + + return config; + } catch (error) { + console.error(`Error reading ${APP_TOML_FILE}:`, error); + throw error; + } +} + +// Method to run the schema update command for each directory +async function updateSchemas() { + try { + const config = await getConfig(); + + if (!config.clientId) { + throw new Error('Client ID not found in shopify.app.toml'); + } + + if (config.directories.length === 0) { + console.warn('No valid extension directories found in shopify.app.toml'); + return; + } + + console.log(`Found ${config.directories.length} extension directories`); + console.log(`Using client ID: ${config.clientId}`); + + for (const dir of config.directories) { + try { + const command = `${COMMAND_TEMPLATE} --path ${dir}`; + console.log(`\nUpdating schema for: ${dir}`); + console.log(`Running: ${command}`); + + const { stdout, stderr } = await execPromise(command); + if (stdout) console.log(`Output: ${stdout.trim()}`); + if (stderr && !stderr.includes('warning')) console.error(`Error: ${stderr.trim()}`); + } catch (error) { + console.error(`Failed to update schema for ${dir}:`, error.message); + } + } + + console.log("\nSchema update completed"); + } catch (error) { + console.error('Failed to update schemas:', error); + } +} + +updateSchemas().catch(error => { + console.error('Unhandled error:', error); + process.exit(1); +}); \ No newline at end of file From 301d20662742886353204b916ff8a3632a44507d Mon Sep 17 00:00:00 2001 From: David Cameron Date: Tue, 29 Apr 2025 11:48:34 -0400 Subject: [PATCH 2/2] Rename check-api-version.js to update-api-version.js for clarity - Rename script file to better reflect its actual function - Update package.json script reference --- package.json | 2 +- util/{check-api-version.js => update-api-version.js} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename util/{check-api-version.js => update-api-version.js} (100%) diff --git a/package.json b/package.json index fe0cba00..93db0ebe 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "typegen": "yarn workspaces run graphql-code-generator --config package.json", "test-js": "yarn expand-liquid vanilla-js && yarn && yarn typegen && yarn workspaces run test run", "test-ts": "yarn expand-liquid typescript && yarn && yarn typegen && yarn workspaces run test run", - "update-api-version": "node ./util/check-api-version.js", + "update-api-version": "node ./util/update-api-version.js", "configure-extension-directories": "node ./util/configure-extension-directories.js", "update-schemas": "node ./util/update-schemas.js", "update-all": "yarn update-api-version && yarn configure-extension-directories && yarn expand-liquid && yarn update-schemas" diff --git a/util/check-api-version.js b/util/update-api-version.js similarity index 100% rename from util/check-api-version.js rename to util/update-api-version.js