webpack-svg-sprites/plugin/plugin.ts

212 lines
6.8 KiB
TypeScript
Raw Normal View History

2020-08-09 11:21:29 +00:00
import * as path from "path";
2021-03-16 19:02:08 +00:00
import * as SystemFs from "fs-extra";
2021-03-16 18:20:57 +00:00
import sha1 from "sha1";
2021-03-16 19:02:08 +00:00
import webpack, {Compiler, Module, RuntimeGlobals} from "webpack";
2020-08-09 11:21:29 +00:00
import {
GeneratedSprite,
generateSprite,
generateSpriteCss, generateSpriteDts, generateSpriteJs,
generateSpriteSvg,
SpriteCssOptions,
SpriteDtsOptions
} from "./generator";
2021-03-16 19:02:08 +00:00
import Compilation = webpack.Compilation;
2020-08-09 11:21:29 +00:00
2021-03-16 18:20:57 +00:00
const { RawSource } = require("webpack-sources");
2020-08-09 11:21:29 +00:00
export interface Options {
modulePrefix?: string, /* defaults to svg-sprites/ */
dtsOutputFolder: string,
configurations: {
[key: string] : SvgSpriteConfiguration
},
publicPath?: string,
2020-08-09 11:21:29 +00:00
}
interface SvgSpriteConfiguration {
folder: string;
cssClassPrefix: string;
dtsOptions: SpriteDtsOptions;
cssOptions: SpriteCssOptions[];
}
2021-03-16 18:20:57 +00:00
const TYPES = new Set(["javascript"]);
const RUNTIME_REQUIREMENTS = new Set([
RuntimeGlobals.module
]);
2020-08-09 11:21:29 +00:00
class SvgSpriteModule extends Module {
private readonly pluginConfig: Options;
private readonly configName: string;
private readonly config: SvgSpriteConfiguration;
2021-03-16 18:20:57 +00:00
2020-08-09 11:21:29 +00:00
private sprite: GeneratedSprite;
private spriteSvg: string;
private spriteCss: string;
private spriteJs: string;
2021-03-16 18:20:57 +00:00
2020-08-09 11:21:29 +00:00
private spriteAssetName: string;
private spriteAssetUrl: string;
constructor(context: string, pluginConfig: Options, configName: string, config: SvgSpriteConfiguration) {
2021-03-16 18:20:57 +00:00
super("javascript/dynamic", null);
2020-08-09 11:21:29 +00:00
this.pluginConfig = pluginConfig;
this.configName = configName;
this.config = config;
2021-03-16 18:20:57 +00:00
this.buildInfo = {};
this.clearDependenciesAndBlocks();
}
getSourceTypes() {
return TYPES;
2020-08-09 11:21:29 +00:00
}
identifier() {
2021-03-16 18:20:57 +00:00
return "svg-sprite/" + this.configName;
2020-08-09 11:21:29 +00:00
}
2021-03-16 18:20:57 +00:00
readableIdentifier() {
2020-08-09 11:21:29 +00:00
return `SVG sprite ` + this.configName;
}
2021-03-16 18:20:57 +00:00
needBuild(context, callback) {
2021-03-16 19:02:08 +00:00
context.fileSystemInfo.getContextHash(this.config.folder, (error, hash) => {
if(error) {
callback(error);
return;
}
const needBuild = this.buildMeta?.directoryHash !== hash;
callback(null, needBuild);
})
2020-08-09 11:21:29 +00:00
}
2021-03-16 19:02:08 +00:00
build(options, compilation: Compilation, resolver, fs, callback) {
this.buildAsync(options, compilation).then(() => {
callback();
}).catch(error => {
callback(error);
});
}
2020-08-09 11:21:29 +00:00
2021-03-16 19:02:08 +00:00
private async buildAsync(options_, compilation: Compilation) {
2020-08-09 11:21:29 +00:00
this.buildMeta = {
async: false,
exportsType: undefined
};
2021-03-16 19:02:08 +00:00
this.buildInfo = {
cacheable: true,
2021-03-16 18:20:57 +00:00
assets: {},
};
2020-08-09 11:21:29 +00:00
if(this.spriteAssetName) {
delete compilation.assets[this.spriteAssetName];
}
2021-03-16 19:02:08 +00:00
this.buildMeta.directoryHash = await new Promise((resolve, reject) => {
compilation.fileSystemInfo.getContextHash(this.config.folder, (err, hash) => {
if(err) {
reject(err);
} else {
resolve(hash);
}
});
});
compilation.logger.info("Building SVG sprite for configuration %s (Hash: %s).", this.configName, this.buildMeta.directoryHash);
const files = await SystemFs.readdir(this.config.folder);
this.sprite = await generateSprite(files.map(file => path.join(this.config.folder, file)));
2020-08-09 11:21:29 +00:00
2021-03-16 19:02:08 +00:00
this.spriteSvg = await generateSpriteSvg(this.sprite);
this.spriteAssetName = "sprite-" + sha1(this.spriteSvg).substr(-20) + ".svg";
this.spriteAssetUrl = (this.pluginConfig.publicPath || "") + this.spriteAssetName;
2020-08-09 11:21:29 +00:00
2021-03-16 19:02:08 +00:00
this.buildInfo.assets[this.spriteAssetName] = new RawSource(this.spriteSvg);
2020-08-09 11:21:29 +00:00
2021-03-16 19:02:08 +00:00
this.spriteCss = "";
for(const cssOption of this.config.cssOptions) {
this.spriteCss += await generateSpriteCss(cssOption, this.config.cssClassPrefix, this.sprite, this.spriteAssetUrl);
}
2020-08-09 11:21:29 +00:00
2021-03-16 19:02:08 +00:00
this.spriteJs = await generateSpriteJs(this.config.dtsOptions, this.sprite, this.spriteAssetUrl, this.config.cssClassPrefix);
2020-08-09 11:21:29 +00:00
2021-03-16 19:02:08 +00:00
const dtsContent = await generateSpriteDts(this.config.dtsOptions, this.configName, this.sprite, this.config.cssClassPrefix, this.pluginConfig.modulePrefix, this.config.folder);
await SystemFs.writeFile(path.join(this.pluginConfig.dtsOutputFolder, this.configName + ".d.ts"), dtsContent);
compilation.logger.info("SVG sprite configuration %s contains %d/%d sprites", this.configName, this.sprite.entries.length, files.length);
2020-08-09 11:21:29 +00:00
}
2021-03-16 18:20:57 +00:00
codeGeneration(context) {
const sources = new Map();
2020-08-09 11:21:29 +00:00
const encodedCss = this.spriteCss
.replace(/%/g, "%25")
.replace(/"/g, "%22")
.replace(/\n/g, "%0A");
let lines = [];
lines.push(`/* initialize css */`);
lines.push(`var element = document.createElement("style");`);
lines.push(`element.innerText = decodeURIComponent("${encodedCss}");`);
lines.push(`document.head.append(element);`);
lines.push(``);
lines.push(`/* initialize typescript objects */`);
lines.push(...this.spriteJs.split("\n"));
2021-03-16 18:20:57 +00:00
sources.set("javascript", new RawSource(lines.join("\n")));
return { sources: sources, runtimeRequirements: RUNTIME_REQUIREMENTS };
2020-08-09 11:21:29 +00:00
}
size() {
return 12;
}
updateHash(hash, chunkGraph) {
hash.update("svg-sprite module");
hash.update(this.configName || "none");
hash.update(this.spriteCss || "none");
hash.update(this.spriteSvg || "none");
super.updateHash(hash, chunkGraph);
}
2021-03-16 18:20:57 +00:00
addReason(_requestModule, _dependency) { }
2021-03-16 19:02:08 +00:00
addCacheDependencies(
fileDependencies,
contextDependencies,
missingDependencies,
buildDependencies
) {
contextDependencies.add(this.config.folder);
}
2020-08-09 11:21:29 +00:00
}
2021-03-16 18:20:57 +00:00
2020-08-09 11:21:29 +00:00
export class SpriteGenerator {
readonly options: Options;
constructor(options: Options) {
this.options = options || {} as any;
this.options.configurations = this.options.configurations || {};
this.options.modulePrefix = this.options.modulePrefix || "svg-sprites/";
}
2021-03-16 18:20:57 +00:00
apply(compiler: Compiler) {
compiler.hooks.normalModuleFactory.tap("SpriteGenerator", normalModuleFactory => {
normalModuleFactory.hooks.resolve.tap("SpriteGenerator", resolveData => {
if(!resolveData.request.startsWith(this.options.modulePrefix)) {
return;
}
const configName = resolveData.request.substr(this.options.modulePrefix.length);
if(!this.options.configurations[configName]) {
2020-08-09 11:21:29 +00:00
return;
}
2021-03-16 18:20:57 +00:00
return new SvgSpriteModule(resolveData.request, this.options, configName, this.options.configurations[configName]);
2020-08-09 11:21:29 +00:00
});
});
}
}