diff --git a/.gitignore b/.gitignore index 8a380aa..f676b42 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,14 @@ node_modules .vscode-test/ *.vsix .env +dist/ +build/ +.DS_Store +coverage +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnp.* +.cache/ +.idea/ +*.iml \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index ea25f54..fb4181b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,17 @@ All notable changes to the "responsive-image-generator" extension will be documented in this file. -Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. +## [Unreleased] - 2025-10-26 -## [Unreleased] +- Added TODO list for future enhancements and features. +- Refactored code for better maintainability. +- Improved documentation and comments throughout the codebase. +- Added icons for the extension in both light and dark themes. +- Added editor title menu option for generating responsive images from the editor view. +- Added explorer context menu option for generating responsive images from image files. -- Initial release \ No newline at end of file +## [1.0.0] - 2025-09-14 + +- Added command for responsive image generation. +- Added support for snippet insertion. +- Added settings options for relative paths and public root folder. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..7aaa2f6 --- /dev/null +++ b/TODO.md @@ -0,0 +1,7 @@ +# TODO + +- [x] Add right-click context menu option to generate responsive images from selected image files in the explorer. +- [x] Add editor title menu option to generate responsive images from editor view. +- [ ] Implement an editor view to preview generated responsive images before insertion and manage settings. +- [ ] Add unit tests for core functionalities like image processing and prompt handling. +- [ ] Improve error handling and user feedback for scenarios like unsupported file types or processing failures. diff --git a/config/eslint.config.mjs b/config/eslint.config.mjs deleted file mode 100644 index c893c8a..0000000 --- a/config/eslint.config.mjs +++ /dev/null @@ -1,25 +0,0 @@ -import globals from "globals"; - -export default [{ - files: ["*.js"], - languageOptions: { - globals: { - ...globals.commonjs, - ...globals.node, - ...globals.mocha, - }, - - ecmaVersion: 2022, - sourceType: "module", - }, - - rules: { - "no-const-assign": "warn", - "no-this-before-super": "warn", - "no-undef": "warn", - "no-unreachable": "warn", - "no-unused-vars": "warn", - "constructor-super": "warn", - "valid-typeof": "warn", - }, -}]; \ No newline at end of file diff --git a/config/jsconfig.json b/config/jsconfig.json deleted file mode 100644 index 508e58d..0000000 --- a/config/jsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "module": "Node16", - "target": "ES2022", - "checkJs": true, /* Typecheck .js files. */ - "lib": [ - "ES2022" - ] - }, - "exclude": [ - "node_modules" - ] -} diff --git a/eslint.config.mts b/eslint.config.mts new file mode 100644 index 0000000..7cdab82 --- /dev/null +++ b/eslint.config.mts @@ -0,0 +1,10 @@ +import js from "@eslint/js"; +import globals from "globals"; +import tseslint from "typescript-eslint"; +import { defineConfig } from "eslint/config"; + +export default defineConfig([ + { files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.browser } }, + { files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } }, + tseslint.configs.recommended, +]); diff --git a/extension.js b/extension.js deleted file mode 100644 index a39abc2..0000000 --- a/extension.js +++ /dev/null @@ -1,273 +0,0 @@ -// The module 'vscode' contains the VS Code extensibility API -// Import the module and reference it with the alias vscode in your code below -const vscode = require('vscode'); -const sharp = require('sharp'); -const fs = require('fs'); -const path = require('path'); - -const config = vscode.workspace.getConfiguration('responsiveImageGenerator'); - -// This method is called when your extension is activated -// Your extension is activated the very first time the command is executed - -/** - * @param {vscode.ExtensionContext} context - */ - - -/** - * Activates the Responsive Image Generator extension. - * Registers commands and completion providers. - * @param {vscode.ExtensionContext} context - The extension context provided by VS Code. - */ -function activate(context) { - const extensionPackageJson = require(path.join(context.extensionPath, 'package.json')); - - // Register the main command for generating responsive images - const disposable = vscode.commands.registerCommand('responsive-image-generator.generate', async function () { - try { - const result = await promptForAllInputs(); - if (!result) return; - const { imageUris, outputDir, sizesToGenerate } = result; - - // Show progress while processing images - await vscode.window.withProgress({ - location: vscode.ProgressLocation.Notification, - title: 'Generating responsive images...', - cancellable: false - }, async (progress) => { - progress.report({ message: 'Processing images...' }); - // Process all images and sizes in parallel - await Promise.all( - imageUris.map(imageUri => { - const itemName = path.basename(imageUri.fsPath, path.extname(imageUri.fsPath)); - return processImage(imageUri.fsPath, outputDir, itemName, sizesToGenerate); - }) - ); - }); - - vscode.window.showInformationMessage('Responsive images generated successfully!'); - } catch (err) { - vscode.window.showErrorMessage(`Error: ${err.message}`); - console.error(err); - } - }); - context.subscriptions.push(disposable); - - // Supported languages for completion provider - // Dynamically fetch supported languages from package.json activationEvents - let supportedLanguages = []; - if (extensionPackageJson.activationEvents) { - supportedLanguages = extensionPackageJson.activationEvents - .filter(event => event.startsWith('onLanguage:')) - .map(event => event.replace('onLanguage:', '')); - } - - const searchWord = 'responsive'; - const triggerCharacters = ['<', '>', '!']; - - // Register completion provider for responsive image tag - const provider = vscode.languages.registerCompletionItemProvider( - supportedLanguages, - { - /** - * Provides completion items for responsive image tag. - * @param {vscode.TextDocument} document - * @param {vscode.Position} position - * @returns {vscode.CompletionItem[]|undefined} - */ - provideCompletionItems(document, position) { - const linePrefix = document.lineAt(position).text.split(new RegExp(`[${triggerCharacters.join('')}]`)).at(1)?.trim() || ''; - if (searchWord.includes(linePrefix) && linePrefix.length > 0) { - const completion = new vscode.CompletionItem('responsive_image_basic', vscode.CompletionItemKind.Snippet); - completion.command = { - command: 'responsive-image-generator.fillResponsiveTag', - title: 'Fill Responsive Image Tag', - arguments: [document, position.translate(0, -(linePrefix.length + 1))] - }; - return [completion]; - } - return undefined; - } - }, - ...triggerCharacters - ); - context.subscriptions.push(provider); - - // Register command to fill responsive tag after completion is selected - context.subscriptions.push(vscode.commands.registerCommand('responsive-image-generator.fillResponsiveTag', async (document, triggerStart) => { - const result = await promptForAllInputs(); - if (!result) return; - const { imageUris, outputDir, sizesToGenerate } = result; - - // Generate srcset string - let srcsetParts = []; - for (const imageUri of imageUris) { - const itemName = path.basename(imageUri.fsPath, path.extname(imageUri.fsPath)); - for (const size of sizesToGenerate) { - const outputFile = path.join(outputDir, `${itemName}_${size}${path.extname(imageUri.fsPath)}`); - try { - // Resize and save image - await sharp(imageUri.fsPath) - .resize(size) - .toFile(outputFile); - // Use relative paths if configured - if(config.get('useRelativePaths')) { - const root = config.get('staticAssetsRoot') || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; - const relativeOutputFile = root ? `./${path.relative(root, outputFile)}` : outputFile; - srcsetParts.push(`${relativeOutputFile} ${size}w`); - } else { - srcsetParts.push(`${outputFile} ${size}w`); - } - } catch (err) { - vscode.window.showErrorMessage(`Error processing ${imageUri.fsPath} for size ${size}: ${err.message}`); - } - } - } - const srcset = srcsetParts.join(', '); - - // Generate sizes attribute - const sizes = sizesToGenerate.map(size => `(max-width: ${size}px) ${size}px`).join(', ') + ', 100vw'; - - // Insert finished snippet, replacing the prefix - const editor = vscode.window.activeTextEditor; - if (editor) { - const snippet = new vscode.SnippetString(`$1`); - const endPosition = triggerStart.translate(0, document.lineAt(triggerStart).text.length - triggerStart.character); - editor.edit(editBuilder => { - editBuilder.delete(new vscode.Range(triggerStart, endPosition)); - }).then(() => { - editor.insertSnippet(snippet, triggerStart); - }); - } - vscode.window.showInformationMessage('Add responsive image tag and generated images!'); - })); -} - - - - -/** - * Prompts the user for all required inputs: image files, output directory, and sizes. - * Handles duplicate logic and error messaging for missing selections. - * @returns {Promise<{imageUris: vscode.Uri[], outputDir: string, sizesToGenerate: number[]} | undefined>} Object with all inputs, or undefined if cancelled. - */ -async function promptForAllInputs() { - // Prompt for image file(s) - const imageUris = await promptForImageFiles(); - if (!imageUris || imageUris.length === 0) { - vscode.window.showWarningMessage('No image selected. Operation cancelled.'); - return undefined; - } - - // Prompt for output directory - const outputDir = await promptForOutputDirectory(); - if (!outputDir) { - vscode.window.showWarningMessage('No output directory selected. Operation cancelled.'); - return undefined; - } - - // Prompt for sizes - const sizesToGenerate = await promptForSizes(); - if (!sizesToGenerate || sizesToGenerate.length === 0) { - vscode.window.showWarningMessage('No sizes selected. Operation cancelled.'); - return undefined; - } - - // Create output directory if it doesn't exist - if (!fs.existsSync(outputDir)) { - fs.mkdirSync(outputDir, { recursive: true }); - } - - return { imageUris, outputDir, sizesToGenerate }; -} - -/** - * Prompts the user to select image files. - * @returns {Promise} Array of selected image URIs or undefined if cancelled. - */ -async function promptForImageFiles() { - return await vscode.window.showOpenDialog({ - canSelectMany: true, - openLabel: 'Select Image(s)', - filters: { - 'Images': ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg'] - } - }); -} - -/** - * Prompts the user to select an output directory, preferring 'wwwroot' or 'public' if available. - * @returns {Promise} Path to selected output directory or undefined if cancelled. - */ -async function promptForOutputDirectory() { - const workspaceFolders = vscode.workspace.workspaceFolders || []; - let staticContentFolder = workspaceFolders.find(folder => (folder.name.toLowerCase() === 'wwwroot') || (folder.name.toLowerCase() === 'public')); - let preselectUri = staticContentFolder ? staticContentFolder.uri : undefined; - - const folder = await vscode.window.showWorkspaceFolderPick({ - placeHolder: "Select output folder ('wwwroot' or 'public' will be preselected if available)", - }); - if (folder) { - return folder.uri.fsPath; - } else if (preselectUri) { - return preselectUri.fsPath; - } else { - return undefined; - } -} - -/** - * Prompts the user to select image sizes to generate. - * @returns {Promise} Array of selected sizes or undefined if cancelled. - */ -async function promptForSizes() { - const sizes = config.get('defaultSizes') || [320, 480, 768, 1024, 1280, 1600, 1920, 2560, 3840, 5120, 7680]; - const selectedSizes = await vscode.window.showQuickPick(sizes.map(size => size.toString()), { - placeHolder: 'Select sizes to generate (you can select multiple)', - canPickMany: true - }); - return selectedSizes ? selectedSizes.map(size => parseInt(size)) : undefined; -} - - -/** - * Processes an image: resizes and saves to output directory with item name and size. - * @param {string} imagePath - Path to the source image file. - * @param {string} outputDir - Directory to save resized images. - * @param {string} itemName - Base name for output files. - * @param {number[]} sizesToGenerate - Array of sizes to generate. - * @returns {Promise} - */ -async function processImage(imagePath, outputDir, itemName, sizesToGenerate) { - await Promise.all( - sizesToGenerate.map(async (size) => { - const outputFile = path.join( - outputDir, - `${itemName}_${size}${path.extname(imagePath)}` - ); - try { - await sharp(imagePath) - .resize(size) - .toFile(outputFile); - console.log(`Generated ${outputFile}`); - } catch (err) { - vscode.window.showErrorMessage(`Error processing ${imagePath} for size ${size}: ${err.message}`); - } - }) - ); -} - -// This method is called when your extension is deactivated - - -/** - * Deactivates the extension. - * Called when the extension is deactivated. - */ -function deactivate() { } - -module.exports = { - activate, - deactivate -} \ No newline at end of file diff --git a/media/icon-dark.png b/media/icon-dark.png new file mode 100644 index 0000000..882b4f2 Binary files /dev/null and b/media/icon-dark.png differ diff --git a/media/icon-dark.svg b/media/icon-dark.svg new file mode 100644 index 0000000..37cb0ad --- /dev/null +++ b/media/icon-dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/media/icon-light.png b/media/icon-light.png new file mode 100644 index 0000000..dbedcb1 Binary files /dev/null and b/media/icon-light.png differ diff --git a/media/icon-light.svg b/media/icon-light.svg new file mode 100644 index 0000000..782ea36 --- /dev/null +++ b/media/icon-light.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/package-lock.json b/package-lock.json index 1cba53e..ee4cc12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,29 @@ { "name": "responsive-image-generator", - "version": "0.0.1", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "responsive-image-generator", - "version": "0.0.1", + "version": "1.0.0", "dependencies": { - "sharp": "^0.34.3" + "sharp": "^0.34.3", + "typescript": "^5.9.2" }, "devDependencies": { + "@eslint/js": "^9.38.0", "@types/mocha": "^10.0.10", "@types/node": "22.x", "@types/vscode": "^1.104.0", + "@typescript-eslint/eslint-plugin": "^8.46.2", + "@typescript-eslint/parser": "^8.46.2", "@vscode/test-cli": "^0.0.11", "@vscode/test-electron": "^2.5.2", - "eslint": "^9.34.0" + "eslint": "^9.38.0", + "globals": "^16.4.0", + "jiti": "^2.6.1", + "typescript-eslint": "^8.46.2" }, "engines": { "vscode": "^1.104.0" @@ -82,13 +89,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -121,19 +128,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", - "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz", + "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -178,6 +188,19 @@ "concat-map": "0.0.1" } }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -192,9 +215,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.35.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz", - "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==", + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", + "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", "dev": true, "license": "MIT", "engines": { @@ -205,9 +228,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -215,13 +238,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.2", + "@eslint/core": "^0.16.0", "levn": "^0.4.1" }, "engines": { @@ -754,6 +777,44 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -810,6 +871,238 @@ "dev": true, "license": "MIT" }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", + "integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/type-utils": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.2", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz", + "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz", + "integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.2", + "@typescript-eslint/types": "^8.46.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz", + "integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz", + "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz", + "integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz", + "integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz", + "integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.2", + "@typescript-eslint/tsconfig-utils": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz", + "integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz", + "integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@vscode/test-cli": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.11.tgz", @@ -1398,25 +1691,24 @@ } }, "node_modules/eslint": { - "version": "9.35.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz", - "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", + "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.1", - "@eslint/core": "^0.15.2", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.1", + "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.35.0", - "@eslint/plugin-kit": "^0.3.5", + "@eslint/js": "9.38.0", + "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", @@ -1596,6 +1888,23 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -1610,6 +1919,16 @@ "dev": true, "license": "MIT" }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -1781,9 +2100,9 @@ } }, "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", "dev": true, "license": "MIT", "engines": { @@ -1800,6 +2119,13 @@ "dev": true, "license": "ISC" }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2098,6 +2424,16 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2242,6 +2578,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -2689,6 +3049,27 @@ "node": ">=6" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -2765,6 +3146,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -3129,6 +3545,19 @@ "node": ">=8.0" } }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -3149,6 +3578,43 @@ "node": ">= 0.8.0" } }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.2.tgz", + "integrity": "sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.46.2", + "@typescript-eslint/parser": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/utils": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", diff --git a/package.json b/package.json index 9a6e5f7..8ca042f 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,9 @@ "name": "responsive-image-generator", "displayName": "Responsive Image Generator", "description": "Generate responsive images in multiple sizes for web development. Works inline in the text editor.", - "version": "1.0.0", + "version": "1.1.0", "publisher": "coltonmcgraw", + "icon": "media/icon-light.png", "repository": { "type": "git", "url": "https://github.com/ColtMcG0/responsive-image-generator.vs-code" @@ -25,22 +26,57 @@ "onLanguage:csharp", "onLanguage:cshtml" ], - "main": "./src/extension.js", + "main": "./dist/extension.js", "contributes": { "commands": [ { "command": "responsive-image-generator.generate", - "title": "Generate Responsive Image" + "title": "Generate Responsive Image", + "category": "Responsive Image", + "icon": { + "light": "media/icon-dark.svg", + "dark": "media/icon-light.svg" + } } ], + "menus": { + "explorer/context": [ + { + "command": "responsive-image-generator.generate", + "when": "resourceExtname =~ /\\.(png|jpg|jpeg|gif|bmp|webp|svg)$/i", + "group": "7_modification" + } + ], + "editor/title": [ + { + "command": "responsive-image-generator.generate", + "when": "resourceExtname =~ /\\.(png|jpg|jpeg|gif|bmp|webp|svg)$/i", + "group": "navigation" + } + ] + }, "configuration": { "type": "object", "title": "Responsive Image Generator", "properties": { "responsiveImageGenerator.defaultSizes": { "type": "array", - "items": { "type": "number" }, - "default": [320, 480, 640, 960, 1280, 1600, 1920, 2560, 3840, 5120, 7680], + "items": { + "type": "number" + }, + "default": [ + 320, + 480, + 640, + 960, + 1280, + 1600, + 1920, + 2560, + 3840, + 5120, + 7680 + ], "description": "Default image sizes to generate." }, "responsiveImageGenerator.useRelativePaths": { @@ -57,19 +93,28 @@ } }, "scripts": { - "lint": "eslint .", - "pretest": "npm run lint", + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "package": "npm run compile", "test": "vscode-test" }, "devDependencies": { + "@eslint/js": "^9.38.0", "@types/mocha": "^10.0.10", "@types/node": "22.x", "@types/vscode": "^1.104.0", + "@typescript-eslint/eslint-plugin": "^8.46.2", + "@typescript-eslint/parser": "^8.46.2", "@vscode/test-cli": "^0.0.11", "@vscode/test-electron": "^2.5.2", - "eslint": "^9.34.0" + "eslint": "^9.38.0", + "globals": "^16.4.0", + "jiti": "^2.6.1", + "typescript-eslint": "^8.46.2" }, "dependencies": { - "sharp": "^0.34.3" + "sharp": "^0.34.3", + "typescript": "^5.9.2" } -} +} \ No newline at end of file diff --git a/src/commands.ts b/src/commands.ts new file mode 100644 index 0000000..67eaa97 --- /dev/null +++ b/src/commands.ts @@ -0,0 +1,162 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import sharp from 'sharp'; +import { promptForAllInputs } from './prompts'; +import { processImage, ProcessImageResult } from './images'; +import { config } from './extension'; + +/** + * Helper to generate output file name. + */ +function getOutputFileName(imagePath: string, outputDir: string, itemName: string, size: number): string { + return path.join(outputDir, `${itemName}_${size}${path.extname(imagePath)}`); +} + +/** + * Command: Generate responsive images + */ +export const disposable = vscode.commands.registerCommand( + 'responsive-image-generator.generate', + async (resourceUri?: vscode.Uri): Promise => { + try { + const result = await promptForAllInputs(resourceUri ? [resourceUri] : undefined); + if (!result) return; + + const { imageUris, outputDir, sizesToGenerate } = result; + const allErrors: string[] = []; + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Generating responsive images...', + cancellable: false, + }, + async (progress) => { + let completed = 0; + const total = imageUris.length; + for (const imageUri of imageUris) { + const itemName = path.basename( + imageUri.fsPath, + path.extname(imageUri.fsPath) + ); + const imageResult: ProcessImageResult = await processImage( + imageUri.fsPath, + outputDir, + itemName, + sizesToGenerate + ); + if (imageResult.errors.length > 0) { + allErrors.push( + ...imageResult.errors.map(e => + `File: ${imageUri.fsPath}, Size: ${e.size}px, Error: ${e.error}` + ) + ); + } + completed++; + progress.report({ message: `Processed ${completed} of ${total}` }); + } + } + ); + + if (allErrors.length > 0) { + vscode.window.showErrorMessage( + `Some images failed to process:\n${allErrors.join('\n')}` + ); + } else { + vscode.window.showInformationMessage( + 'Responsive images generated successfully!' + ); + } + } catch (err: unknown) { + const error = err instanceof Error ? err : new Error(String(err)); + vscode.window.showErrorMessage(`Error: ${error.message}`); + console.error(error); + } + } +); + +/** + * Command: Fill responsive image tag + */ +export const fillResponsiveTagCommand = vscode.commands.registerCommand( + 'responsive-image-generator.fillResponsiveTag', + async ( + document: vscode.TextDocument, + triggerStart: vscode.Position + ): Promise => { + const result = await promptForAllInputs(null); + if (!result) return; + + const { imageUris, outputDir, sizesToGenerate } = result; + const srcsetParts: string[] = []; + const allErrors: string[] = []; + + for (const imageUri of imageUris) { + const itemName = path.basename( + imageUri.fsPath, + path.extname(imageUri.fsPath) + ); + for (const size of sizesToGenerate) { + const outputFile = getOutputFileName(imageUri.fsPath, outputDir, itemName, size); + try { + await sharp(imageUri.fsPath) + .resize(size) + .toFile(outputFile); + + // Use relative paths if configured + if (config.get('useRelativePaths')) { + const rootValue = config.get('staticAssetsRoot'); + const root = + typeof rootValue === 'string' && rootValue.length > 0 + ? rootValue + : vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + const relativeOutputFile = + typeof root === 'string' && root.length > 0 + ? `./${path.relative(root, outputFile)}` + : outputFile; + srcsetParts.push(`${relativeOutputFile} ${size}w`); + } else { + srcsetParts.push(`${outputFile} ${size}w`); + } + } catch (err: unknown) { + const error = err instanceof Error ? err : new Error(String(err)); + allErrors.push( + `File: ${imageUri.fsPath}, Size: ${size}px, Error: ${error.message}` + ); + } + } + } + + const srcset = srcsetParts.join(', '); + const sizes = + sizesToGenerate + .map((size: number) => `(max-width: ${size}px) ${size}px`) + .join(', ') + ', 100vw'; + + // Insert finished snippet, replacing the prefix + const editor = vscode.window.activeTextEditor; + if (editor) { + const snippet = new vscode.SnippetString( + `$1` + ); + const endPosition = triggerStart.translate( + 0, + document.lineAt(triggerStart).text.length - triggerStart.character + ); + await editor.edit((editBuilder) => { + editBuilder.delete(new vscode.Range(triggerStart, endPosition)); + }); + editor.insertSnippet(snippet, triggerStart); + } + + if (allErrors.length > 0) { + vscode.window.showErrorMessage( + `Some images failed to process:\n${allErrors.join('\n')}` + ); + } else { + vscode.window.showInformationMessage( + 'Added responsive image tag and generated images!' + ); + } + } +); \ No newline at end of file diff --git a/src/extension.js b/src/extension.js deleted file mode 100644 index a39abc2..0000000 --- a/src/extension.js +++ /dev/null @@ -1,273 +0,0 @@ -// The module 'vscode' contains the VS Code extensibility API -// Import the module and reference it with the alias vscode in your code below -const vscode = require('vscode'); -const sharp = require('sharp'); -const fs = require('fs'); -const path = require('path'); - -const config = vscode.workspace.getConfiguration('responsiveImageGenerator'); - -// This method is called when your extension is activated -// Your extension is activated the very first time the command is executed - -/** - * @param {vscode.ExtensionContext} context - */ - - -/** - * Activates the Responsive Image Generator extension. - * Registers commands and completion providers. - * @param {vscode.ExtensionContext} context - The extension context provided by VS Code. - */ -function activate(context) { - const extensionPackageJson = require(path.join(context.extensionPath, 'package.json')); - - // Register the main command for generating responsive images - const disposable = vscode.commands.registerCommand('responsive-image-generator.generate', async function () { - try { - const result = await promptForAllInputs(); - if (!result) return; - const { imageUris, outputDir, sizesToGenerate } = result; - - // Show progress while processing images - await vscode.window.withProgress({ - location: vscode.ProgressLocation.Notification, - title: 'Generating responsive images...', - cancellable: false - }, async (progress) => { - progress.report({ message: 'Processing images...' }); - // Process all images and sizes in parallel - await Promise.all( - imageUris.map(imageUri => { - const itemName = path.basename(imageUri.fsPath, path.extname(imageUri.fsPath)); - return processImage(imageUri.fsPath, outputDir, itemName, sizesToGenerate); - }) - ); - }); - - vscode.window.showInformationMessage('Responsive images generated successfully!'); - } catch (err) { - vscode.window.showErrorMessage(`Error: ${err.message}`); - console.error(err); - } - }); - context.subscriptions.push(disposable); - - // Supported languages for completion provider - // Dynamically fetch supported languages from package.json activationEvents - let supportedLanguages = []; - if (extensionPackageJson.activationEvents) { - supportedLanguages = extensionPackageJson.activationEvents - .filter(event => event.startsWith('onLanguage:')) - .map(event => event.replace('onLanguage:', '')); - } - - const searchWord = 'responsive'; - const triggerCharacters = ['<', '>', '!']; - - // Register completion provider for responsive image tag - const provider = vscode.languages.registerCompletionItemProvider( - supportedLanguages, - { - /** - * Provides completion items for responsive image tag. - * @param {vscode.TextDocument} document - * @param {vscode.Position} position - * @returns {vscode.CompletionItem[]|undefined} - */ - provideCompletionItems(document, position) { - const linePrefix = document.lineAt(position).text.split(new RegExp(`[${triggerCharacters.join('')}]`)).at(1)?.trim() || ''; - if (searchWord.includes(linePrefix) && linePrefix.length > 0) { - const completion = new vscode.CompletionItem('responsive_image_basic', vscode.CompletionItemKind.Snippet); - completion.command = { - command: 'responsive-image-generator.fillResponsiveTag', - title: 'Fill Responsive Image Tag', - arguments: [document, position.translate(0, -(linePrefix.length + 1))] - }; - return [completion]; - } - return undefined; - } - }, - ...triggerCharacters - ); - context.subscriptions.push(provider); - - // Register command to fill responsive tag after completion is selected - context.subscriptions.push(vscode.commands.registerCommand('responsive-image-generator.fillResponsiveTag', async (document, triggerStart) => { - const result = await promptForAllInputs(); - if (!result) return; - const { imageUris, outputDir, sizesToGenerate } = result; - - // Generate srcset string - let srcsetParts = []; - for (const imageUri of imageUris) { - const itemName = path.basename(imageUri.fsPath, path.extname(imageUri.fsPath)); - for (const size of sizesToGenerate) { - const outputFile = path.join(outputDir, `${itemName}_${size}${path.extname(imageUri.fsPath)}`); - try { - // Resize and save image - await sharp(imageUri.fsPath) - .resize(size) - .toFile(outputFile); - // Use relative paths if configured - if(config.get('useRelativePaths')) { - const root = config.get('staticAssetsRoot') || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; - const relativeOutputFile = root ? `./${path.relative(root, outputFile)}` : outputFile; - srcsetParts.push(`${relativeOutputFile} ${size}w`); - } else { - srcsetParts.push(`${outputFile} ${size}w`); - } - } catch (err) { - vscode.window.showErrorMessage(`Error processing ${imageUri.fsPath} for size ${size}: ${err.message}`); - } - } - } - const srcset = srcsetParts.join(', '); - - // Generate sizes attribute - const sizes = sizesToGenerate.map(size => `(max-width: ${size}px) ${size}px`).join(', ') + ', 100vw'; - - // Insert finished snippet, replacing the prefix - const editor = vscode.window.activeTextEditor; - if (editor) { - const snippet = new vscode.SnippetString(`$1`); - const endPosition = triggerStart.translate(0, document.lineAt(triggerStart).text.length - triggerStart.character); - editor.edit(editBuilder => { - editBuilder.delete(new vscode.Range(triggerStart, endPosition)); - }).then(() => { - editor.insertSnippet(snippet, triggerStart); - }); - } - vscode.window.showInformationMessage('Add responsive image tag and generated images!'); - })); -} - - - - -/** - * Prompts the user for all required inputs: image files, output directory, and sizes. - * Handles duplicate logic and error messaging for missing selections. - * @returns {Promise<{imageUris: vscode.Uri[], outputDir: string, sizesToGenerate: number[]} | undefined>} Object with all inputs, or undefined if cancelled. - */ -async function promptForAllInputs() { - // Prompt for image file(s) - const imageUris = await promptForImageFiles(); - if (!imageUris || imageUris.length === 0) { - vscode.window.showWarningMessage('No image selected. Operation cancelled.'); - return undefined; - } - - // Prompt for output directory - const outputDir = await promptForOutputDirectory(); - if (!outputDir) { - vscode.window.showWarningMessage('No output directory selected. Operation cancelled.'); - return undefined; - } - - // Prompt for sizes - const sizesToGenerate = await promptForSizes(); - if (!sizesToGenerate || sizesToGenerate.length === 0) { - vscode.window.showWarningMessage('No sizes selected. Operation cancelled.'); - return undefined; - } - - // Create output directory if it doesn't exist - if (!fs.existsSync(outputDir)) { - fs.mkdirSync(outputDir, { recursive: true }); - } - - return { imageUris, outputDir, sizesToGenerate }; -} - -/** - * Prompts the user to select image files. - * @returns {Promise} Array of selected image URIs or undefined if cancelled. - */ -async function promptForImageFiles() { - return await vscode.window.showOpenDialog({ - canSelectMany: true, - openLabel: 'Select Image(s)', - filters: { - 'Images': ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg'] - } - }); -} - -/** - * Prompts the user to select an output directory, preferring 'wwwroot' or 'public' if available. - * @returns {Promise} Path to selected output directory or undefined if cancelled. - */ -async function promptForOutputDirectory() { - const workspaceFolders = vscode.workspace.workspaceFolders || []; - let staticContentFolder = workspaceFolders.find(folder => (folder.name.toLowerCase() === 'wwwroot') || (folder.name.toLowerCase() === 'public')); - let preselectUri = staticContentFolder ? staticContentFolder.uri : undefined; - - const folder = await vscode.window.showWorkspaceFolderPick({ - placeHolder: "Select output folder ('wwwroot' or 'public' will be preselected if available)", - }); - if (folder) { - return folder.uri.fsPath; - } else if (preselectUri) { - return preselectUri.fsPath; - } else { - return undefined; - } -} - -/** - * Prompts the user to select image sizes to generate. - * @returns {Promise} Array of selected sizes or undefined if cancelled. - */ -async function promptForSizes() { - const sizes = config.get('defaultSizes') || [320, 480, 768, 1024, 1280, 1600, 1920, 2560, 3840, 5120, 7680]; - const selectedSizes = await vscode.window.showQuickPick(sizes.map(size => size.toString()), { - placeHolder: 'Select sizes to generate (you can select multiple)', - canPickMany: true - }); - return selectedSizes ? selectedSizes.map(size => parseInt(size)) : undefined; -} - - -/** - * Processes an image: resizes and saves to output directory with item name and size. - * @param {string} imagePath - Path to the source image file. - * @param {string} outputDir - Directory to save resized images. - * @param {string} itemName - Base name for output files. - * @param {number[]} sizesToGenerate - Array of sizes to generate. - * @returns {Promise} - */ -async function processImage(imagePath, outputDir, itemName, sizesToGenerate) { - await Promise.all( - sizesToGenerate.map(async (size) => { - const outputFile = path.join( - outputDir, - `${itemName}_${size}${path.extname(imagePath)}` - ); - try { - await sharp(imagePath) - .resize(size) - .toFile(outputFile); - console.log(`Generated ${outputFile}`); - } catch (err) { - vscode.window.showErrorMessage(`Error processing ${imagePath} for size ${size}: ${err.message}`); - } - }) - ); -} - -// This method is called when your extension is deactivated - - -/** - * Deactivates the extension. - * Called when the extension is deactivated. - */ -function deactivate() { } - -module.exports = { - activate, - deactivate -} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 0000000..6a63497 --- /dev/null +++ b/src/extension.ts @@ -0,0 +1,31 @@ +// The module 'vscode' contains the VS Code extensibility API +// Import the module and reference it with the alias vscode in your code below +import * as vscode from 'vscode'; +import * as commands from './commands'; +import * as provider from './provider'; + +export const config = vscode.workspace.getConfiguration('responsiveImageGenerator'); + +/** + * Activates the Responsive Image Generator extension. + * Registers commands and completion providers. + */ +export function activate(context: vscode.ExtensionContext) { + + // Register the main command for generating responsive images + context.subscriptions.push(commands.disposable); + + // Register command to fill responsive tag after completion is selected + context.subscriptions.push(commands.fillResponsiveTagCommand); + + context.subscriptions.push(provider.provider); +} + + + + +/** + * Deactivates the extension. + * Called when the extension is deactivated. + */ +export function deactivate() {} \ No newline at end of file diff --git a/src/images.ts b/src/images.ts new file mode 100644 index 0000000..0c013ea --- /dev/null +++ b/src/images.ts @@ -0,0 +1,59 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs'; +import sharp from 'sharp'; + +export interface ProcessImageResult { + successes: string[]; + errors: { size: number; error: string }[]; +} + +/** + * Processes an image: resizes and saves to output directory with item name and size. + * Returns a summary of successes and errors. + */ +export async function processImage( + imagePath: string, + outputDir: string, + itemName: string, + sizesToGenerate: number[] +): Promise { + const result: ProcessImageResult = { successes: [], errors: [] }; + + if (!fs.existsSync(imagePath)) { + const msg = `Input file does not exist: ${imagePath}`; + vscode.window.showErrorMessage(msg); + result.errors.push({ size: 0, error: msg }); + return result; + } + + await Promise.all( + sizesToGenerate.map(async (size: number) => { + const outputFile = path.join( + outputDir, + `${itemName}_${size}${path.extname(imagePath)}` + ); + try { + await sharp(imagePath) + .resize(size) + .toFile(outputFile); + console.log(`Generated ${outputFile}`); + result.successes.push(outputFile); + } catch (err: unknown) { + const error = err instanceof Error ? err : new Error(String(err)); + const errorMsg = `Error processing ${imagePath} for size ${size}: ${error.message}`; + console.error(errorMsg); + result.errors.push({ size, error: errorMsg }); + } + }) + ); + + // Show a summary message if there were errors + if (result.errors.length > 0) { + vscode.window.showErrorMessage( + `Some images failed to process: ${result.errors.map(e => `${e.size}px`).join(', ')}` + ); + } + + return result; +} \ No newline at end of file diff --git a/src/prompts.ts b/src/prompts.ts new file mode 100644 index 0000000..48ff860 --- /dev/null +++ b/src/prompts.ts @@ -0,0 +1,102 @@ +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import { config } from './extension'; + +/** + * Prompts the user for all required inputs: image files, output directory, and sizes. + * Handles duplicate logic and error messaging for missing selections. + */ +export async function promptForAllInputs(imageUris: vscode.Uri[] | undefined | null): Promise<{ imageUris: vscode.Uri[]; outputDir: string; sizesToGenerate: number[] } | undefined> { + // If imageUris not provided, prompt user to select image files + if (!imageUris || imageUris === null) { + imageUris = await promptForImageFiles(); + } + + // Handle case where no images were selected + if (!imageUris || imageUris.length === 0) { + vscode.window.showWarningMessage('No image selected. Operation cancelled.'); + return undefined; + } + + // Prompt for output directory + const outputDir = await promptForOutputDirectory(); + if (!outputDir) { + vscode.window.showWarningMessage('No output directory selected. Operation cancelled.'); + return undefined; + } + + // Prompt for sizes to generate + const sizesToGenerate = await promptForSizes(); + if (!sizesToGenerate || sizesToGenerate.length === 0) { + vscode.window.showWarningMessage('No sizes selected. Operation cancelled.'); + return undefined; + } + + // Ensure output directory exists + try { + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + } catch (err: unknown) { + const error = err instanceof Error ? err : new Error(String(err)); + vscode.window.showErrorMessage(`Failed to create output directory: ${error.message}`); + return undefined; + } + + // Return all collected inputs + return { imageUris, outputDir, sizesToGenerate }; +} + +/** + * Prompts the user to select image files. + */ +export async function promptForImageFiles(): Promise { + return vscode.window.showOpenDialog({ + canSelectMany: true, + openLabel: 'Select Image(s)', + filters: { + Images: ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg'], + }, + }); +} + +/** + * Prompts the user to select an output directory, preferring 'wwwroot' or 'public' if available. + */ +export async function promptForOutputDirectory(): Promise { + const workspaceFolders = vscode.workspace.workspaceFolders || []; + const staticContentFolder = workspaceFolders.find( + (folder) => folder.name.toLowerCase() === 'wwwroot' || folder.name.toLowerCase() === 'public' + ); + + const folder = await vscode.window.showWorkspaceFolderPick({ + placeHolder: "Select output folder ('wwwroot' or 'public' will be preselected if available)", + }); + + if (folder) { + return folder.uri.fsPath; + } else if (staticContentFolder) { + return staticContentFolder.uri.fsPath; + } else { + return undefined; + } +} + +/** + * Prompts the user to select image sizes to generate. + */ +export async function promptForSizes(): Promise { + const sizesConfig = config.get('defaultSizes'); + const sizes = Array.isArray(sizesConfig) + ? sizesConfig + : [320, 480, 768, 1024, 1280, 1600, 1920, 2560, 3840, 5120, 7680]; + + const selectedSizes = await vscode.window.showQuickPick( + sizes.map((size: number) => size.toString()), + { + placeHolder: 'Select sizes to generate (you can select multiple)', + canPickMany: true, + } + ); + return selectedSizes ? selectedSizes.map((size: string) => parseInt(size, 10)) : undefined; +} \ No newline at end of file diff --git a/src/provider.ts b/src/provider.ts new file mode 100644 index 0000000..a60dafe --- /dev/null +++ b/src/provider.ts @@ -0,0 +1,49 @@ +import * as vscode from 'vscode'; + +// Import package.json for activationEvents +// @ts-expect-error: Ignore import error for JSON file +import extensionPackageJson from '../package.json'; + +// Constants +const searchWord = 'responsive'; +const triggerCharacters = ['<', '>', '!']; + +// Dynamically fetch supported languages from package.json activationEvents +const supportedLanguages: string[] = extensionPackageJson.activationEvents + ? extensionPackageJson.activationEvents + .filter((event: string) => event.startsWith('onLanguage:')) + .map((event: string) => event.replace('onLanguage:', '')) + : []; + +/** + * Completion provider for responsive image tag. + */ +export const provider = vscode.languages.registerCompletionItemProvider( + supportedLanguages, + { + provideCompletionItems( + document: vscode.TextDocument, + position: vscode.Position + ): vscode.CompletionItem[] | undefined { + // Extract the word before the cursor that may trigger completion + const line = document.lineAt(position).text; + const prefixMatch = line.slice(0, position.character).match(/(\w+)$/); + const linePrefix = prefixMatch ? prefixMatch[1] : ''; + + if (searchWord.includes(linePrefix) && linePrefix.length > 0) { + const completion = new vscode.CompletionItem( + 'responsive_image_basic', + vscode.CompletionItemKind.Snippet + ); + completion.command = { + command: 'responsive-image-generator.fillResponsiveTag', + title: 'Fill Responsive Image Tag', + arguments: [document, position.translate(0, -(linePrefix.length))] + }; + return [completion]; + } + return undefined; + } + }, + ...triggerCharacters +); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..92026e9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "dist" + }, + "include": ["src/**/*"] +}