Skip to content

Add API version update utilities #613

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ sample-apps/*/public/build/*
!sample-apps/**/shopify.extension.toml
!sample-apps/**/locales/*.json
dev.sqlite
shopify.app.toml
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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/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"
},
"private": true,
"workspaces": [
Expand Down
71 changes: 71 additions & 0 deletions util/configure-extension-directories.js
Original file line number Diff line number Diff line change
@@ -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);
});
114 changes: 114 additions & 0 deletions util/update-api-version.js
Original file line number Diff line number Diff line change
@@ -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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we only want to update the versions for Liquid files? This would exclude the sample apps.

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();
Comment on lines +12 to +14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use JS dates directly instead of bringing in a new dependency.

Suggested change
const date = dayjs();
const year = date.year();
const month = date.month();
const date = new Date();
const year = date.getFullYear();
const month = date.getMonth();

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like some functions run into this error because the liquid placeholder logic doesn't create a valid TOML file. For example order-routing/rust/location-rules/default/shopify.extension.toml.liquid:

{% if uid %}uid = "{{ uid }}"{% endif %}

Maybe we need a different approach that doesn't really expect a valid TOML file. Perhaps a simple find/replace would be sufficient here (assuming nobody does any Liquid shenanigans with the API version).

}
}

// 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);
});
93 changes: 93 additions & 0 deletions util/update-schemas.js
Original file line number Diff line number Diff line change
@@ -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);
});