feat: enable bundling of encrypted modules with CSS injection and external dependency support

This commit is contained in:
Vitalii Litvinchuk
2026-06-10 22:55:37 +03:00
parent 8fa7846da0
commit c20540e4ca
7 changed files with 89 additions and 18 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
#### Created by Claude Opus 4.6 #### Created by Agents
NPM link: https://www.npmjs.com/package/vite-plugin-component-locker NPM link: https://www.npmjs.com/package/vite-plugin-component-locker
+21 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "vite-plugin-component-locker", "name": "vite-plugin-component-locker",
"version": "1.0.0", "version": "1.0.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "vite-plugin-component-locker", "name": "vite-plugin-component-locker",
"version": "1.0.0", "version": "1.0.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
@@ -15,6 +15,7 @@
"devDependencies": { "devDependencies": {
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/mime-types": "^3.0.1", "@types/mime-types": "^3.0.1",
"@types/node": "^25.9.2",
"@types/react": "^19.1.0", "@types/react": "^19.1.0",
"@types/react-dom": "^19.1.0", "@types/react-dom": "^19.1.0",
"@vitejs/plugin-react": "^4.5.2", "@vitejs/plugin-react": "^4.5.2",
@@ -1226,6 +1227,17 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": {
"version": "25.9.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.2.tgz",
"integrity": "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": ">=7.24.0 <7.24.7"
}
},
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "19.2.17", "version": "19.2.17",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
@@ -1768,6 +1780,13 @@
"node": ">=14.17" "node": ">=14.17"
} }
}, },
"node_modules/undici-types": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
"dev": true,
"license": "MIT"
},
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+10 -4
View File
@@ -10,16 +10,21 @@
"security" "security"
], ],
"name": "vite-plugin-component-locker", "name": "vite-plugin-component-locker",
"version": "1.0.0", "version": "1.0.3",
"type": "module", "type": "module",
"main": "./dist/crypto-locker.umd.cjs", "main": "./dist/crypto-locker.umd.cjs",
"module": "./dist/crypto-locker.js", "module": "./dist/crypto-locker.js",
"types": "./dist/index.d.ts",
"exports": { "exports": {
".": { ".": {
"types": "./dist/index.d.ts",
"import": "./dist/crypto-locker.js", "import": "./dist/crypto-locker.js",
"require": "./dist/crypto-locker.umd.cjs" "require": "./dist/crypto-locker.umd.cjs"
}, },
"./plugin": "./src/plugin.ts" "./plugin": {
"types": "./dist/plugin.d.ts",
"default": "./src/plugin.ts"
}
}, },
"files": [ "files": [
"dist", "dist",
@@ -36,6 +41,7 @@
"devDependencies": { "devDependencies": {
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/mime-types": "^3.0.1", "@types/mime-types": "^3.0.1",
"@types/node": "^25.9.2",
"@types/react": "^19.1.0", "@types/react": "^19.1.0",
"@types/react-dom": "^19.1.0", "@types/react-dom": "^19.1.0",
"@vitejs/plugin-react": "^4.5.2", "@vitejs/plugin-react": "^4.5.2",
@@ -45,6 +51,6 @@
"vite": "^6.3.5" "vite": "^6.3.5"
}, },
"scripts": { "scripts": {
"build": "vite build" "build": "vite build && tsc --emitDeclarationOnly"
} }
} }
+40 -4
View File
@@ -163,17 +163,53 @@ export const mimeType = ${JSON.stringify(mimeType)};
const isAsset = !/\.[jt]sx?$/i.test(cleanId); const isAsset = !/\.[jt]sx?$/i.test(cleanId);
if (isAsset) return null; // Handled in load() if (isAsset) return null; // Handled in load()
const { transform: esbuildTransform } = await import("esbuild"); const { build: esbuildBuild } = await import("esbuild");
const { code: jsCode } = await esbuildTransform(code, { const buildResult = await esbuildBuild({
loader: cleanId.match(/\.tsx$/i) ? "tsx" : "ts", entryPoints: [cleanId],
bundle: true,
format: "cjs", format: "cjs",
target: "es2020", target: "es2020",
write: false,
outdir: "out",
jsx: "transform", jsx: "transform",
jsxFactory: "React.createElement", jsxFactory: "React.createElement",
jsxFragment: "React.Fragment" jsxFragment: "React.Fragment",
loader: {
".svg": "dataurl",
".png": "dataurl",
".jpg": "dataurl",
".jpeg": "dataurl",
".gif": "dataurl",
".pdf": "dataurl",
},
// Exclude React and user-specified external dependencies
external: [
"react",
"react-dom",
...(options.external || []),
],
}); });
const jsFile = buildResult.outputFiles.find(f => f.path.endsWith('.js'));
const cssFile = buildResult.outputFiles.find(f => f.path.endsWith('.css'));
let jsCode = jsFile ? jsFile.text : "";
// Inject bundled CSS into the JS code so it applies at runtime
if (cssFile && cssFile.text.trim()) {
const escapedCss = JSON.stringify(cssFile.text);
const injectCss = `
if (typeof document !== 'undefined') {
const style = document.createElement('style');
style.setAttribute('data-crypto-locker', 'true');
style.textContent = ${escapedCss};
document.head.appendChild(style);
}
`;
jsCode = injectCss + '\n' + jsCode;
}
const encrypted = cryptoJs.AES.encrypt(jsCode, password).toString(); const encrypted = cryptoJs.AES.encrypt(jsCode, password).toString();
return { return {
+3
View File
@@ -24,4 +24,7 @@ export interface CryptoLockerPluginOptions {
* @default /\.secret\.[jt]sx?$/i * @default /\.secret\.[jt]sx?$/i
*/ */
include?: string | RegExp; include?: string | RegExp;
/** Additional external dependencies to exclude from bundling */
external?: string[];
} }
+1 -1
View File
@@ -81,7 +81,7 @@ export function useCryptoAsset() {
// Convert base64 back to Blob // Convert base64 back to Blob
const uint8Array = base64ToUint8Array(plaintextBase64); const uint8Array = base64ToUint8Array(plaintextBase64);
const blob = new Blob([uint8Array], { type: mod.mimeType || "application/octet-stream" }); const blob = new Blob([uint8Array as any], { type: mod.mimeType || "application/octet-stream" });
const objectUrl = URL.createObjectURL(blob); const objectUrl = URL.createObjectURL(blob);
setDecryptedUrl(objectUrl); setDecryptedUrl(objectUrl);
+13 -6
View File
@@ -1,6 +1,7 @@
import { useState, useCallback } from "react"; import { useState, useCallback } from "react";
import CryptoJS from "crypto-js"; import CryptoJS from "crypto-js";
import React from "react"; import * as React from "react";
import * as jsxRuntime from "react/jsx-runtime";
import type { CryptoLockerModule } from "./types"; import type { CryptoLockerModule } from "./types";
export type CryptoLockerStatus = "idle" | "loading" | "success" | "error"; export type CryptoLockerStatus = "idle" | "loading" | "success" | "error";
@@ -8,11 +9,13 @@ export type CryptoLockerStatus = "idle" | "loading" | "success" | "error";
/** /**
* Hook that provides logic for AES decryption of dynamic modules. * Hook that provides logic for AES decryption of dynamic modules.
* UI, password handling, and validation are fully delegated to the developer. * UI, password handling, and validation are fully delegated to the developer.
*
* @param dependencies Optional dictionary of external dependencies to provide to the encrypted module.
*/ */
export function useCryptoLocker<T = unknown>() { export function useCryptoLocker<T = unknown>(dependencies: Record<string, any> = {}) {
const [status, setStatus] = useState<CryptoLockerStatus>("idle"); const [status, setStatus] = React.useState<CryptoLockerStatus>("idle");
const [decryptedData, setDecryptedData] = useState<T | null>(null); const [decryptedData, setDecryptedData] = React.useState<T | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = React.useState<string | null>(null);
/** /**
* Decrypts the dynamically loaded module and evaluates its code. * Decrypts the dynamically loaded module and evaluates its code.
@@ -53,9 +56,13 @@ export function useCryptoLocker<T = unknown>() {
const exportsObj: any = {}; const exportsObj: any = {};
const moduleObj = { exports: exportsObj }; const moduleObj = { exports: exportsObj };
// Provide React dependency to the evaluated module // Provide dependencies to the evaluated module
const requireFn = (id: string) => { const requireFn = (id: string) => {
if (id === "react") return React; if (id === "react") return React;
if (id === "react/jsx-runtime") return jsxRuntime;
if (id === "react-dom") return (window as any).ReactDOM || {};
if (/\.(css|scss|less)$/i.test(id)) return {}; // Ignore CSS imports inside secret modules
if (id in dependencies) return dependencies[id];
throw new Error(`Cannot require '${id}' in encrypted module.`); throw new Error(`Cannot require '${id}' in encrypted module.`);
}; };