mirror of
https://github.com/koloml/furbooru-tagging-assistant.git
synced 2025-12-23 14:52:59 +00:00
Reworking post-svelte build process for the extension
This commit is contained in:
125
.vite/lib/content-scripts.js
Normal file
125
.vite/lib/content-scripts.js
Normal 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
46
.vite/lib/index-file.js
Normal 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
103
.vite/lib/manifest.js
Normal 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
65
.vite/pack-extension.js
Normal 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.
|
||||
*/
|
||||
11
build-extension.js
Normal file
11
build-extension.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import {packExtension} from "./.vite/pack-extension.js";
|
||||
import path from "path";
|
||||
import {fileURLToPath} from "url";
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
void packExtension({
|
||||
rootDir: __dirname,
|
||||
exportDir: path.resolve(__dirname, 'build'),
|
||||
contentScriptsDir: path.resolve(__dirname, 'build', 'content')
|
||||
});
|
||||
@@ -5,7 +5,7 @@
|
||||
"scripts": {
|
||||
"build": "npm run build:popup && npm run build:extension",
|
||||
"build:popup": "vite build",
|
||||
"build:extension": "vite build --config vite.config.extension.js",
|
||||
"build:extension": "node build-extension.js",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch"
|
||||
},
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
import {defineConfig, normalizePath} from 'vite';
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import {load} from "cheerio";
|
||||
import crypto from "crypto";
|
||||
|
||||
const packageJsonPath = path.resolve(__dirname, 'package.json');
|
||||
const manifestJsonPath = path.resolve(__dirname, 'manifest.json');
|
||||
|
||||
const buildDirectoryPath = path.resolve(__dirname, 'build');
|
||||
const contentScriptsDirectoryPath = path.resolve(buildDirectoryPath, 'assets', 'content');
|
||||
|
||||
if (!fs.existsSync(manifestJsonPath)) {
|
||||
throw new Error(
|
||||
`The manifest.json file is missing from the root of the project.`
|
||||
);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
throw new Error(
|
||||
`The package.json file is missing from the root of the project.`
|
||||
);
|
||||
}
|
||||
|
||||
const packageInformation = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
const manifestInformation = JSON.parse(fs.readFileSync(manifestJsonPath, 'utf8'));
|
||||
|
||||
/** @type {import('vite').RollupOptions} */
|
||||
const rollupOptions = {
|
||||
input: {},
|
||||
output: {
|
||||
dir: contentScriptsDirectoryPath,
|
||||
entryFileNames: '[name].js',
|
||||
assetFileNames: '[name].[ext]',
|
||||
}
|
||||
};
|
||||
|
||||
function hashFilePath(filePath) {
|
||||
return crypto
|
||||
.createHash('sha256')
|
||||
.update(filePath)
|
||||
.digest('base64url')
|
||||
.slice(0, 8);
|
||||
}
|
||||
|
||||
// This is somewhat hacky, but it works for now. This code goes over the list of content scripts, adding them to the
|
||||
// rollupOptions.input object, and then modifying the content_scripts array to point to the built file names.
|
||||
// TODO: This fragment should probably somehow be moved into a plugin, together with the code that copies the source
|
||||
// mainifest.json file to the build directory.
|
||||
if (manifestInformation?.['content_scripts']) {
|
||||
manifestInformation['content_scripts'] = manifestInformation['content_scripts'].map(entry => {
|
||||
if (entry.js) {
|
||||
entry.js = entry.js.map(filePath => {
|
||||
const fileName = path.basename(filePath);
|
||||
const baseName = fileName.split('.').slice(0, -1).join('.');
|
||||
const outputBaseName = `${baseName}-${hashFilePath(filePath)}`;
|
||||
|
||||
rollupOptions.input[outputBaseName] = filePath;
|
||||
|
||||
return normalizePath(
|
||||
path.relative(
|
||||
buildDirectoryPath,
|
||||
path.resolve(contentScriptsDirectoryPath, outputBaseName + '.js')
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (entry.css) {
|
||||
entry.css = entry.css.map(filePath => {
|
||||
const fileName = path.basename(filePath);
|
||||
const baseName = fileName.split('.').slice(0, -1).join('.');
|
||||
const outputBaseName = `${baseName}-${hashFilePath(filePath)}`;
|
||||
|
||||
rollupOptions.input[outputBaseName] = filePath;
|
||||
|
||||
return normalizePath(
|
||||
path.relative(
|
||||
buildDirectoryPath,
|
||||
path.resolve(contentScriptsDirectoryPath, outputBaseName + '.css')
|
||||
)
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
return entry;
|
||||
});
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
rollupOptions,
|
||||
emptyOutDir: false,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"$lib": path.resolve(__dirname, 'src/lib'),
|
||||
"$entities": path.resolve(__dirname, 'src/lib/extension/entities'),
|
||||
"$styles": path.resolve(__dirname, 'src/styles'),
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
name: 'extract-inline-js',
|
||||
async buildEnd() {
|
||||
const buildPath = path.resolve(__dirname, 'build');
|
||||
const indexFilePath = path.resolve(buildPath, 'index.html');
|
||||
const indexHtml = fs.readFileSync(indexFilePath, 'utf8');
|
||||
|
||||
const ch = load(indexHtml);
|
||||
|
||||
ch('script').each((index, scriptElement) => {
|
||||
const $script = ch(scriptElement);
|
||||
const scriptContent = $script.text();
|
||||
|
||||
const entryHash = crypto.createHash('sha256')
|
||||
.update(scriptContent)
|
||||
.digest('base64url');
|
||||
|
||||
const scriptName = `init.${entryHash.slice(0, 8)}.js`;
|
||||
const scriptFilePath = path.resolve(buildPath, scriptName);
|
||||
const scriptPublicPath = `./${scriptName}`;
|
||||
|
||||
fs.writeFileSync(scriptFilePath, scriptContent);
|
||||
|
||||
$script.attr('src', scriptPublicPath);
|
||||
$script.text('');
|
||||
});
|
||||
|
||||
fs.writeFileSync(indexFilePath, ch.html());
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "bypass-manifest-extension",
|
||||
async buildEnd() {
|
||||
manifestInformation.version = packageInformation.version;
|
||||
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, 'build', 'manifest.json'),
|
||||
JSON.stringify(manifestInformation, null, 2)
|
||||
);
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
Reference in New Issue
Block a user