1
0
mirror of https://github.com/koloml/furbooru-tagging-assistant.git synced 2025-12-23 23:02:58 +00:00

Reworking post-svelte build process for the extension

This commit is contained in:
2024-05-08 01:47:35 +04:00
parent 2ffef5951d
commit 571b8dd575
7 changed files with 351 additions and 146 deletions

View File

@@ -0,0 +1,125 @@
import {build} from "vite";
import {createHash} from "crypto";
import path from "path";
import fs from "fs";
/**
* Create the result base file name for the file.
* @param {string} inputPath Path to the original filename.
* @return {string} Result base file name without extension. Contains original filename + hash suffix.
*/
function createOutputBaseName(inputPath) {
const hashSuffix = createHash('sha256')
.update(
// Yes, suffix is only dependent on the entry file, dependencies are not included.
fs.readFileSync(inputPath, 'utf8')
)
.digest('base64url')
.substring(0, 8);
const baseName = path.basename(inputPath, path.extname(inputPath));
return `${baseName}-${hashSuffix}`;
}
/**
* Small workaround plugin to cover each individual content script into IIFE. This is pretty much mandatory to use,
* otherwise helper functions made by Vite will collide with each other. Only include this plugin into config with
* script!
* @return {import('vite').Plugin}
*/
function wrapScriptIntoIIFE() {
return {
name: 'wrap-scripts-into-iife',
generateBundle(outputBundles, bundle) {
Object.keys(bundle).forEach(fileName => {
const file = bundle[fileName];
file.code = `(() => {\n${file.code}})();`
});
}
}
}
/**
* Default aliases used inside popup app.
* @param {string} rootDir Root directory of the repo for building paths.
* @return {Record<string, string>} Aliases to include into the config object.
*/
function makeAliases(rootDir) {
return {
"$lib": path.resolve(rootDir, 'src/lib'),
"$entities": path.resolve(rootDir, 'src/lib/extension/entities'),
"$styles": path.resolve(rootDir, 'src/styles'),
}
}
/**
* Build the selected script separately.
* @param {AssetBuildOptions} buildOptions Building options for the script.
* @return {Promise<string>} Result file path.
*/
export async function buildScript(buildOptions) {
const outputBaseName = createOutputBaseName(buildOptions.input);
await build({
configFile: false,
publicDir: false,
build: {
rollupOptions: {
input: {
[outputBaseName]: buildOptions.input
},
output: {
dir: buildOptions.outputDir,
entryFileNames: '[name].js'
}
},
emptyOutDir: false,
},
resolve: {
alias: makeAliases(buildOptions.rootDir)
},
plugins: [
wrapScriptIntoIIFE()
]
});
return path.resolve(buildOptions.outputDir, `${outputBaseName}.js`);
}
/**
* Build the selected stylesheet.
* @param {AssetBuildOptions} buildOptions Build options for the stylesheet.
* @return {Promise<string>} Result file path.
*/
export async function buildStyle(buildOptions) {
const outputBaseName = createOutputBaseName(buildOptions.input);
await build({
configFile: false,
publicDir: false,
build: {
rollupOptions: {
input: {
[outputBaseName]: buildOptions.input
},
output: {
dir: buildOptions.outputDir,
entryFileNames: '[name].js',
assetFileNames: '[name].[ext]',
}
},
emptyOutDir: false,
}
});
return path.resolve(buildOptions.outputDir, `${outputBaseName}.css`);
}
/**
* @typedef {Object} AssetBuildOptions
* @property {string} input Full path to the input file to build.
* @property {string} outputDir Destination folder for the script.
* @property {string} rootDir Root directory of the repository.
*/

46
.vite/lib/index-file.js Normal file
View File

@@ -0,0 +1,46 @@
import fs from "fs";
import {createHash} from "crypto";
import {load} from "cheerio";
import path from "path";
/**
* Find and extract all inline scripts injected into index file by the SvelteKit builder. This needs to be done due to
* ManifestV3 restrictions on inline/loaded scripts usage. The only way to run scripts in popup is by specifying
* `<script>` tag with the path. Thanks, ManifestV3!
*
* @param {string} indexFilePath Path to the index.html file. This file will be overridden and all the inline scripts
* found inside it will be placed in the same directory.
*/
export function extractInlineScriptsFromIndex(indexFilePath) {
const directory = path.dirname(indexFilePath);
const html = fs.readFileSync(indexFilePath, 'utf8');
const ch = load(html);
ch('script').each((index, scriptElement) => {
const $script = ch(scriptElement);
const scriptContent = $script.text();
const contentsHash = createHash('sha256')
.update(scriptContent)
.digest('base64url')
.substring(0, 8);
const fileName = `init.${contentsHash}.js`;
const filePath = path.resolve(directory, fileName);
const publicPath = `./${fileName}`;
fs.writeFileSync(
filePath,
// This will work for minifying index script of the SvelteKit, but might cause some issues if any other scripts
// will appear. Good for now.
scriptContent
.replaceAll("\t", "")
.replaceAll("\n", "")
);
$script.attr('src', publicPath);
$script.text('');
});
fs.writeFileSync(indexFilePath, ch.html());
}

103
.vite/lib/manifest.js Normal file
View File

@@ -0,0 +1,103 @@
import fs from "fs";
/**
* Helper class for processing and using manifest for packing the extension.
*/
class ManifestProcessor {
/**
* Current state of the manifest object.
* @type {Manifest}
*/
#manifestObject;
/**
* @param {Manifest} parsedManifest Original manifest contents parsed as JSON object.
*/
constructor(parsedManifest) {
this.#manifestObject = parsedManifest;
}
/**
* Map over every content script defined in the manifest. If no content scripts defined, no calls will be made to the
* callback.
*
* @param {function(ContentScriptsEntry): Promise<ContentScriptsEntry>} mapCallback Processing function to call on
* every entry. Entries should be modified and returned. Function should be asynchronous.
*
* @return {Promise<void>}
*/
async mapContentScripts(mapCallback) {
const contentScripts = this.#manifestObject.content_scripts;
if (!contentScripts) {
console.info('No content scripts to map over.');
return;
}
for (let entryIndex = 0; entryIndex < contentScripts.length; entryIndex++) {
contentScripts[entryIndex] = await mapCallback(contentScripts[entryIndex]);
}
}
/**
* Pass the version of the plugin from following package.json file.
*
* @param {string} packageFilePath Path to the JSON file to parse and extract the version from. If version is not
* found, original version will be kept.
*/
passVersionFromPackage(packageFilePath) {
/** @type {PackageObject} */
const packageObject = JSON.parse(fs.readFileSync(packageFilePath, 'utf8'));
if (packageObject.version) {
this.#manifestObject.version = packageObject.version;
}
}
/**
* Save the current state of the manifest into the selected file.
*
* @param {string} manifestFilePath File to write the resulting manifest to. Should be called after all the
* modifications.
*/
saveTo(manifestFilePath) {
fs.writeFileSync(
manifestFilePath,
JSON.stringify(this.#manifestObject, null, 2),
{
encoding: 'utf8'
}
);
}
}
/**
* Load the manifest and create a processor object.
*
* @param {string} filePath Path to the original manifest file.
*
* @return {ManifestProcessor} Object for manipulating manifest file.
*/
export function loadManifest(filePath) {
const manifest = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
return new ManifestProcessor(manifest);
}
/**
* @typedef {Object} Manifest
* @property {string} version
* @property {ContentScriptsEntry[]|undefined} content_scripts
*/
/**
* @typedef {Object} ContentScriptsEntry
* @property {string[]} mathces
* @property {string[]|undefined} js
* @property {string[]|undefined} css
*/
/**
* @typedef {Object} PackageObject
* @property {string|undefined} version
*/

65
.vite/pack-extension.js Normal file
View File

@@ -0,0 +1,65 @@
import {loadManifest} from "./lib/manifest.js";
import path from "path";
import {buildScript, buildStyle} from "./lib/content-scripts.js";
import {normalizePath} from "vite";
import {extractInlineScriptsFromIndex} from "./lib/index-file.js";
/**
* Build addition assets required for the extension and pack it into the directory.
* @param {PackExtensionSettings} settings Build settings.
*/
export async function packExtension(settings) {
const manifest = loadManifest(path.resolve(settings.rootDir, 'manifest.json'));
// Since we CAN'T really build all scripts and stylesheets in a single build entry, we will run build for every single
// one of them in a row. This way, no chunks will be generated. Thanks, ManifestV3!
await manifest.mapContentScripts(async (entry) => {
if (entry.js) {
for (let scriptIndex = 0; scriptIndex < entry.js.length; scriptIndex++) {
const builtScriptFilePath = await buildScript({
input: path.resolve(settings.rootDir, entry.js[scriptIndex]),
outputDir: settings.contentScriptsDir,
rootDir: settings.rootDir,
});
entry.js[scriptIndex] = normalizePath(
path.relative(
settings.exportDir,
builtScriptFilePath
)
);
}
}
if (entry.css) {
for (let styleIndex = 0; styleIndex < entry.css.length; styleIndex++) {
const builtStylesheetFilePath = await buildStyle({
input: path.resolve(settings.rootDir, entry.css[styleIndex]),
outputDir: settings.contentScriptsDir,
rootDir: settings.rootDir
});
entry.css[styleIndex] = normalizePath(
path.relative(
settings.exportDir,
builtStylesheetFilePath
)
);
}
}
return entry;
});
manifest.passVersionFromPackage(path.resolve(settings.rootDir, 'package.json'));
manifest.saveTo(path.resolve(settings.exportDir, 'manifest.json'));
extractInlineScriptsFromIndex(path.resolve(settings.exportDir, 'index.html'));
}
/**
* @typedef {Object} PackExtensionSettings
* @property {string} rootDir Root directory of the repository. Required for properly fetching source files.
* @property {string} exportDir Directory of the built extension.
* @property {string} contentScriptsDir Directory specifically for content scripts entries.
*/