From 54a41f2431c3d60f5845a15447f13413299e41f2 Mon Sep 17 00:00:00 2001 From: memdmp Date: Mon, 24 Feb 2025 01:09:00 +0100 Subject: feat: da extension --- adapter/index.js | 192 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 adapter/index.js (limited to 'adapter/index.js') diff --git a/adapter/index.js b/adapter/index.js new file mode 100644 index 0000000..03e2730 --- /dev/null +++ b/adapter/index.js @@ -0,0 +1,192 @@ +// https://github.com/michmich112/sveltekit-adapter-chrome-extension +import staticAdapter from '@sveltejs/adapter-static'; +import { load } from 'cheerio'; +import { + createReadStream, + createWriteStream, + existsSync, + readFileSync, + statSync, + unlinkSync, + writeFileSync, +} from 'fs'; +import { join } from 'path'; +import { pipeline } from 'stream'; +import glob from 'tiny-glob'; +import { promisify } from 'util'; +import zlib from 'zlib'; + +const pipe = promisify(pipeline); + +/** @type {import('.')} */ +export default function (options) { + return { + name: 'sveltekit-adapter-chrome-extension', + + async adapt(builder) { + staticAdapter(options).adapt(builder); + + /* extension */ + const pages = options?.pages ?? 'build'; + const assets = options?.assets ?? pages; + const manifest = options?.manifest ?? 'manifest.json'; + + await removeInlineScripts(options, assets, builder.log); + + await removeAppManifest(assets, builder.config.kit.appDir, builder.log); + await removeAppManifest('.', assets, builder.log); + + // operation required since generated app manifest will overwrite the static extension manifest.json + reWriteExtensionManifest(assets, manifest, builder); + }, + }; +} + +/** + * Hash using djb2 + * @param {import('types/hooks').StrictBody} value + */ +function hash(value) { + let hash = 5381; + let i = value.length; + + if (typeof value === 'string') { + while (i) hash = (hash * 33) ^ value.charCodeAt(--i); + } else { + while (i) hash = (hash * 33) ^ value[--i]; + } + + return (hash >>> 0).toString(36); +} + +async function removeAppManifest(directory, appDir, log) { + log('Removing App Manifest'); + const files = await glob(`**/${appDir}/*manifest*.json`, { + cwd: directory, + dot: true, + absolute: true, + filesOnly: true, + }); + + files.forEach((path) => { + try { + unlinkSync(path); + log.success(`Removed app manifest file at path: ${path}`); + } catch (err) { + log.warn( + `Error removing app manifest file at path: ${path}. You may have to delete it manually before submitting you extension.\nError: ${err}` + ); + } + }); +} + +async function removeInlineScripts(options, directory, log) { + log('Removing Inline Scripts'); + const files = await glob('**/*.{html}', { + cwd: directory, + dot: true, + aboslute: true, + filesOnly: true, + }); + + files + .map((f) => join(directory, f)) + .forEach((file) => { + log.minor(`file: ${file}`); + const f = readFileSync(file); + const $ = load(f.toString()); + const node = $('script').get()[0]; + + if (!node) return; + if (Object.keys(node.attribs).includes('src')) return; // if there is a src, it's not an inline script + + const attribs = Object.keys(node.attribs).reduce( + (a, c) => a + `${c}="${node.attribs[c]}" `, + '' + ); + let innerScript = node.children[0].data; + const fullTag = $('script').toString(); + //get new filename + const fn = `${ + options?.paths?.app ? '/' + options.paths.app + '/immutable' : '.' + }/inline-${hash(innerScript)}.js`; + if (options?.paths?.app) + innerScript = innerScript.replaceAll(options.paths.app + '/', '../'); + //remove from orig html file and replace with new script tag + const newHtml = f + .toString() + .replace( + fullTag, + `` + ); + writeFileSync(file, newHtml); + log.minor(`Rewrote ${file}`); + + const p = `${directory}${fn}`; + writeFileSync(p, innerScript); + log.success(`Inline script extracted and saved at: ${p}`); + }); +} + +function reWriteExtensionManifest(directory, manifest, builder) { + const { log, getStaticDirectory, getClientDirectory, copy } = builder; + log('Re-writing extension manifest'); + let sourceFilePath; + if (typeof getStaticDirectory !== 'undefined') { + sourceFilePath = join(getStaticDirectory(), manifest); + } else { + sourceFilePath = join(getClientDirectory(), manifest); + } + if (existsSync(sourceFilePath)) { + log.info('Extension manifest found'); + const res = copy(sourceFilePath, join(directory, 'manifest.json')); + log.success('Successfully re-wrote extension manifest'); + } else { + log.error( + `Extension manifest not found. Make sure you've added your extension manifest in your statics directory with the name ${manifest}` + ); + } +} + +/** + * @param {string} directory + */ +async function compress(directory) { + const files = await glob('**/*.{html,js,json,css,svg,xml}', { + cwd: directory, + dot: true, + absolute: true, + filesOnly: true, + }); + + await Promise.all( + files.map((file) => + Promise.all([compress_file(file, 'gz'), compress_file(file, 'br')]) + ) + ); +} + +/** + * @param {string} file + * @param {'gz' | 'br'} format + */ +async function compress_file(file, format = 'gz') { + const compress = + format == 'br' + ? zlib.createBrotliCompress({ + params: { + [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT, + [zlib.constants.BROTLI_PARAM_QUALITY]: + zlib.constants.BROTLI_MAX_QUALITY, + [zlib.constants.BROTLI_PARAM_SIZE_HINT]: statSync(file).size, + }, + }) + : zlib.createGzip({ level: zlib.constants.Z_BEST_COMPRESSION }); + + const source = createReadStream(file); + const destination = createWriteStream(`${file}.${format}`); + + await pipe(source, compress, destination); +} -- cgit v1.2.3