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(`
`);
- 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(
+ `
`
+ );
+ 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(`
`);
- 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/**/*"]
+}