Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,29 @@ 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: Generate/update the extension directories list in shopify.app.toml
yarn generate-app

# Step 3: Run the comprehensive update command
yarn update-api-version
```

This process:
1. First, links to a Shopify app to create shopify.app.toml with the client ID
2. Then adds all extension directories to the same file (preserving the client_id)
3. Finally, runs a sequence of commands that:
- Updates API versions across all extensions
- Expands liquid templates
- Updates function schemas

### Run Tests

```shell
Expand All @@ -34,3 +57,19 @@ cargo test
cargo fmt
cargo clippy -- -D warnings
```

### Update Dependencies

To check and update JavaScript dependencies in all package.json.liquid files:

```shell
yarn check-js-dependencies
```

To check and update Rust dependencies in all Cargo.toml and Cargo.toml.liquid files:

```shell
yarn check-rust-dependencies
```

These utilities will fetch the latest versions from npm and crates.io respectively and update your templates.
20 changes: 14 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,30 @@
"version": "1.0.0",
"type": "module",
"devDependencies": {
"@iarna/toml": "^2.2.5",
"fast-glob": "^3.2.11",
"liquidjs": "^9.37.0",
"@graphql-codegen/cli": "^3.2.2",
"@graphql-codegen/typescript": "^3.0.2",
"@graphql-codegen/typescript-operations": "^3.0.2",
"graphql": "^16.6.0"
"@iarna/toml": "^2.2.5",
"dayjs": "^1.11.11",
"fast-glob": "^3.2.11",
"graphql": "^16.6.0",
"liquidjs": "^9.37.0"
},
"scripts": {
"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",
"check-api-version": "node ./util/check-api-version.js",
"check-js-dependencies": "node ./util/check-js-dependencies.js",
"check-rust-dependencies": "node ./util/check-rust-dependencies.js",
"check-rust": "yarn check-rust-dependencies && yarn expand-liquid && cargo test",
"generate-app": "node ./util/generate-app.js",
"update-schemas": "node ./util/update-schemas.js",
"update-api-version": "yarn check-api-version && yarn expand-liquid && yarn update-schemas"
},
"private": true,
"workspaces": [
"*/javascript/**"
]
}
}
60 changes: 60 additions & 0 deletions util/check-api-version.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import fs from 'fs/promises';
import fastGlob from 'fast-glob';
import dayjs from 'dayjs';

const ROOT_DIR = '.';
const FILE_PATTERN = '**/shopify.extension.toml.liquid';

// 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 });
}

// Method to update the API version in the file
async function updateApiVersion(filePath, latestVersion) {
const content = await fs.readFile(filePath, 'utf8');
const updatedContent = content.replace(/api_version\s*=\s*"\d{4}-\d{2}"/, `api_version = "${latestVersion}"`);

await fs.writeFile(filePath, updatedContent, 'utf8');
console.log(`Updated API version in ${filePath}`);
}

// Main method to check and update API versions
async function checkAndUpdateApiVersions() {
const latestVersion = getLatestApiVersion();
const extensionFiles = await findAllExtensionFiles();

for (const filePath of extensionFiles) {
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) {
await updateApiVersion(filePath, latestVersion);
} else {
console.log(`API version in ${filePath} is already up to date.`);
}
} else {
console.warn(`No API version found in ${filePath}`);
}
}
}

checkAndUpdateApiVersions().catch(error => {
console.error('Error checking and updating API versions:', error);
process.exit(1);
});
58 changes: 58 additions & 0 deletions util/check-js-dependencies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import fs from 'fs/promises';
import path from 'path';
import fastGlob from 'fast-glob';
import { execSync } from 'child_process';

const ROOT_DIR = '.';
const FILE_PATTERN = '**/package.json.liquid';

async function findAllPackageFiles() {
return fastGlob(FILE_PATTERN, { cwd: ROOT_DIR, absolute: true });
}

async function getLatestVersion(packageName) {
try {
// Fetch the latest version of a package from the npm registry
const output = execSync(`npm show ${packageName} version`, { encoding: 'utf8' });
return output.trim();
} catch (error) {
console.warn(`Could not fetch version for package ${packageName}:`, error.message);
return null;
}
}

async function checkAndUpdateDependencies(filePath) {
const content = await fs.readFile(filePath, 'utf8');
const jsonContent = JSON.parse(content);

const { dependencies = {}, devDependencies = {} } = jsonContent;

const updateDependencyVersion = async (dependencies) => {
for (const [name, currentVersion] of Object.entries(dependencies)) {
const latestVersion = await getLatestVersion(name);
Copy link
Contributor

Choose a reason for hiding this comment

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

How can we ensure that it's safe to update to the latest version? For example, if there's a major version bump. Are we relying on tests to tell us that something is wrong?

if (latestVersion && currentVersion !== `^${latestVersion}`) {
console.log(`Updating ${name} from ${currentVersion} to ^${latestVersion}`);
dependencies[name] = `^${latestVersion}`;
}
}
};

await updateDependencyVersion(dependencies);
await updateDependencyVersion(devDependencies);

const updatedContent = JSON.stringify(jsonContent, null, 2);
await fs.writeFile(filePath, updatedContent, 'utf8');
console.log(`Updated dependencies in ${filePath}`);
}

async function main() {
const packageFiles = await findAllPackageFiles();
for (const filePath of packageFiles) {
await checkAndUpdateDependencies(filePath);
}
}

main().catch(error => {
console.error('Error checking and updating dependencies:', error);
process.exit(1);
});
106 changes: 106 additions & 0 deletions util/check-rust-dependencies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import fs from 'fs/promises';
import fastGlob from 'fast-glob';
import toml from '@iarna/toml';

const ROOT_DIR = '.';
const FILE_PATTERNS = ['**/Cargo.toml', '**/Cargo.toml.liquid'];
const LIQUID_PLACEHOLDER = 'LIQUID_PLACEHOLDER';

async function findAllCargoFiles() {
return fastGlob(FILE_PATTERNS, { cwd: ROOT_DIR, absolute: true });
}

async function getLatestVersion(packageName) {
try {
// Fetch the latest version of a package from crates.io
const response = await fetch(`https://crates.io/api/v1/crates/${packageName}`);
const jsonResponse = await response.json();
return jsonResponse.crate.max_version;
} catch (error) {
console.warn(`Could not fetch version for package ${packageName}:`, error.message);
return null;
}
}

async function updateDependencyVersion(name, currentVersion) {
const latestVersion = await getLatestVersion(name);

if (latestVersion) {
if (typeof currentVersion === 'string') {
if (!currentVersion.includes(latestVersion)) {
console.log(`Updating ${name} from ${currentVersion} to ${latestVersion}`);
return latestVersion;
}
} else if (typeof currentVersion === 'object' && 'version' in currentVersion) {
if (!currentVersion.version.includes(latestVersion)) {
console.log(`Updating ${name} from ${currentVersion.version} to ${latestVersion}`);
return { ...currentVersion, version: latestVersion };
}
}
}
return currentVersion;
}

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 restoreLiquidSyntax(content, liquidExpressions) {
return content.replace(new RegExp(`\\{${LIQUID_PLACEHOLDER}:(\\d+)\\}`, 'g'), (match, index) => {
return liquidExpressions[Number(index)];
});
}

async function checkAndUpdateDependencies(filePath) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of doing this, can't we transform the Liquid files to TOML files, run cargo update, and then map back to Liquid (if applicable, like what you did in this file)?

Similarly, could we do that for JS too?

let content = await fs.readFile(filePath, 'utf8');

const isLiquidFile = filePath.endsWith('.liquid');
let liquidExpressions = [];

if (isLiquidFile) {
const processed = preprocessLiquidSyntax(content);
content = processed.placeholderContent;
liquidExpressions = processed.liquidExpressions;
}

let tomlData;
try {
tomlData = toml.parse(content);
} catch (error) {
console.error(`Failed to parse TOML in file: ${filePath}`, error.message);
return;
}

if (tomlData.dependencies) {
const dependencyNames = Object.keys(tomlData.dependencies);
for (const name of dependencyNames) {
const currentVersion = tomlData.dependencies[name];
tomlData.dependencies[name] = await updateDependencyVersion(name, currentVersion);
}
}

let updatedContent = toml.stringify(tomlData);
if (isLiquidFile) {
updatedContent = restoreLiquidSyntax(updatedContent, liquidExpressions);
}

await fs.writeFile(filePath, updatedContent, 'utf8');
console.log(`Updated dependencies in ${filePath}`);
}

async function main() {
const cargoFiles = await findAllCargoFiles();
for (const filePath of cargoFiles) {
await checkAndUpdateDependencies(filePath);
}
}

main().catch(error => {
console.error('Error checking and updating dependencies:', error);
process.exit(1);
});
33 changes: 26 additions & 7 deletions util/expand-liquid.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ async function directoryNames(parentPath) {
async function expandExtensionLiquidTemplates(domainName, flavor) {
console.log(`Expanding liquid templates for ${domainName}`);
const domainPath = path.join(process.cwd(), domainName);
let handleCounter = 1; // Counter for potentially truncated handles within this domain

const langNames = await directoryNames(domainPath);
for (const langName of langNames) {
Expand All @@ -79,18 +80,36 @@ async function expandExtensionLiquidTemplates(domainName, flavor) {
await (await glob(path.join(templatePath, 'src', '!(*.liquid|*.graphql)'))).forEach(async (path) => await fs.rm(path));
}

let baseHandle = `${langName}-${domainName}-${extensionTypeName}-${templateName}`;
let handle;

if (baseHandle.length > 30) {
// Truncate to leave space for a counter suffix (e.g., '-99')
const maxBaseLength = 27;
// Remove trailing hyphen if present after slicing, then add counter
handle = `${baseHandle.slice(0, maxBaseLength).replace(/-$/, '')}-${handleCounter}`;
// Ensure final handle does not exceed 30 characters (safeguard)
if (handle.length > 30) {
handle = handle.slice(0, 30);
}
handleCounter++; // Increment counter only when a handle is truncated
} else {
// Use the base handle if it's within the length limit
handle = baseHandle;
}

const liquidData = {
name: `${domainName}-${extensionTypeName}-${templateName}`,
handle: `${domainName}-${extensionTypeName}-${templateName}`,
name: `${langName}-${domainName}-${extensionTypeName}-${templateName}`, // Keep original descriptive name
handle, // Use the potentially modified handle
flavor,
};

await expandLiquidTemplates(templatePath, liquidData);

if (langName === "javascript") {
const srcFilePaths = await glob(path.join(templatePath, 'src', '!(*.liquid|*.graphql)'))
const srcFileExtensionsToChange = []

const srcFilePaths = await glob(path.join(templatePath, 'src', '!(*.liquid|*.graphql)'));
const srcFileExtensionsToChange = [];
const fileExtension = flavor === "typescript" ? "ts" : "js";

for (const srcFilePath of srcFilePaths) {
Expand All @@ -99,7 +118,7 @@ async function expandExtensionLiquidTemplates(domainName, flavor) {
}));
}

await Promise.all(srcFileExtensionsToChange)
await Promise.all(srcFileExtensionsToChange);
}
}
}
Expand Down Expand Up @@ -141,4 +160,4 @@ console.log('The above files should be added to .gitignore if they have not alre

if (process.env.CI) {
ensureNoGitChanges();
}
}
Loading