Skip to content
This repository was archived by the owner on Mar 4, 2025. It is now read-only.
Closed
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
21 changes: 15 additions & 6 deletions src/commands/theme/component/copy.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
/**
* This command copies component files into a theme directory.
*
*
* - Copies rendered component files (snippets and assets) into the theme directory
* - Updates the theme CLI config (shopify.theme.json) with the component collection details
*/

import fs from 'node:fs'
import path from 'node:path'

import Args from '../../../utilities/args.js'
import Args from '../../../utilities/args.js'
import BaseCommand from '../../../utilities/base-command.js'
import { copyFileIfChanged } from '../../../utilities/files.js';
import { copyFileIfChanged } from '../../../utilities/files.js'
import Flags from '../../../utilities/flags.js'
import { getManifest } from '../../../utilities/manifest.js'
import { getCollectionNodes } from '../../../utilities/nodes.js'
import { getNameFromPackageJson , getVersionFromPackageJson } from '../../../utilities/package-json.js'
import { getCopyrightConfigFromPackageJson, getNameFromPackageJson, getVersionFromPackageJson } from '../../../utilities/package-json.js'
import { CopyrightOptions } from '../../../utilities/types.js'

export default class Copy extends BaseCommand {
static override args = Args.getDefinitions([
Expand Down Expand Up @@ -49,25 +50,33 @@ export default class Copy extends BaseCommand {
const collectionName = this.flags[Flags.COLLECTION_NAME] || getNameFromPackageJson(process.cwd())
const collectionVersion = this.flags[Flags.COLLECTION_VERSION] || getVersionFromPackageJson(process.cwd())

const copyright = getCopyrightConfigFromPackageJson(process.cwd());

if (!fs.existsSync(path.join(themeDir, 'component.manifest.json'))) {
this.error('Error: component.manifest.json file not found in the theme directory. Run "shopify theme component map" to generate a component.manifest.json file.');
}

const manifest = getManifest(path.join(themeDir, 'component.manifest.json'))
const componentNodes = await getCollectionNodes(currentDir)

if (manifest.collections[collectionName].version !== collectionVersion) {
this.error(`Version mismatch: Expected ${collectionVersion} but found ${manifest.collections[collectionName].version}. Run "shopify theme component map" to update the component.manifest.json file.`);
}

const copyOptions: CopyrightOptions = {
collectionName,
collectionVersion,
copyright
};

const copyManifestFiles = (fileType: 'assets' | 'snippets') => {
for (const [fileName, fileCollection] of Object.entries(manifest.files[fileType])) {
if (fileCollection === collectionName) {
const node = componentNodes.find(node => node.name === fileName && node.themeFolder === fileType);
if (node) {
const src = node.file;
const dest = path.join(themeDir, fileType, fileName);
copyFileIfChanged(src, dest);
copyFileIfChanged(src, dest, copyOptions);
}
}
}
Expand Down
74 changes: 74 additions & 0 deletions src/utilities/copyright.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import path from 'node:path'

import { CopyrightOptions } from './types.js'

export const COPYRIGHT_TEMPLATES: Record<string, (message: string) => string> = {
css: (message: string) => `/* ${message} */\n\n`,
js: (message: string) => `// ${message}\n\n`,
liquid: (message: string) => `{% # ${message} %}\n\n`,
svg: (message: string) => `<!-- ${message} -->\n\n`
}

export function generateCopyrightMessage(
collectionName: string,
collectionVersion: string,
copyright?: CopyrightOptions['copyright']
): string {
const year = new Date().getFullYear();
const author = copyright?.author || '';
const license = copyright?.license || '';

return `${collectionName} v${collectionVersion} | Copyright © ${year}` +
(author && ` ${author}`) +
(license && ` | ${license}`);
}

export function shouldAddCopyright(filePath: string): boolean {
// Skip vendor files
if (path.basename(filePath).startsWith('vendor.')) {
return false;
}

const fileExtension = path.extname(filePath).toLowerCase().slice(1);

// Only add copyright to supported file types
return Object.keys(COPYRIGHT_TEMPLATES).includes(fileExtension);
}

export function addCopyrightComment(
content: string,
filePath: string,
options: {
collectionName: string;
collectionVersion: string;
} & Partial<CopyrightOptions>
): string {
if (!shouldAddCopyright(filePath)) {
return content;
}

const { collectionName, collectionVersion, copyright } = options;
const fileExtension = path.extname(filePath).toLowerCase().slice(1);

// Check if the copyright comment is already present
if (content.includes(`${collectionName} v${collectionVersion}`)) {
return content;
}

const copyrightMessage = generateCopyrightMessage(
collectionName,
collectionVersion,
copyright
);

const commentWrapper = COPYRIGHT_TEMPLATES[fileExtension];
const copyrightComment = commentWrapper(copyrightMessage);

// For SVG files, we need to insert the comment after the XML declaration if present
if (fileExtension === 'svg' && content.startsWith('<?xml')) {
const xmlEndIndex = content.indexOf('?>') + 2;
return content.slice(0, xmlEndIndex) + '\n' + copyrightComment + content.slice(xmlEndIndex);
}

return copyrightComment + content;
}
24 changes: 20 additions & 4 deletions src/utilities/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
import fs from 'fs-extra';
import path from 'node:path'

import { addCopyrightComment } from './copyright.js'
import logger from './logger.js'
import { CopyrightOptions } from './types.js'

export function syncFiles(srcDir: string, destDir: string) {
if (!fs.existsSync(srcDir)) {
Expand Down Expand Up @@ -32,7 +34,7 @@ export function syncFiles(srcDir: string, destDir: string) {
}
}
}

// Copy each file/directory from source
for (const file of srcFiles) {
if (file.startsWith('.')) continue;
Expand Down Expand Up @@ -62,14 +64,28 @@ export function syncFiles(srcDir: string, destDir: string) {
}
}

export function copyFileIfChanged(src: string, dest: string) {
export function copyFileIfChanged(
src: string,
dest: string,
options?: CopyrightOptions
) {
const destDir = path.dirname(dest);
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
}

if (!fs.existsSync(dest) || fs.readFileSync(src, 'utf8') !== fs.readFileSync(dest, 'utf8')) {
fs.copyFileSync(src, dest);
let content = fs.readFileSync(src, 'utf8');

if (options?.collectionName && options?.collectionVersion) {
content = addCopyrightComment(content, src, {
collectionName: options.collectionName,
collectionVersion: options.collectionVersion,
copyright: options.copyright
});
}

fs.writeFileSync(dest, content);
}
}

Expand All @@ -86,7 +102,7 @@ export function writeFileIfChanged(content: string, dest: string) {

export function cleanDir(dir: string) {
if (fs.existsSync(dir)) {
fs.rmSync(dir, {recursive: true})
fs.rmSync(dir, { recursive: true })
}

fs.mkdirSync(dir)
Expand Down
4 changes: 2 additions & 2 deletions src/utilities/flags.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Flags as OclifFlags} from '@oclif/core'
import { Flags as OclifFlags } from '@oclif/core'
import { FlagInput } from '@oclif/core/interfaces';

import { ComponentConfig } from './types.js'
Expand Down Expand Up @@ -26,7 +26,7 @@ export default class Flags {
static readonly THEME = 'theme';
static readonly THEME_DIR = 'theme-dir';
static readonly WATCH = 'watch';

private flagValues: Record<string, FlagInput<object>>;
constructor(flags: Record<string, FlagInput<object>>) {
this.flagValues = flags
Expand Down
54 changes: 43 additions & 11 deletions src/utilities/package-json.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,56 @@
import fs from "node:fs";
import path from "node:path";

export function getNameFromPackageJson(dir: string): string|undefined {
const pkgPath = path.join(dir, 'package.json');
import { CopyrightConfig, PackageJSON } from "./types.js";

export function getNameFromPackageJson(dir: string): string | undefined {
const packagePath = path.join(dir, 'package.json');
let name;
if (fs.existsSync(pkgPath)) {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
name = pkg.name;

if (fs.existsSync(packagePath)) {
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
name = packageJson.name;
}

return name;
}

export function getVersionFromPackageJson(dir: string): string|undefined {
const pkgPath = path.join(dir, 'package.json');
export function getVersionFromPackageJson(dir: string): string | undefined {
const packagePath = path.join(dir, 'package.json');
let version;
if (fs.existsSync(pkgPath)) {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
version = pkg.version;

if (fs.existsSync(packagePath)) {
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
version = packageJson.version;
}

return version;
}
}

export function getCopyrightConfigFromPackageJson(dir: string): CopyrightConfig {
const packagePath = path.join(dir, 'package.json');

if (!fs.existsSync(packagePath)) {
return {};
}

try {
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')) as PackageJSON;
const config: CopyrightConfig = {};

if (packageJson.author) {
config.author = typeof packageJson.author === 'string'
? packageJson.author
: packageJson.author.name;
}

if (packageJson.license) {
config.license = packageJson.license;
}

return config;
} catch (error) {
console.error(`Failed to parse package.json for copyright config: ${error}`);
return {};
}
}
8 changes: 4 additions & 4 deletions src/utilities/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { DeepObject, deepMerge } from './objects.js'
import { LiquidNode } from './types.js'

export async function copySetupComponentFiles(
collectionDir: string,
destination: string,
collectionDir: string,
destination: string,
componentSelector: string
): Promise<void> {
const collectionNodes = await getCollectionNodes(collectionDir)
Expand Down Expand Up @@ -37,7 +37,7 @@ export async function copySetupComponentFiles(

// Write combined settings schema
writeFileIfChanged(
JSON.stringify(settingsSchema),
JSON.stringify(settingsSchema),
path.join(destination, 'config', 'settings_schema.json')
)

Expand All @@ -49,7 +49,7 @@ export async function copySetupComponentFiles(
}

export async function processSettingsSchema(
setupFile: string,
setupFile: string,
node: LiquidNode
): Promise<object[]> {
if (node?.name !== 'settings_schema.json') {
Expand Down
17 changes: 15 additions & 2 deletions src/utilities/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface TomlConfig {

export interface LiquidNode {
assets: string[]
body: string
body: string
file: string
name: string
setup: string[]
Expand All @@ -27,6 +27,8 @@ export interface LiquidNode {
}

export interface PackageJSON {
author?: { name: string } | string;
license?: string;
name: string;
repository: string;
version: string;
Expand All @@ -44,4 +46,15 @@ export interface Manifest {
[name: string]: string;
};
}
}
}

export interface CopyrightConfig {
author?: string;
license?: string;
}

export interface CopyrightOptions {
collectionName?: string;
collectionVersion?: string;
copyright?: CopyrightConfig;
}
Loading