wip
# Conflicts: # codecs/cpp.Dockerfile # codecs/imagequant/example.html # codecs/webp/dec/webp_dec.d.ts # codecs/webp/dec/webp_dec.js # codecs/webp/dec/webp_dec.wasm # codecs/webp/enc/webp_enc.d.ts # codecs/webp/enc/webp_enc.js # codecs/webp/enc/webp_enc.wasm # package-lock.json # package.json # src/codecs/tiny.webp # src_old/codecs/encoders.ts # src_old/codecs/processor-worker/tiny.avif # src_old/codecs/processor-worker/tiny.webp # src_old/codecs/tiny.webp # src_old/components/compress/index.tsx # src_old/lib/util.ts # src_old/sw/util.ts
7
.gitignore
vendored
@@ -1,6 +1,9 @@
|
||||
.tmp
|
||||
node_modules
|
||||
/build
|
||||
/*.log
|
||||
*.scss.d.ts
|
||||
*.css.d.ts
|
||||
build
|
||||
*.o
|
||||
|
||||
# Auto-generated by lib/image-worker-plugin.js
|
||||
src/image-worker/index.ts
|
||||
|
||||
4
.prettierrc.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
18
_headers.ejs
@@ -1,18 +0,0 @@
|
||||
# Long-term cache by default.
|
||||
/*
|
||||
Cache-Control: max-age=31536000
|
||||
|
||||
# And here are the exceptions:
|
||||
/
|
||||
Cache-Control: no-cache
|
||||
|
||||
/serviceworker.js
|
||||
Cache-Control: no-cache
|
||||
|
||||
/manifest.json
|
||||
Cache-Control: must-revalidate, max-age=3600
|
||||
|
||||
# URLs in /assets do not include a hash and are mutable.
|
||||
# But it isn't a big deal if the user gets an old version.
|
||||
/assets/*
|
||||
Cache-Control: must-revalidate, max-age=3600
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM emscripten/emsdk:1.40.0
|
||||
FROM emscripten/emsdk:2.0.3
|
||||
RUN apt-get update && apt-get install -qqy autoconf libtool pkg-config
|
||||
ENV CFLAGS "-Os -flto"
|
||||
ENV CXXFLAGS "${CFLAGS} -std=c++17"
|
||||
|
||||
@@ -18,7 +18,9 @@ all: $(OUT_JS)
|
||||
--closure 1 \
|
||||
-s ALLOW_MEMORY_GROWTH=1 \
|
||||
-s MODULARIZE=1 \
|
||||
-s 'EXPORT_NAME="$(basename $(@F))"' \
|
||||
-s TEXTDECODER=2 \
|
||||
-s ENVIRONMENT='worker' \
|
||||
-s EXPORT_ES6=1 \
|
||||
-o $@ \
|
||||
$+
|
||||
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<style>
|
||||
canvas {
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
</style>
|
||||
<script src='imagequant.js'></script>
|
||||
<script>
|
||||
const Module = imagequant();
|
||||
<script type="module">
|
||||
import imagequant from './imagequant.js';
|
||||
|
||||
async function loadImage(src) {
|
||||
// Load image
|
||||
const img = document.createElement('img');
|
||||
img.src = src;
|
||||
await new Promise(resolve => img.onload = resolve);
|
||||
await new Promise((resolve) => (img.onload = resolve));
|
||||
// Make canvas same size as image
|
||||
const canvas = document.createElement('canvas');
|
||||
[canvas.width, canvas.height] = [img.width, img.height];
|
||||
@@ -22,19 +21,32 @@
|
||||
return ctx.getImageData(0, 0, img.width, img.height);
|
||||
}
|
||||
|
||||
Module.onRuntimeInitialized = async _ => {
|
||||
console.log('Version:', Module.version().toString(16));
|
||||
async function main() {
|
||||
const module = await imagequant();
|
||||
|
||||
console.log('Version:', module.version().toString(16));
|
||||
const image = await loadImage('../example.png');
|
||||
// const rawImage = Module.quantize(image.data, image.width, image.height, 256, 1.0);
|
||||
const rawImage = Module.zx_quantize(image.data, image.width, image.height, 1.0);
|
||||
const rawImage = module.quantize(
|
||||
image.data,
|
||||
image.width,
|
||||
image.height,
|
||||
256,
|
||||
1.0,
|
||||
);
|
||||
console.log('done');
|
||||
|
||||
const imageData = new ImageData(new Uint8ClampedArray(rawImage.buffer), image.width, image.height);
|
||||
const imageData = new ImageData(
|
||||
new Uint8ClampedArray(rawImage.buffer),
|
||||
image.width,
|
||||
image.height,
|
||||
);
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
document.body.appendChild(canvas);
|
||||
};
|
||||
}
|
||||
|
||||
main();
|
||||
</script>
|
||||
|
||||
21
codecs/imagequant/imagequant.d.ts
vendored
@@ -1,6 +1,19 @@
|
||||
interface QuantizerModule extends EmscriptenWasm.Module {
|
||||
quantize(data: BufferSource, width: number, height: number, numColors: number, dither: number): Uint8ClampedArray;
|
||||
zx_quantize(data: BufferSource, width: number, height: number, dither: number): Uint8ClampedArray;
|
||||
export interface QuantizerModule extends EmscriptenWasm.Module {
|
||||
quantize(
|
||||
data: BufferSource,
|
||||
width: number,
|
||||
height: number,
|
||||
numColors: number,
|
||||
dither: number,
|
||||
): Uint8ClampedArray;
|
||||
zx_quantize(
|
||||
data: BufferSource,
|
||||
width: number,
|
||||
height: number,
|
||||
dither: number,
|
||||
): Uint8ClampedArray;
|
||||
}
|
||||
|
||||
export default function(opts: EmscriptenWasm.ModuleOpts): QuantizerModule;
|
||||
declare var moduleFactory: EmscriptenWasm.ModuleFactory<QuantizerModule>;
|
||||
|
||||
export default moduleFactory;
|
||||
|
||||
@@ -14,11 +14,13 @@ all: $(OUT_JS)
|
||||
-I $(CODEC_DIR) \
|
||||
${CXXFLAGS} \
|
||||
${LDFLAGS} \
|
||||
--bind \
|
||||
--closure 1 \
|
||||
--bind \
|
||||
-s ALLOW_MEMORY_GROWTH=1 \
|
||||
-s MODULARIZE=1 \
|
||||
-s 'EXPORT_NAME="$(basename $(@F))"' \
|
||||
-s TEXTDECODER=2 \
|
||||
-s ENVIRONMENT='worker' \
|
||||
-s EXPORT_ES6=1 \
|
||||
-o $@ \
|
||||
$+
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<!doctype html>
|
||||
<script src='mozjpeg_enc.js'></script>
|
||||
<script>
|
||||
const module = mozjpeg_enc();
|
||||
<!DOCTYPE html>
|
||||
<script type="module">
|
||||
import mozjpeg_enc from './mozjpeg_enc.js';
|
||||
|
||||
async function loadImage(src) {
|
||||
// Load image
|
||||
const img = document.createElement('img');
|
||||
img.src = src;
|
||||
await new Promise(resolve => img.onload = resolve);
|
||||
await new Promise((resolve) => (img.onload = resolve));
|
||||
// Make canvas same size as image
|
||||
const canvas = document.createElement('canvas');
|
||||
[canvas.width, canvas.height] = [img.width, img.height];
|
||||
@@ -17,7 +16,9 @@
|
||||
return ctx.getImageData(0, 0, img.width, img.height);
|
||||
}
|
||||
|
||||
module.onRuntimeInitialized = async _ => {
|
||||
async function main() {
|
||||
const module = await mozjpeg_enc();
|
||||
|
||||
console.log('Version:', module.version().toString(16));
|
||||
const image = await loadImage('../example.png');
|
||||
const result = module.encode(image.data, image.width, image.height, {
|
||||
@@ -39,10 +40,12 @@
|
||||
chroma_quality: 75,
|
||||
});
|
||||
|
||||
const blob = new Blob([result], {type: 'image/jpeg'});
|
||||
const blob = new Blob([result], { type: 'image/jpeg' });
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
const img = document.createElement('img');
|
||||
img.src = blobURL;
|
||||
document.body.appendChild(img);
|
||||
};
|
||||
}
|
||||
|
||||
main();
|
||||
</script>
|
||||
|
||||
15
codecs/mozjpeg_enc/mozjpeg_enc.d.ts
vendored
@@ -1,7 +1,14 @@
|
||||
import { EncodeOptions } from '../../src/codecs/mozjpeg/encoder-meta';
|
||||
import { EncodeOptions } from 'image-worker/mozjpegEncode';
|
||||
|
||||
interface MozJPEGModule extends EmscriptenWasm.Module {
|
||||
encode(data: BufferSource, width: number, height: number, options: EncodeOptions): Uint8Array;
|
||||
export interface MozJPEGModule extends EmscriptenWasm.Module {
|
||||
encode(
|
||||
data: BufferSource,
|
||||
width: number,
|
||||
height: number,
|
||||
options: EncodeOptions,
|
||||
): Uint8Array;
|
||||
}
|
||||
|
||||
export default function(opts: EmscriptenWasm.ModuleOpts): MozJPEGModule;
|
||||
declare var moduleFactory: EmscriptenWasm.ModuleFactory<MozJPEGModule>;
|
||||
|
||||
export default moduleFactory;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
CODEC_URL := https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-1.0.2.tar.gz
|
||||
CODEC_URL := https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-1.1.0.tar.gz
|
||||
CODEC_DIR = node_modules/libwebp
|
||||
CODEC_OUT_RELATIVE = src/.libs/libwebp.a
|
||||
CODEC_OUT := $(addprefix $(CODEC_DIR)/, $(CODEC_OUT_RELATIVE))
|
||||
@@ -18,7 +18,9 @@ all: $(OUT_JS)
|
||||
--closure 1 \
|
||||
-s ALLOW_MEMORY_GROWTH=1 \
|
||||
-s MODULARIZE=1 \
|
||||
-s 'EXPORT_NAME="$(basename $(@F))"' \
|
||||
-s TEXTDECODER=2 \
|
||||
-s ENVIRONMENT='worker' \
|
||||
-s EXPORT_ES6=1 \
|
||||
-o $@ \
|
||||
$+
|
||||
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
<!doctype html>
|
||||
<script src='webp_dec.js'></script>
|
||||
<script>
|
||||
const Module = webp_dec();
|
||||
<!DOCTYPE html>
|
||||
<script type="module">
|
||||
import webp_dec from './webp_dec.js';
|
||||
|
||||
async function loadFile(src) {
|
||||
const resp = await fetch(src);
|
||||
return await resp.arrayBuffer();
|
||||
}
|
||||
|
||||
Module.onRuntimeInitialized = async _ => {
|
||||
console.log('Version:', Module.version().toString(16));
|
||||
async function main() {
|
||||
const module = await webp_dec();
|
||||
console.log('Version:', module.version().toString(16));
|
||||
const image = await loadFile('../../example.webp');
|
||||
const imageData = Module.decode(image);
|
||||
const imageData = module.decode(image);
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = result.width;
|
||||
canvas.height = result.height;
|
||||
canvas.width = imageData.width;
|
||||
canvas.height = imageData.height;
|
||||
document.body.appendChild(canvas);
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
};
|
||||
}
|
||||
|
||||
main();
|
||||
</script>
|
||||
|
||||
6
codecs/webp/dec/webp_dec.d.ts
vendored
@@ -1,5 +1,7 @@
|
||||
interface WebPModule extends EmscriptenWasm.Module {
|
||||
export interface WebPModule extends EmscriptenWasm.Module {
|
||||
decode(data: BufferSource): ImageData | null;
|
||||
}
|
||||
|
||||
export default function(opts: EmscriptenWasm.ModuleOpts): WebPModule;
|
||||
declare var moduleFactory: EmscriptenWasm.ModuleFactory<WebPModule>;
|
||||
|
||||
export default moduleFactory;
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<!doctype html>
|
||||
<script src='webp_enc.js'></script>
|
||||
<script>
|
||||
const module = webp_enc();
|
||||
<!DOCTYPE html>
|
||||
<script type="module">
|
||||
import webp_enc from './webp_enc.js';
|
||||
|
||||
async function loadImage(src) {
|
||||
// Load image
|
||||
const img = document.createElement('img');
|
||||
img.src = src;
|
||||
await new Promise(resolve => img.onload = resolve);
|
||||
await new Promise((resolve) => (img.onload = resolve));
|
||||
// Make canvas same size as image
|
||||
const canvas = document.createElement('canvas');
|
||||
[canvas.width, canvas.height] = [img.width, img.height];
|
||||
@@ -17,7 +16,9 @@
|
||||
return ctx.getImageData(0, 0, img.width, img.height);
|
||||
}
|
||||
|
||||
module.onRuntimeInitialized = async _ => {
|
||||
async function main() {
|
||||
const module = await webp_enc();
|
||||
|
||||
console.log('Version:', module.version().toString(16));
|
||||
const image = await loadImage('../../example.png');
|
||||
const result = module.encode(image.data, image.width, image.height, {
|
||||
@@ -50,11 +51,13 @@
|
||||
use_sharp_yuv: 0,
|
||||
});
|
||||
console.log('size', result.length);
|
||||
const blob = new Blob([result], {type: 'image/webp'});
|
||||
const blob = new Blob([result], { type: 'image/webp' });
|
||||
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
const img = document.createElement('img');
|
||||
img.src = blobURL;
|
||||
document.body.appendChild(img);
|
||||
};
|
||||
}
|
||||
|
||||
main();
|
||||
</script>
|
||||
|
||||
15
codecs/webp/enc/webp_enc.d.ts
vendored
@@ -1,7 +1,14 @@
|
||||
import { EncodeOptions } from '../../../src/codecs/webp/encoder-meta';
|
||||
import { EncodeOptions } from 'image-worker/webpEncode';
|
||||
|
||||
interface WebPModule extends EmscriptenWasm.Module {
|
||||
encode(data: BufferSource, width: number, height: number, options: EncodeOptions): Uint8Array | null;
|
||||
export interface WebPModule extends EmscriptenWasm.Module {
|
||||
encode(
|
||||
data: BufferSource,
|
||||
width: number,
|
||||
height: number,
|
||||
options: EncodeOptions,
|
||||
): Uint8Array | null;
|
||||
}
|
||||
|
||||
export default function(opts: EmscriptenWasm.ModuleOpts): WebPModule;
|
||||
declare var moduleFactory: EmscriptenWasm.ModuleFactory<WebPModule>;
|
||||
|
||||
export default moduleFactory;
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
const DtsCreator = require('typed-css-modules');
|
||||
const chokidar = require('chokidar');
|
||||
const util = require('util');
|
||||
const sass = require('node-sass');
|
||||
const normalizePath = require('normalize-path');
|
||||
|
||||
const sassRender = util.promisify(sass.render);
|
||||
|
||||
async function sassToCss(path) {
|
||||
const result = await sassRender({ file: path });
|
||||
return result.css;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} Opts
|
||||
* @property {boolean} watch Watch for changes
|
||||
*/
|
||||
/**
|
||||
* Create typing files for CSS & SCSS.
|
||||
*
|
||||
* @param {string[]} rootPaths Paths to search within
|
||||
* @param {Opts} [opts={}] Options.
|
||||
*/
|
||||
function addCssTypes(rootPaths, opts = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const { watch = false } = opts;
|
||||
|
||||
const paths = [];
|
||||
const preReadyPromises = [];
|
||||
let ready = false;
|
||||
|
||||
for (const rootPath of rootPaths) {
|
||||
const rootPathUnix = normalizePath(rootPath);
|
||||
// Look for scss & css in each path.
|
||||
paths.push(rootPathUnix + '/**/*.scss');
|
||||
paths.push(rootPathUnix + '/**/*.css');
|
||||
}
|
||||
|
||||
// For simplicity, the watcher is used even if we're not watching.
|
||||
// If we're not watching, we stop the watcher after the initial files are found.
|
||||
const watcher = chokidar.watch(paths, {
|
||||
// Avoid processing already-processed files.
|
||||
ignored: '*.d.*',
|
||||
// Without this, travis and netlify builds never complete. I'm not sure why, but it might be
|
||||
// related to https://github.com/paulmillr/chokidar/pull/758
|
||||
persistent: watch,
|
||||
});
|
||||
|
||||
function change(path) {
|
||||
const promise = (async function() {
|
||||
const creator = new DtsCreator({ camelCase: true });
|
||||
const result = path.endsWith('.scss') ?
|
||||
await creator.create(path, await sassToCss(path)) :
|
||||
await creator.create(path);
|
||||
|
||||
await result.writeFile();
|
||||
})();
|
||||
|
||||
if (!ready) preReadyPromises.push(promise);
|
||||
}
|
||||
|
||||
watcher.on('change', change);
|
||||
watcher.on('add', change);
|
||||
|
||||
// 'ready' is when events have been fired for file discovery.
|
||||
watcher.on('ready', () => {
|
||||
ready = true;
|
||||
// Wait for the current set of processing to finish.
|
||||
Promise.all(preReadyPromises).then(resolve);
|
||||
// And if we're not watching, close the watcher.
|
||||
if (!watch) watcher.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = addCssTypes;
|
||||
@@ -1,47 +0,0 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const ejs = require('ejs');
|
||||
const AssetsPlugin = require('assets-webpack-plugin');
|
||||
|
||||
module.exports = class AssetTemplatePlugin extends AssetsPlugin {
|
||||
constructor(options) {
|
||||
options = options || {};
|
||||
if (!options.template) throw Error('AssetTemplatePlugin: template option is required.');
|
||||
super({
|
||||
useCompilerPath: true,
|
||||
filename: options.filename,
|
||||
processOutput: files => this._processOutput(files)
|
||||
});
|
||||
this._template = path.resolve(process.cwd(), options.template);
|
||||
const ignore = options.ignore || /(manifest\.json|\.DS_Store)$/;
|
||||
this._ignore = typeof ignore === 'function' ? ({ test: ignore }) : ignore;
|
||||
}
|
||||
|
||||
_processOutput(files) {
|
||||
const mapping = {
|
||||
all: [],
|
||||
byType: {},
|
||||
entries: {}
|
||||
};
|
||||
for (const entryName in files) {
|
||||
// non-entry-point-derived assets are collected under an empty string key
|
||||
// since that's a bit awkward, we'll call them "assets"
|
||||
const name = entryName === '' ? 'assets' : entryName;
|
||||
const listing = files[entryName];
|
||||
const entry = mapping.entries[name] = {
|
||||
all: [],
|
||||
byType: {}
|
||||
};
|
||||
for (let type in listing) {
|
||||
const list = [].concat(listing[type]).filter(file => !this._ignore.test(file));
|
||||
if (!list.length) continue;
|
||||
mapping.all = mapping.all.concat(list);
|
||||
mapping.byType[type] = (mapping.byType[type] || []).concat(list);
|
||||
entry.all = entry.all.concat(list);
|
||||
entry.byType[type] = (entry.byType[type] || []).concat(list);
|
||||
}
|
||||
}
|
||||
mapping.files = mapping.all;
|
||||
return ejs.render(fs.readFileSync(this._template, 'utf8'), mapping);
|
||||
}
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
let loaderUtils = require('loader-utils');
|
||||
let componentPath = require.resolve('./async-component');
|
||||
|
||||
module.exports = function () { };
|
||||
module.exports.pitch = function (remainingRequest) {
|
||||
this.cacheable && this.cacheable();
|
||||
let query = loaderUtils.getOptions(this) || {};
|
||||
let routeName = typeof query.name === 'function' ? query.name(this.resourcePath) : null;
|
||||
let name;
|
||||
if (routeName !== null) {
|
||||
name = routeName;
|
||||
}
|
||||
else if ('name' in query) {
|
||||
name = query.name;
|
||||
}
|
||||
else if ('formatName' in query) {
|
||||
name = query.formatName(this.resourcePath);
|
||||
}
|
||||
|
||||
return `
|
||||
import async from ${JSON.stringify(componentPath)};
|
||||
function load(cb) {
|
||||
require.ensure([], function (require) {
|
||||
cb( require(${loaderUtils.stringifyRequest(this, '!!' + remainingRequest)}) );
|
||||
}${name ? (', ' + JSON.stringify(name)) : ''});
|
||||
}
|
||||
export default async(load);
|
||||
`;
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
export default function (req) {
|
||||
function Async() {
|
||||
Component.call(this);
|
||||
|
||||
let b, old;
|
||||
this.componentWillMount = () => {
|
||||
b = this.base = this.nextBase || this.__b; // short circuits 1st render
|
||||
req(m => {
|
||||
this.setState({ child: m.default || m });
|
||||
});
|
||||
};
|
||||
|
||||
this.shouldComponentUpdate = (_, nxt) => {
|
||||
nxt = nxt.child === void 0;
|
||||
if (nxt && old === void 0 && !!b) {
|
||||
old = h(b.nodeName, { dangerouslySetInnerHTML: { __html: b.innerHTML } });
|
||||
}
|
||||
else {
|
||||
old = ''; // dump it
|
||||
}
|
||||
return !nxt;
|
||||
};
|
||||
|
||||
this.render = (p, s) => s.child ? h(s.child, p) : old;
|
||||
}
|
||||
(Async.prototype = new Component()).constructor = Async;
|
||||
return Async;
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
const util = require('util');
|
||||
const minimatch = require('minimatch');
|
||||
const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
|
||||
const WebWorkerTemplatePlugin = require('webpack/lib/webworker/WebWorkerTemplatePlugin');
|
||||
const ParserHelpers = require('webpack/lib/ParserHelpers');
|
||||
|
||||
const NAME = 'auto-sw-plugin';
|
||||
const JS_TYPES = ['auto', 'esm', 'dynamic'];
|
||||
|
||||
/**
|
||||
* Automatically finds and bundles Service Workers by looking for navigator.serviceWorker.register(..).
|
||||
* An Array of webpack assets is injected into the Service Worker bundle as a `BUILD_ASSETS` global.
|
||||
* Hidden and `.map` files are excluded by default, and this can be customized using the include & exclude options.
|
||||
* @example
|
||||
* // webpack config
|
||||
* plugins: [
|
||||
* new AutoSWPlugin({
|
||||
* exclude: [
|
||||
* '**\/.*', // don't expose hidden files (default)
|
||||
* '**\/*.map', // don't precache sourcemaps (default)
|
||||
* 'index.html' // don't cache the page itself
|
||||
* ]
|
||||
* })
|
||||
* ]
|
||||
* @param {Object} [options={}]
|
||||
* @param {string[]} [options.exclude] Minimatch pattern(s) of which assets to omit from BUILD_ASSETS.
|
||||
* @param {string[]} [options.include] Minimatch pattern(s) of assets to allow in BUILD_ASSETS.
|
||||
*/
|
||||
module.exports = class AutoSWPlugin {
|
||||
constructor(options) {
|
||||
this.options = Object.assign({
|
||||
exclude: [
|
||||
'**/*.map',
|
||||
'**/.*'
|
||||
]
|
||||
}, options || {});
|
||||
}
|
||||
|
||||
apply(compiler) {
|
||||
const serviceWorkers = [];
|
||||
|
||||
compiler.hooks.emit.tapPromise(NAME, compilation => this.emit(compiler, compilation, serviceWorkers));
|
||||
|
||||
compiler.hooks.normalModuleFactory.tap(NAME, (factory) => {
|
||||
for (const type of JS_TYPES) {
|
||||
factory.hooks.parser.for(`javascript/${type}`).tap(NAME, parser => {
|
||||
let counter = 0;
|
||||
|
||||
const processRegisterCall = expr => {
|
||||
const dep = parser.evaluateExpression(expr.arguments[0]);
|
||||
|
||||
if (!dep.isString()) {
|
||||
parser.state.module.warnings.push({
|
||||
message: 'navigator.serviceWorker.register() will only be bundled if passed a String literal.'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const filename = dep.string;
|
||||
const outputFilename = this.options.filename || 'serviceworker.js'
|
||||
const context = parser.state.current.context;
|
||||
serviceWorkers.push({
|
||||
outputFilename,
|
||||
filename,
|
||||
context
|
||||
});
|
||||
|
||||
const id = `__webpack__serviceworker__${++counter}`;
|
||||
ParserHelpers.toConstantDependency(parser, id)(expr.arguments[0]);
|
||||
return ParserHelpers.addParsedVariableToModule(parser, id, '__webpack_public_path__ + ' + JSON.stringify(outputFilename));
|
||||
};
|
||||
|
||||
parser.hooks.call.for('navigator.serviceWorker.register').tap(NAME, processRegisterCall);
|
||||
parser.hooks.call.for('self.navigator.serviceWorker.register').tap(NAME, processRegisterCall);
|
||||
parser.hooks.call.for('window.navigator.serviceWorker.register').tap(NAME, processRegisterCall);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createFilter(list) {
|
||||
const filters = [].concat(list);
|
||||
for (let i=0; i<filters.length; i++) {
|
||||
if (typeof filters[i] === 'string') {
|
||||
filters[i] = minimatch.filter(filters[i]);
|
||||
}
|
||||
}
|
||||
return filters;
|
||||
}
|
||||
|
||||
async emit(compiler, compilation, serviceWorkers) {
|
||||
let assetMapping = Object.keys(compilation.assets);
|
||||
if (this.options.include) {
|
||||
const filters = this.createFilter(this.options.include);
|
||||
assetMapping = assetMapping.filter(filename => {
|
||||
for (const filter of filters) {
|
||||
if (filter(filename)) return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
if (this.options.exclude) {
|
||||
const filters = this.createFilter(this.options.exclude);
|
||||
assetMapping = assetMapping.filter(filename => {
|
||||
for (const filter of filters) {
|
||||
if (filter(filename)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
await Promise.all(serviceWorkers.map(
|
||||
(serviceWorker, index) => this.compileServiceWorker(compiler, compilation, serviceWorker, index, assetMapping)
|
||||
));
|
||||
}
|
||||
|
||||
async compileServiceWorker(compiler, compilation, options, index, assetMapping) {
|
||||
const entryFilename = options.filename;
|
||||
|
||||
const chunkFilename = compiler.options.output.chunkFilename.replace(/\.([a-z]+)$/i, '.serviceworker.$1');
|
||||
const workerOptions = {
|
||||
filename: options.outputFilename, // chunkFilename.replace(/\.?\[(?:chunkhash|contenthash|hash)(:\d+(?::\d+)?)?\]/g, ''),
|
||||
chunkFilename: this.options.chunkFilename || chunkFilename,
|
||||
globalObject: 'self'
|
||||
};
|
||||
|
||||
const childCompiler = compilation.createChildCompiler(NAME, { filename: workerOptions.filename });
|
||||
(new WebWorkerTemplatePlugin(workerOptions)).apply(childCompiler);
|
||||
|
||||
/* The duplication DefinePlugin ends up causing is problematic (it doesn't hoist injections), so we'll do it manually. */
|
||||
// (new DefinePlugin({
|
||||
// BUILD_ASSETS: JSON.stringify(assetMapping)
|
||||
// })).apply(childCompiler);
|
||||
(new SingleEntryPlugin(options.context, entryFilename, workerOptions.filename)).apply(childCompiler);
|
||||
|
||||
const subCache = `subcache ${__dirname} ${entryFilename} ${index}`;
|
||||
let childCompilation;
|
||||
childCompiler.hooks.compilation.tap(NAME, c => {
|
||||
childCompilation = c;
|
||||
if (childCompilation.cache) {
|
||||
if (!childCompilation.cache[subCache]) childCompilation.cache[subCache] = {};
|
||||
childCompilation.cache = childCompilation.cache[subCache];
|
||||
}
|
||||
});
|
||||
|
||||
await (util.promisify(childCompiler.runAsChild.bind(childCompiler)))();
|
||||
|
||||
const versionVar = this.options.version ?
|
||||
`var VERSION = ${JSON.stringify(this.options.version)};` : '';
|
||||
const original = childCompilation.assets[workerOptions.filename].source();
|
||||
const source = `${versionVar}var BUILD_ASSETS=${JSON.stringify(assetMapping)};${original}`;
|
||||
childCompilation.assets[workerOptions.filename] = {
|
||||
source: () => source,
|
||||
size: () => Buffer.byteLength(source, 'utf8')
|
||||
};
|
||||
|
||||
Object.assign(compilation.assets, childCompilation.assets);
|
||||
}
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
const fs = require('fs');
|
||||
|
||||
/** A Webpack plugin to refresh file mtime values from disk before compiling.
|
||||
* This is used in order to account for SCSS-generated .d.ts files written
|
||||
* as part of compilation so they trigger only a single recompile per write.
|
||||
*
|
||||
* All credit for the technique and implementation goes to @reiv. See:
|
||||
* https://github.com/Jimdo/typings-for-css-modules-loader/issues/48#issuecomment-347036461
|
||||
*/
|
||||
module.exports = class WatchTimestampsPlugin {
|
||||
constructor(patterns) {
|
||||
this.patterns = patterns;
|
||||
}
|
||||
|
||||
apply(compiler) {
|
||||
compiler.hooks.watchRun.tapAsync('watch-timestamps-plugin', (watch, callback) => {
|
||||
const patterns = this.patterns;
|
||||
const timestamps = watch.fileTimestamps;
|
||||
|
||||
for (const filepath of timestamps) {
|
||||
if (patterns.some(pat => pat instanceof RegExp ? pat.test(filepath) : filepath.indexOf(pat) === 0)) {
|
||||
let time = fs.statSync(filepath).mtime;
|
||||
if (timestamps instanceof Map) timestamps.set(filepath, time);
|
||||
else timestamps[filepath] = time;
|
||||
}
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}
|
||||
};
|
||||
51
emscripten-wasm.d.ts → emscripten-types.d.ts
vendored
@@ -1,7 +1,11 @@
|
||||
// These types roughly model the object that the JS files generated by Emscripten define. Copied from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/emscripten/index.d.ts and turned into a type definition rather than a global to support our way of using Emscripten.
|
||||
// TODO(@surma): Upstream this?
|
||||
declare namespace EmscriptenWasm {
|
||||
type EnvironmentType = "WEB" | "NODE" | "SHELL" | "WORKER";
|
||||
type ModuleFactory<T extends Module = Module> = (
|
||||
moduleOverrides?: ModuleOpts,
|
||||
) => Promise<T>;
|
||||
|
||||
type EnvironmentType = 'WEB' | 'NODE' | 'SHELL' | 'WORKER';
|
||||
|
||||
// Options object for modularized Emscripten files. Shoe-horned by @surma.
|
||||
// FIXME: This an incomplete definition!
|
||||
@@ -16,9 +20,9 @@ declare namespace EmscriptenWasm {
|
||||
printErr(str: string): void;
|
||||
arguments: string[];
|
||||
environment: EnvironmentType;
|
||||
preInit: { (): void }[];
|
||||
preRun: { (): void }[];
|
||||
postRun: { (): void }[];
|
||||
preInit: { (): void }[];
|
||||
preRun: { (): void }[];
|
||||
postRun: { (): void }[];
|
||||
preinitializedWebGLContext: WebGLRenderingContext;
|
||||
noInitialRun: boolean;
|
||||
noExitRuntime: boolean;
|
||||
@@ -27,17 +31,25 @@ declare namespace EmscriptenWasm {
|
||||
wasmBinary: ArrayBuffer;
|
||||
|
||||
destroy(object: object): void;
|
||||
getPreloadedPackage(remotePackageName: string, remotePackageSize: number): ArrayBuffer;
|
||||
getPreloadedPackage(
|
||||
remotePackageName: string,
|
||||
remotePackageSize: number,
|
||||
): ArrayBuffer;
|
||||
instantiateWasm(
|
||||
imports: WebAssembly.Imports,
|
||||
successCallback: (module: WebAssembly.Module) => void
|
||||
imports: WebAssembly.Imports,
|
||||
successCallback: (module: WebAssembly.Module) => void,
|
||||
): WebAssembly.Exports;
|
||||
locateFile(url: string): string;
|
||||
onCustomMessage(event: MessageEvent): void;
|
||||
|
||||
Runtime: any;
|
||||
|
||||
ccall(ident: string, returnType: string | null, argTypes: string[], args: any[]): any;
|
||||
ccall(
|
||||
ident: string,
|
||||
returnType: string | null,
|
||||
argTypes: string[],
|
||||
args: any[],
|
||||
): any;
|
||||
cwrap(ident: string, returnType: string | null, argTypes: string[]): any;
|
||||
|
||||
setValue(ptr: number, value: any, type: string, noSafe?: boolean): void;
|
||||
@@ -50,7 +62,12 @@ declare namespace EmscriptenWasm {
|
||||
ALLOC_NONE: number;
|
||||
|
||||
allocate(slab: any, types: string, allocator: number, ptr: number): number;
|
||||
allocate(slab: any, types: string[], allocator: number, ptr: number): number;
|
||||
allocate(
|
||||
slab: any,
|
||||
types: string[],
|
||||
allocator: number,
|
||||
ptr: number,
|
||||
): number;
|
||||
|
||||
Pointer_stringify(ptr: number, length?: number): string;
|
||||
UTF16ToString(ptr: number): string;
|
||||
@@ -67,7 +84,7 @@ declare namespace EmscriptenWasm {
|
||||
HEAP8: Int8Array;
|
||||
HEAP16: Int16Array;
|
||||
HEAP32: Int32Array;
|
||||
HEAPU8: Uint8Array;
|
||||
HEAPU8: Uint8Array;
|
||||
HEAPU16: Uint16Array;
|
||||
HEAPU32: Uint32Array;
|
||||
HEAPF32: Float32Array;
|
||||
@@ -84,16 +101,23 @@ declare namespace EmscriptenWasm {
|
||||
addOnPostRun(cb: () => any): void;
|
||||
|
||||
// Tools
|
||||
intArrayFromString(stringy: string, dontAddNull?: boolean, length?: number): number[];
|
||||
intArrayFromString(
|
||||
stringy: string,
|
||||
dontAddNull?: boolean,
|
||||
length?: number,
|
||||
): number[];
|
||||
intArrayToString(array: number[]): string;
|
||||
writeStringToMemory(str: string, buffer: number, dontAddNull: boolean): void;
|
||||
writeStringToMemory(
|
||||
str: string,
|
||||
buffer: number,
|
||||
dontAddNull: boolean,
|
||||
): void;
|
||||
writeArrayToMemory(array: number[], buffer: number): void;
|
||||
writeAsciiToMemory(str: string, buffer: number, dontAddNull: boolean): void;
|
||||
|
||||
addRunDependency(id: any): void;
|
||||
removeRunDependency(id: any): void;
|
||||
|
||||
|
||||
preloadedImages: any;
|
||||
preloadedAudios: any;
|
||||
|
||||
@@ -104,4 +128,3 @@ declare namespace EmscriptenWasm {
|
||||
onRuntimeInitialized: () => void | null;
|
||||
}
|
||||
}
|
||||
|
||||
22
generic-tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2019",
|
||||
"downlevelIteration": true,
|
||||
"module": "esnext",
|
||||
"jsx": "react",
|
||||
"jsxFactory": "h",
|
||||
"strict": true,
|
||||
"moduleResolution": "node",
|
||||
"composite": true,
|
||||
"declarationMap": true,
|
||||
"baseUrl": "./",
|
||||
"rootDir": "./",
|
||||
"outDir": ".tmp/ts",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"paths": {
|
||||
"static-build/*": ["src/static-build/*"],
|
||||
"image-worker/*": ["src/image-worker/*"],
|
||||
"worker-main-shared/*": ["src/worker-main-shared/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
23
global.d.ts
vendored
@@ -1,23 +0,0 @@
|
||||
declare const __webpack_public_path__: string;
|
||||
declare const PRERENDER: boolean;
|
||||
|
||||
declare interface NodeModule {
|
||||
hot: any;
|
||||
}
|
||||
|
||||
declare interface Window {
|
||||
STATE: any;
|
||||
ga: typeof ga;
|
||||
}
|
||||
|
||||
declare namespace JSX {
|
||||
interface Element { }
|
||||
interface IntrinsicElements { }
|
||||
interface HTMLAttributes {
|
||||
decoding?: string;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'classnames' {
|
||||
export default function classnames(...args: any[]): string;
|
||||
}
|
||||
75
lib/asset-plugin.js
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { promises as fs } from 'fs';
|
||||
import { basename } from 'path';
|
||||
|
||||
const defaultOpts = {
|
||||
prefix: 'url',
|
||||
};
|
||||
|
||||
export default function urlPlugin(opts) {
|
||||
opts = Object.assign({}, defaultOpts, opts);
|
||||
|
||||
/** @type {Map<string, Buffer>} */
|
||||
let assetIdToSourceBuffer;
|
||||
|
||||
const prefix = opts.prefix + ':';
|
||||
return {
|
||||
name: 'url-plugin',
|
||||
buildStart() {
|
||||
assetIdToSourceBuffer = new Map();
|
||||
},
|
||||
augmentChunkHash(info) {
|
||||
// Get the sources for all assets imported by this chunk.
|
||||
const buffers = Object.keys(info.modules)
|
||||
.map((moduleId) => assetIdToSourceBuffer.get(moduleId))
|
||||
.filter(Boolean);
|
||||
|
||||
if (buffers.length === 0) return;
|
||||
|
||||
for (const moduleId of Object.keys(info.modules)) {
|
||||
const buffer = assetIdToSourceBuffer.get(moduleId);
|
||||
if (buffer) buffers.push(buffer);
|
||||
}
|
||||
|
||||
const combinedBuffer =
|
||||
buffers.length === 1 ? buffers[0] : Buffer.concat(buffers);
|
||||
|
||||
return combinedBuffer;
|
||||
},
|
||||
async resolveId(id, importer) {
|
||||
if (!id.startsWith(prefix)) return;
|
||||
const realId = id.slice(prefix.length);
|
||||
const resolveResult = await this.resolve(realId, importer);
|
||||
|
||||
if (!resolveResult) {
|
||||
throw Error(`Cannot find ${realId}`);
|
||||
}
|
||||
// Add an additional .js to the end so it ends up with .js at the end in the _virtual folder.
|
||||
return prefix + resolveResult.id + '.js';
|
||||
},
|
||||
async load(id) {
|
||||
if (!id.startsWith(prefix)) return;
|
||||
const realId = id.slice(prefix.length, -'.js'.length);
|
||||
const source = await fs.readFile(realId);
|
||||
assetIdToSourceBuffer.set(id, source);
|
||||
this.addWatchFile(realId);
|
||||
|
||||
return `export default import.meta.ROLLUP_FILE_URL_${this.emitFile({
|
||||
type: 'asset',
|
||||
source,
|
||||
name: basename(realId),
|
||||
})}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
163
lib/client-bundle-plugin.js
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import rollup from 'rollup';
|
||||
|
||||
const prefix = 'client-bundle:';
|
||||
const entryPathPlaceholder = 'CLIENT_BUNDLE_PLUGIN_ENTRY_PATH';
|
||||
const importsPlaceholder = 'CLIENT_BUNDLE_PLUGIN_IMPORTS';
|
||||
|
||||
export function getDependencies(clientOutput, item) {
|
||||
const crawlDependencies = new Set([item.fileName]);
|
||||
|
||||
for (const fileName of crawlDependencies) {
|
||||
const chunk = clientOutput.find((v) => v.fileName === fileName);
|
||||
|
||||
for (const dep of chunk.imports) {
|
||||
crawlDependencies.add(dep);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't add self as dependency
|
||||
crawlDependencies.delete(item.fileName);
|
||||
|
||||
return [...crawlDependencies];
|
||||
}
|
||||
|
||||
export default function (inputOptions, outputOptions, resolveFileUrl) {
|
||||
let cache;
|
||||
let entryPointPlaceholderMap;
|
||||
let exportCounter;
|
||||
let clientBundle;
|
||||
let clientOutput;
|
||||
|
||||
return {
|
||||
name: 'client-bundle',
|
||||
buildStart() {
|
||||
entryPointPlaceholderMap = new Map();
|
||||
exportCounter = 0;
|
||||
},
|
||||
async resolveId(id, importer) {
|
||||
if (!id.startsWith(prefix)) return null;
|
||||
|
||||
const realId = id.slice(prefix.length);
|
||||
const resolveResult = await this.resolve(realId, importer);
|
||||
// Add an additional .js to the end so it ends up with .js at the end in the _virtual folder.
|
||||
if (resolveResult) return prefix + resolveResult.id + '.js';
|
||||
// This Rollup couldn't resolve it, but maybe the inner one can.
|
||||
return id + '.js';
|
||||
},
|
||||
load(id) {
|
||||
if (!id.startsWith(prefix)) return;
|
||||
|
||||
const realId = id.slice(prefix.length, -'.js'.length);
|
||||
|
||||
exportCounter++;
|
||||
|
||||
entryPointPlaceholderMap.set(exportCounter, realId);
|
||||
|
||||
return [
|
||||
`export default import.meta.${entryPathPlaceholder + exportCounter};`,
|
||||
`export const imports = import.meta.${
|
||||
importsPlaceholder + exportCounter
|
||||
};`,
|
||||
].join('\n');
|
||||
},
|
||||
async buildEnd(error) {
|
||||
const entryPoints = [...entryPointPlaceholderMap.values()];
|
||||
// The static-build is done, so now we can perform our client build.
|
||||
// Exit early if there's nothing to build.
|
||||
if (error || entryPoints.length === 0) return;
|
||||
|
||||
clientBundle = await rollup.rollup({
|
||||
...inputOptions,
|
||||
cache,
|
||||
input: entryPoints,
|
||||
});
|
||||
|
||||
cache = clientBundle.cache;
|
||||
},
|
||||
async renderStart(staticBuildOutputOpts) {
|
||||
// The static-build has started generating output, so we can do the same for our client build.
|
||||
// Exit early if there's nothing to build.
|
||||
if (!clientBundle) return;
|
||||
const copiedOutputOptions = {
|
||||
assetFileNames: staticBuildOutputOpts.assetFileNames,
|
||||
};
|
||||
clientOutput = (
|
||||
await clientBundle.generate({
|
||||
...copiedOutputOptions,
|
||||
...outputOptions,
|
||||
})
|
||||
).output;
|
||||
},
|
||||
resolveImportMeta(property, { moduleId, format }) {
|
||||
// Pick up the placeholder exports we created earlier, and fill in the correct details.
|
||||
let num = undefined;
|
||||
|
||||
if (property.startsWith(entryPathPlaceholder)) {
|
||||
num = Number(property.slice(entryPathPlaceholder.length));
|
||||
} else if (property.startsWith(importsPlaceholder)) {
|
||||
num = Number(property.slice(importsPlaceholder.length));
|
||||
} else {
|
||||
// This isn't one of our placeholders.
|
||||
return;
|
||||
}
|
||||
|
||||
const id = entryPointPlaceholderMap.get(num);
|
||||
const clientEntry = clientOutput.find(
|
||||
(item) => item.facadeModuleId === id,
|
||||
);
|
||||
|
||||
if (property.startsWith(entryPathPlaceholder)) {
|
||||
return resolveFileUrl({
|
||||
fileName: clientEntry.fileName,
|
||||
moduleId,
|
||||
format,
|
||||
});
|
||||
}
|
||||
|
||||
const dependencies = getDependencies(clientOutput, clientEntry);
|
||||
|
||||
return (
|
||||
'[' +
|
||||
dependencies
|
||||
.map((item) => {
|
||||
const entry = clientOutput.find((v) => v.fileName === item);
|
||||
|
||||
return resolveFileUrl({
|
||||
fileName: entry.fileName,
|
||||
moduleId,
|
||||
format: outputOptions.format,
|
||||
});
|
||||
})
|
||||
.join(',') +
|
||||
']'
|
||||
);
|
||||
},
|
||||
async generateBundle(options, bundle) {
|
||||
// Exit early if there's nothing to build.
|
||||
if (!clientOutput) return;
|
||||
// Copy everything from the client bundle into the main bundle.
|
||||
for (const clientEntry of clientOutput) {
|
||||
// Skip if the file already exists
|
||||
if (clientEntry.fileName in bundle) continue;
|
||||
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
source: clientEntry.code || clientEntry.source,
|
||||
fileName: clientEntry.fileName,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
195
lib/css-plugin.js
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { promises as fsp, readFileSync } from 'fs';
|
||||
import { createHash } from 'crypto';
|
||||
import { promisify } from 'util';
|
||||
import { parse as parsePath, resolve as resolvePath, dirname } from 'path';
|
||||
|
||||
import postcss from 'postcss';
|
||||
import postCSSNested from 'postcss-nested';
|
||||
import postCSSUrl from 'postcss-url';
|
||||
import postCSSModules from 'postcss-modules';
|
||||
import postCSSImport from 'postcss-import';
|
||||
import postCSSSimpleVars from 'postcss-simple-vars';
|
||||
import cssNano from 'cssnano';
|
||||
import camelCase from 'lodash.camelcase';
|
||||
import glob from 'glob';
|
||||
|
||||
const globP = promisify(glob);
|
||||
|
||||
const moduleSuffix = '.css';
|
||||
const prefix = 'css-bundle:';
|
||||
const assetRe = new RegExp('/fake/path/to/asset/([^/]+)/', 'g');
|
||||
|
||||
export default function (resolveFileUrl) {
|
||||
/** @type {string[]} */
|
||||
let emittedCSSIds;
|
||||
/** @type {Map<string, string>} */
|
||||
let hashToId;
|
||||
/** @type {Map<string, { module: string, css: string }>} */
|
||||
let pathToResult;
|
||||
|
||||
async function loadBundledCSS(path, rollupContext) {
|
||||
const parsedPath = parsePath(path);
|
||||
|
||||
if (!pathToResult.has(path)) {
|
||||
throw Error(`Cannot find ${path} in pathToResult`);
|
||||
}
|
||||
|
||||
const file = pathToResult.get(path).css;
|
||||
|
||||
const cssResult = await postcss([
|
||||
postCSSImport({
|
||||
path: ['./', 'static-build/'],
|
||||
load(path) {
|
||||
if (!pathToResult.has(path)) {
|
||||
throw Error(`Cannot find ${path} in pathToResult`);
|
||||
}
|
||||
return pathToResult.get(path).css;
|
||||
},
|
||||
}),
|
||||
postCSSUrl({
|
||||
url: ({ relativePath, url }) => {
|
||||
if (/^https?:\/\//.test(url)) return url;
|
||||
const parsedPath = parsePath(relativePath);
|
||||
const source = readFileSync(resolvePath(dirname(path), relativePath));
|
||||
const fileId = rollupContext.emitFile({
|
||||
type: 'asset',
|
||||
name: parsedPath.base,
|
||||
source,
|
||||
});
|
||||
const hash = createHash('md5');
|
||||
hash.update(source);
|
||||
const md5 = hash.digest('hex');
|
||||
hashToId.set(md5, fileId);
|
||||
return `/fake/path/to/asset/${md5}/`;
|
||||
},
|
||||
}),
|
||||
cssNano,
|
||||
]).process(file, {
|
||||
from: path,
|
||||
});
|
||||
|
||||
const fileId = rollupContext.emitFile({
|
||||
type: 'asset',
|
||||
source: cssResult.css,
|
||||
name: parsedPath.base,
|
||||
});
|
||||
|
||||
emittedCSSIds.push(fileId);
|
||||
|
||||
return [
|
||||
`export default import.meta.ROLLUP_FILE_URL_${fileId}`,
|
||||
`export const inline = ${JSON.stringify(cssResult.css)}`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'css',
|
||||
async buildStart() {
|
||||
emittedCSSIds = [];
|
||||
hashToId = new Map();
|
||||
pathToResult = new Map();
|
||||
|
||||
const cssPaths = await globP('src/static-build/**/*.css', {
|
||||
nodir: true,
|
||||
absolute: true,
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
cssPaths.map(async (path) => {
|
||||
this.addWatchFile(path);
|
||||
const file = await fsp.readFile(path);
|
||||
let moduleJSON;
|
||||
|
||||
const cssResult = await postcss([
|
||||
postCSSNested,
|
||||
postCSSSimpleVars(),
|
||||
postCSSModules({
|
||||
getJSON(_, json) {
|
||||
moduleJSON = json;
|
||||
},
|
||||
}),
|
||||
]).process(file, {
|
||||
from: undefined,
|
||||
});
|
||||
|
||||
const cssClassExports = Object.entries(moduleJSON).map(
|
||||
([key, val]) =>
|
||||
`export const $${camelCase(key)} = ${JSON.stringify(val)};`,
|
||||
);
|
||||
|
||||
const defs = Object.keys(moduleJSON)
|
||||
.map((key) => `export const $${camelCase(key)}: string;`)
|
||||
.join('\n');
|
||||
|
||||
const defPath = path + '.d.ts';
|
||||
const currentDefFileContent = await fsp
|
||||
.readFile(defPath, { encoding: 'utf8' })
|
||||
.catch(() => undefined);
|
||||
|
||||
// Only write the file if contents have changed, otherwise it causes a loop with
|
||||
// TypeScript's file watcher.
|
||||
if (defs !== currentDefFileContent) {
|
||||
await fsp.writeFile(defPath, defs);
|
||||
}
|
||||
|
||||
pathToResult.set(path, {
|
||||
module: cssClassExports.join('\n'),
|
||||
css: cssResult.css,
|
||||
});
|
||||
}),
|
||||
);
|
||||
},
|
||||
async resolveId(id, importer) {
|
||||
if (!id.startsWith(prefix)) return;
|
||||
const resolved = await this.resolve(id.slice(prefix.length), importer);
|
||||
if (!resolved) throw Error(`Couldn't resolve ${id} from ${importer}`);
|
||||
return prefix + resolved.id;
|
||||
},
|
||||
async load(id) {
|
||||
if (id.startsWith(prefix)) {
|
||||
return loadBundledCSS(id.slice(prefix.length), this);
|
||||
}
|
||||
if (id.endsWith(moduleSuffix)) {
|
||||
if (!pathToResult.has(id)) {
|
||||
throw Error(`Cannot find ${id} in pathToResult`);
|
||||
}
|
||||
|
||||
return pathToResult.get(id).module;
|
||||
}
|
||||
},
|
||||
async generateBundle(options, bundle) {
|
||||
const cssAssets = emittedCSSIds.map((id) => this.getFileName(id));
|
||||
|
||||
for (const cssAsset of cssAssets) {
|
||||
bundle[cssAsset].source = bundle[cssAsset].source.replace(
|
||||
assetRe,
|
||||
(_, p1) =>
|
||||
resolveFileUrl({
|
||||
fileName: this.getFileName(hashToId.get(p1)),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
for (const item of Object.values(bundle)) {
|
||||
if (item.type === 'asset') continue;
|
||||
item.code = item.code.replace(assetRe, (match, p1) =>
|
||||
resolveFileUrl({
|
||||
fileName: this.getFileName(hashToId.get(p1)),
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
37
lib/emit-files-plugin.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import * as path from 'path';
|
||||
import { promises as fs } from 'fs';
|
||||
import glob from 'glob';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const globP = promisify(glob);
|
||||
|
||||
export default function emitFiles({ root, include }) {
|
||||
return {
|
||||
name: 'emit-files-plugin',
|
||||
async buildStart() {
|
||||
const paths = await globP(include, { nodir: true, cwd: root });
|
||||
|
||||
await Promise.all(
|
||||
paths.map(async (filePath) => {
|
||||
return this.emitFile({
|
||||
type: 'asset',
|
||||
source: await fs.readFile(path.join(root, filePath)),
|
||||
fileName: 'static/' + filePath,
|
||||
});
|
||||
}),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
55
lib/image-worker-plugin.js
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { promisify } from 'util';
|
||||
import * as path from 'path';
|
||||
import { promises as fsp } from 'fs';
|
||||
|
||||
import glob from 'glob';
|
||||
|
||||
const globP = promisify(glob);
|
||||
|
||||
export default function () {
|
||||
return {
|
||||
name: 'image-worker-plugin',
|
||||
async buildStart() {
|
||||
const base = path.join(process.cwd(), 'src', 'image-worker');
|
||||
const dirs = (
|
||||
await globP('*/', {
|
||||
cwd: base,
|
||||
})
|
||||
).map((dir) => dir.slice(0, -1));
|
||||
|
||||
const file = [
|
||||
`// This file is autogenerated by lib/image-worker-plugin.js`,
|
||||
`import { expose } from 'comlink';`,
|
||||
`import { timed } from './util';`,
|
||||
dirs.map((dir) => `import ${dir} from './${dir}';`),
|
||||
`const exports = {`,
|
||||
dirs.map((dir) => [
|
||||
` ${dir}(`,
|
||||
` ...args: Parameters<typeof ${dir}>`,
|
||||
` ): ReturnType<typeof ${dir}> {`,
|
||||
` return timed('${dir}', () => ${dir}(...args));`,
|
||||
` },`,
|
||||
]),
|
||||
`};`,
|
||||
`export type ProcessorWorkerApi = typeof exports;`,
|
||||
`expose(exports, self);`,
|
||||
]
|
||||
.flat(Infinity)
|
||||
.join('\n');
|
||||
|
||||
await fsp.writeFile(path.join(base, 'index.ts'), file);
|
||||
},
|
||||
};
|
||||
}
|
||||
19
lib/move-output.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
// Move .tmp/build/static to docs/
|
||||
const fs = require('fs');
|
||||
const del = require('del');
|
||||
const path = require('path');
|
||||
|
||||
del.sync('build');
|
||||
fs.renameSync(path.join('.tmp', 'build', 'static'), 'build');
|
||||
24
lib/node-external-plugin.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
// Check that a node module exists, but treat it as external.
|
||||
export default function () {
|
||||
return {
|
||||
name: 'node-external',
|
||||
resolveId(id) {
|
||||
try {
|
||||
require.resolve(id);
|
||||
return { id, external: true };
|
||||
} catch (err) {}
|
||||
},
|
||||
};
|
||||
}
|
||||
84
lib/omt.ejs
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Copyright 2018 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// If the loader is already loaded, just stop.
|
||||
if (!self.<%- amdFunctionName %>) {
|
||||
const singleRequire = async name => {
|
||||
if (name === 'require') return require;
|
||||
const url = '/c/' + name.slice(2) + '.js';
|
||||
name = './static' + url;
|
||||
if (registry[name]) return registry[name];
|
||||
|
||||
if (!registry[name]) {
|
||||
<% if (useEval) { %>
|
||||
const text = await fetch(url).then(resp => resp.text());
|
||||
eval(text);
|
||||
<% } else { %>
|
||||
if ("document" in self) {
|
||||
await new Promise(resolve => {
|
||||
const script = document.createElement("script");
|
||||
script.src = url;
|
||||
document.head.appendChild(script);
|
||||
script.onload = resolve;
|
||||
});
|
||||
} else {
|
||||
importScripts(url);
|
||||
}
|
||||
<% } %>
|
||||
}
|
||||
if (!registry[name]) {
|
||||
throw new Error(`Module ${name} didn’t register its module`);
|
||||
}
|
||||
return registry[name];
|
||||
};
|
||||
|
||||
const require = (names, resolve) => {
|
||||
Promise.all(names.map(singleRequire))
|
||||
.then(modules => resolve(modules.length === 1 ? modules[0] : modules));
|
||||
};
|
||||
|
||||
const registry = {
|
||||
require: Promise.resolve(require)
|
||||
};
|
||||
|
||||
self.<%- amdFunctionName %> = (moduleName, depsNames, factory) => {
|
||||
if (registry[moduleName]) {
|
||||
// Module is already loading or loaded.
|
||||
return;
|
||||
}
|
||||
registry[moduleName] = Promise.resolve().then(() => {
|
||||
let exports = {};
|
||||
const module = {
|
||||
uri: location.origin + moduleName.slice(1)
|
||||
};
|
||||
return Promise.all(
|
||||
depsNames.map(depName => {
|
||||
switch(depName) {
|
||||
case "exports":
|
||||
return exports;
|
||||
case "module":
|
||||
return module;
|
||||
default:
|
||||
return singleRequire(depName);
|
||||
}
|
||||
})
|
||||
).then(deps => {
|
||||
const facValue = factory(...deps);
|
||||
if(!exports.default) {
|
||||
exports.default = facValue;
|
||||
}
|
||||
return exports;
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
36
lib/resolve-dirs-plugin.js
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { posix as pathUtils } from 'path';
|
||||
|
||||
export default function resolveDirs(paths) {
|
||||
const pathBaseDir = paths.map((path) => [
|
||||
pathUtils.basename(path),
|
||||
pathUtils.dirname(path),
|
||||
]);
|
||||
|
||||
return {
|
||||
name: 'resolve-dirs',
|
||||
async resolveId(id) {
|
||||
const match = pathBaseDir.find(
|
||||
([pathId]) => id === pathId || id.startsWith(pathId + '/'),
|
||||
);
|
||||
if (!match) return;
|
||||
const pathDir = match[1];
|
||||
const resolveResult = await this.resolve(`./${pathDir}/${id}`, './');
|
||||
if (!resolveResult) {
|
||||
throw new Error(`Couldn't find ${'./' + id}`);
|
||||
}
|
||||
return pathUtils.resolve(resolveResult.id);
|
||||
},
|
||||
};
|
||||
}
|
||||
34
lib/run-script.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { fork } from 'child_process';
|
||||
|
||||
export default function runScript(path) {
|
||||
return {
|
||||
name: 'run-script',
|
||||
writeBundle() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = fork(path, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
proc.on('exit', (code) => {
|
||||
if (code !== 0) {
|
||||
reject(Error('Static build failed'));
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
130
lib/simple-ts.js
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { spawn } from 'child_process';
|
||||
import { relative, join } from 'path';
|
||||
import { promises as fsp } from 'fs';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import * as ts from 'typescript';
|
||||
import glob from 'glob';
|
||||
|
||||
const globP = promisify(glob);
|
||||
|
||||
const extRe = /\.tsx?$/;
|
||||
|
||||
function loadConfig(mainPath) {
|
||||
const fileName = ts.findConfigFile(mainPath, ts.sys.fileExists);
|
||||
if (!fileName) throw Error('tsconfig not found');
|
||||
const text = ts.sys.readFile(fileName);
|
||||
const loadedConfig = ts.parseConfigFileTextToJson(fileName, text).config;
|
||||
const parsedTsConfig = ts.parseJsonConfigFileContent(
|
||||
loadedConfig,
|
||||
ts.sys,
|
||||
process.cwd(),
|
||||
undefined,
|
||||
fileName,
|
||||
);
|
||||
return parsedTsConfig;
|
||||
}
|
||||
|
||||
export default function simpleTS(mainPath, { noBuild, watch } = {}) {
|
||||
const config = loadConfig(mainPath);
|
||||
const args = ['-b', mainPath];
|
||||
|
||||
let tsBuildDone;
|
||||
|
||||
async function watchBuiltFiles(rollupContext) {
|
||||
const matches = await globP(config.options.outDir + '/**/*.js');
|
||||
for (const match of matches) rollupContext.addWatchFile(match);
|
||||
}
|
||||
|
||||
async function tsBuild(rollupContext) {
|
||||
if (tsBuildDone) {
|
||||
// Watch lists are cleared on each build, so we need to rewatch all the JS files.
|
||||
await watchBuiltFiles(rollupContext);
|
||||
return tsBuildDone;
|
||||
}
|
||||
if (noBuild) {
|
||||
return (tsBuildDone = Promise.resolve());
|
||||
}
|
||||
tsBuildDone = Promise.resolve().then(async () => {
|
||||
await new Promise((resolve) => {
|
||||
const proc = spawn('tsc', args, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
proc.on('exit', (code) => {
|
||||
if (code !== 0) {
|
||||
throw Error('TypeScript build failed');
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
await watchBuiltFiles(rollupContext);
|
||||
|
||||
if (watch) {
|
||||
tsBuildDone.then(() => {
|
||||
spawn('tsc', [...args, '--watch', '--preserveWatchOutput'], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return tsBuildDone;
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'simple-ts',
|
||||
resolveId(id, importer) {
|
||||
// If there isn't an importer, it's an entry point, so we don't need to resolve it relative
|
||||
// to something.
|
||||
if (!importer) return null;
|
||||
|
||||
const tsResolve = ts.resolveModuleName(
|
||||
id,
|
||||
importer,
|
||||
config.options,
|
||||
ts.sys,
|
||||
);
|
||||
|
||||
if (
|
||||
// It didn't find anything
|
||||
!tsResolve.resolvedModule ||
|
||||
// Or if it's linking to a definition file, it's something in node_modules,
|
||||
// or something local like css.d.ts
|
||||
tsResolve.resolvedModule.extension === '.d.ts'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return tsResolve.resolvedModule.resolvedFileName;
|
||||
},
|
||||
async load(id) {
|
||||
if (!extRe.test(id)) return null;
|
||||
|
||||
// TypeScript building is deferred until the first TS file load.
|
||||
// This allows prerequisites to happen first,
|
||||
// such as css.d.ts generation in css-plugin.
|
||||
await tsBuild(this);
|
||||
|
||||
// Look for the JS equivalent in the tmp folder
|
||||
const newId = join(
|
||||
config.options.outDir,
|
||||
relative(process.cwd(), id),
|
||||
).replace(extRe, '.js');
|
||||
|
||||
return fsp.readFile(newId, { encoding: 'utf8' });
|
||||
},
|
||||
};
|
||||
}
|
||||
23
missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="./emscripten-types.d.ts" />
|
||||
|
||||
declare module 'url:*' {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module 'omt:*' {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
17399
package-lock.json
generated
99
package.json
@@ -1,77 +1,46 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "squoosh",
|
||||
"version": "1.12.0",
|
||||
"version": "2.0.0",
|
||||
"license": "apache-2.0",
|
||||
"scripts": {
|
||||
"start": "webpack-dev-server --host 0.0.0.0 --hot",
|
||||
"build": "webpack -p",
|
||||
"lint": "tslint -c tslint.json -p tsconfig.json -t verbose",
|
||||
"lintfix": "tslint -c tslint.json -p tsconfig.json -t verbose --fix 'src/**/*.{ts,tsx,js,jsx}'",
|
||||
"sizereport": "sizereport --config"
|
||||
"build": "rollup -c && node lib/move-output.js",
|
||||
"dev": "rollup -cw & npm run serve",
|
||||
"serve": "serve --config server.json .tmp/build/static"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^15.0.0",
|
||||
"@rollup/plugin-node-resolve": "^9.0.0",
|
||||
"@surma/rollup-plugin-off-main-thread": "^1.4.1",
|
||||
"@types/node": "^14.10.1",
|
||||
"comlink": "^4.3.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"del": "^5.1.0",
|
||||
"husky": "^4.3.0",
|
||||
"lint-staged": "^10.3.0",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"postcss": "^7.0.32",
|
||||
"postcss-import": "^12.0.1",
|
||||
"postcss-modules": "^3.2.2",
|
||||
"postcss-nested": "^4.2.3",
|
||||
"postcss-simple-vars": "^5.0.2",
|
||||
"postcss-url": "^8.0.0",
|
||||
"preact": "^10.4.8",
|
||||
"preact-render-to-string": "^5.1.10",
|
||||
"prettier": "^2.1.1",
|
||||
"rollup": "^2.26.11",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"serve": "^11.3.2",
|
||||
"typescript": "^4.0.2"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npm run lint"
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "10.14.15",
|
||||
"@types/pretty-bytes": "5.1.0",
|
||||
"@types/webassembly-js-api": "0.0.3",
|
||||
"@webcomponents/custom-elements": "1.2.4",
|
||||
"@webpack-cli/serve": "0.1.8",
|
||||
"assets-webpack-plugin": "3.9.10",
|
||||
"chalk": "2.4.2",
|
||||
"chokidar": "3.0.2",
|
||||
"classnames": "2.2.6",
|
||||
"clean-webpack-plugin": "1.0.1",
|
||||
"comlink": "3.1.1",
|
||||
"copy-webpack-plugin": "5.0.4",
|
||||
"critters-webpack-plugin": "2.4.0",
|
||||
"css-loader": "1.0.1",
|
||||
"ejs": "2.6.2",
|
||||
"escape-string-regexp": "2.0.0",
|
||||
"exports-loader": "0.7.0",
|
||||
"file-drop-element": "0.2.0",
|
||||
"file-loader": "4.2.0",
|
||||
"gzip-size": "5.1.1",
|
||||
"html-webpack-plugin": "3.2.0",
|
||||
"husky": "3.0.4",
|
||||
"idb-keyval": "3.2.0",
|
||||
"linkstate": "1.1.1",
|
||||
"loader-utils": "1.2.3",
|
||||
"mini-css-extract-plugin": "0.8.0",
|
||||
"minimatch": "3.0.4",
|
||||
"node-fetch": "2.6.0",
|
||||
"node-sass": "4.13.0",
|
||||
"normalize-path": "^3.0.0",
|
||||
"optimize-css-assets-webpack-plugin": "5.0.1",
|
||||
"pointer-tracker": "2.0.3",
|
||||
"preact": "8.4.2",
|
||||
"prerender-loader": "1.3.0",
|
||||
"pretty-bytes": "5.3.0",
|
||||
"progress-bar-webpack-plugin": "1.12.1",
|
||||
"raw-loader": "3.1.0",
|
||||
"readdirp": "3.1.2",
|
||||
"sass-loader": "7.3.1",
|
||||
"script-ext-html-webpack-plugin": "2.1.4",
|
||||
"source-map-loader": "0.2.4",
|
||||
"style-loader": "1.0.0",
|
||||
"terser-webpack-plugin": "1.4.1",
|
||||
"travis-size-report": "1.1.0",
|
||||
"ts-loader": "6.0.3",
|
||||
"tslint": "5.19.0",
|
||||
"tslint-config-airbnb": "5.11.1",
|
||||
"tslint-config-semistandard": "8.0.1",
|
||||
"tslint-react": "4.0.0",
|
||||
"typed-css-modules": "0.4.2",
|
||||
"typescript": "3.5.3",
|
||||
"url-loader": "2.1.0",
|
||||
"webpack": "4.39.3",
|
||||
"webpack-bundle-analyzer": "3.4.1",
|
||||
"webpack-cli": "3.3.4",
|
||||
"webpack-dev-server": "3.8.0",
|
||||
"worker-plugin": "3.1.0"
|
||||
"lint-staged": {
|
||||
"*.{js,css,json,md,ts,tsx}": [
|
||||
"prettier --write"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
106
rollup.config.js
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import * as path from 'path';
|
||||
import { promises as fsp } from 'fs';
|
||||
import del from 'del';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
import OMT from '@surma/rollup-plugin-off-main-thread';
|
||||
|
||||
import simpleTS from './lib/simple-ts';
|
||||
import clientBundlePlugin from './lib/client-bundle-plugin';
|
||||
import nodeExternalPlugin from './lib/node-external-plugin';
|
||||
import cssPlugin from './lib/css-plugin';
|
||||
import assetPlugin from './lib/asset-plugin';
|
||||
import resolveDirsPlugin from './lib/resolve-dirs-plugin';
|
||||
import runScript from './lib/run-script';
|
||||
import emitFiles from './lib/emit-files-plugin';
|
||||
import imageWorkerPlugin from './lib/image-worker-plugin';
|
||||
|
||||
function resolveFileUrl({ fileName }) {
|
||||
return JSON.stringify(fileName.replace(/^static\//, '/'));
|
||||
}
|
||||
|
||||
// With AMD output, Rollup always uses document.baseURI, which breaks in workers.
|
||||
// This fixes it:
|
||||
function resolveImportMeta(property, { chunkId }) {
|
||||
if (property !== 'url') return;
|
||||
return `new URL(${resolveFileUrl({ fileName: chunkId })}, location).href`;
|
||||
}
|
||||
|
||||
export default async function ({ watch }) {
|
||||
const omtLoaderPromise = fsp.readFile(
|
||||
path.join(__dirname, 'lib', 'omt.ejs'),
|
||||
'utf-8',
|
||||
);
|
||||
await del('.tmp/build');
|
||||
|
||||
const tsPluginInstance = simpleTS('.', {
|
||||
watch,
|
||||
});
|
||||
const commonPlugins = () => [
|
||||
tsPluginInstance,
|
||||
resolveDirsPlugin([
|
||||
'src/static-build',
|
||||
'src/client',
|
||||
'src/image-worker',
|
||||
'src/worker-main-shared',
|
||||
'codecs',
|
||||
]),
|
||||
assetPlugin(),
|
||||
cssPlugin(resolveFileUrl),
|
||||
];
|
||||
const dir = '.tmp/build';
|
||||
const staticPath = 'static/c/[name]-[hash][extname]';
|
||||
|
||||
return {
|
||||
input: 'src/static-build/index.tsx',
|
||||
output: {
|
||||
dir,
|
||||
format: 'cjs',
|
||||
assetFileNames: staticPath,
|
||||
exports: 'named',
|
||||
},
|
||||
// Don't watch the ts files. Instead we watch the output from the ts compiler.
|
||||
watch: { clearScreen: false, exclude: ['**/*.ts', '**/*.tsx'] },
|
||||
preserveModules: true,
|
||||
plugins: [
|
||||
{ resolveFileUrl, resolveImportMeta },
|
||||
clientBundlePlugin(
|
||||
{
|
||||
plugins: [
|
||||
{ resolveFileUrl, resolveImportMeta },
|
||||
OMT({ loader: await omtLoaderPromise }),
|
||||
...commonPlugins(),
|
||||
commonjs(),
|
||||
resolve(),
|
||||
terser({ module: true }),
|
||||
],
|
||||
},
|
||||
{
|
||||
dir,
|
||||
format: 'amd',
|
||||
chunkFileNames: staticPath.replace('[extname]', '.js'),
|
||||
entryFileNames: staticPath.replace('[extname]', '.js'),
|
||||
},
|
||||
resolveFileUrl,
|
||||
),
|
||||
...commonPlugins(),
|
||||
emitFiles({ include: '**/*', root: path.join(__dirname, 'src', 'copy') }),
|
||||
nodeExternalPlugin(),
|
||||
imageWorkerPlugin(),
|
||||
runScript(dir + '/index.js'),
|
||||
],
|
||||
};
|
||||
}
|
||||
13
serve.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"headers": [
|
||||
{
|
||||
"source": "**/*",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "no-cache"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
const escapeRE = require("escape-string-regexp");
|
||||
|
||||
module.exports = {
|
||||
repo: "GoogleChromeLabs/squoosh",
|
||||
path: "build/**/!(*.map)",
|
||||
branch: "dev",
|
||||
findRenamed(path, newPaths) {
|
||||
const nameParts = /^(.+\.)[a-f0-9]+(\..+)$/.exec(path);
|
||||
if (!nameParts) return;
|
||||
|
||||
const matchRe = new RegExp(`^${escapeRE(nameParts[1])}[a-f0-9]+${escapeRE(nameParts[2])}$`);
|
||||
return newPaths.find(newPath => matchRe.test(newPath));
|
||||
}
|
||||
};
|
||||
59
src/client/index.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { wrap } from 'comlink';
|
||||
import workerURL from 'omt:image-worker';
|
||||
import imgURL from 'url:./tmp.png';
|
||||
|
||||
import type { ProcessorWorkerApi } from 'image-worker/index';
|
||||
const worker = new Worker(workerURL);
|
||||
const api = wrap<ProcessorWorkerApi>(worker);
|
||||
|
||||
async function demo() {
|
||||
const img = document.createElement('img');
|
||||
img.src = imgURL;
|
||||
await img.decode();
|
||||
// Make canvas same size as image
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const data = ctx.getImageData(0, 0, img.width, img.height);
|
||||
const result = await api.mozjpegEncode(data, {
|
||||
quality: 75,
|
||||
baseline: false,
|
||||
arithmetic: false,
|
||||
progressive: true,
|
||||
optimize_coding: true,
|
||||
smoothing: 0,
|
||||
color_space: 3,
|
||||
quant_table: 3,
|
||||
trellis_multipass: false,
|
||||
trellis_opt_zero: false,
|
||||
trellis_opt_table: false,
|
||||
trellis_loops: 1,
|
||||
auto_subsample: true,
|
||||
chroma_subsample: 2,
|
||||
separate_chroma_quality: false,
|
||||
chroma_quality: 75,
|
||||
});
|
||||
|
||||
{
|
||||
const resultUrl = URL.createObjectURL(new Blob([result]));
|
||||
const img = new Image();
|
||||
img.src = resultUrl;
|
||||
document.body.append(img);
|
||||
}
|
||||
}
|
||||
|
||||
demo();
|
||||
13
src/client/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../missing-types.d.ts" />
|
||||
BIN
src/client/tmp.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
8
src/client/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../generic-tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["esnext", "dom", "dom.iterable"],
|
||||
"types": []
|
||||
},
|
||||
"references": [{ "path": "../image-worker" }]
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import imagequant, { QuantizerModule } from '../../../codecs/imagequant/imagequant';
|
||||
import wasmUrl from '../../../codecs/imagequant/imagequant.wasm';
|
||||
import { QuantizeOptions } from './processor-meta';
|
||||
import { initEmscriptenModule } from '../util';
|
||||
|
||||
let emscriptenModule: Promise<QuantizerModule>;
|
||||
|
||||
export async function process(data: ImageData, opts: QuantizeOptions): Promise<ImageData> {
|
||||
if (!emscriptenModule) emscriptenModule = initEmscriptenModule(imagequant, wasmUrl);
|
||||
|
||||
const module = await emscriptenModule;
|
||||
|
||||
const result = opts.zx ?
|
||||
module.zx_quantize(data.data, data.width, data.height, opts.dither)
|
||||
:
|
||||
module.quantize(data.data, data.width, data.height, opts.maxNumColors, opts.dither);
|
||||
|
||||
return new ImageData(result, data.width, data.height);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { WorkerResizeOptions } from './processor-meta';
|
||||
import { getContainOffsets } from './util';
|
||||
import { resize as codecResize } from '../../../codecs/resize/pkg';
|
||||
|
||||
function crop(data: ImageData, sx: number, sy: number, sw: number, sh: number): ImageData {
|
||||
const inputPixels = new Uint32Array(data.data.buffer);
|
||||
|
||||
// Copy within the same buffer for speed and memory efficiency.
|
||||
for (let y = 0; y < sh; y += 1) {
|
||||
const start = ((y + sy) * data.width) + sx;
|
||||
inputPixels.copyWithin(y * sw, start, start + sw);
|
||||
}
|
||||
|
||||
return new ImageData(
|
||||
new Uint8ClampedArray(inputPixels.buffer.slice(0, sw * sh * 4)),
|
||||
sw, sh,
|
||||
);
|
||||
}
|
||||
|
||||
/** Resize methods by index */
|
||||
const resizeMethods: WorkerResizeOptions['method'][] = [
|
||||
'triangle', 'catrom', 'mitchell', 'lanczos3',
|
||||
];
|
||||
|
||||
export async function resize(data: ImageData, opts: WorkerResizeOptions): Promise<ImageData> {
|
||||
let input = data;
|
||||
|
||||
if (opts.fitMethod === 'contain') {
|
||||
const { sx, sy, sw, sh } = getContainOffsets(data.width, data.height, opts.width, opts.height);
|
||||
input = crop(input, Math.round(sx), Math.round(sy), Math.round(sw), Math.round(sh));
|
||||
}
|
||||
|
||||
const result = codecResize(
|
||||
new Uint8Array(input.data.buffer), input.width, input.height, opts.width, opts.height,
|
||||
resizeMethods.indexOf(opts.method), opts.premultiply, opts.linearRGB,
|
||||
);
|
||||
|
||||
return new ImageData(new Uint8ClampedArray(result.buffer), opts.width, opts.height);
|
||||
}
|
||||
5
src/copy/_headers
Normal file
@@ -0,0 +1,5 @@
|
||||
/*
|
||||
Cache-Control: no-cache
|
||||
|
||||
/c/*
|
||||
Cache-Control: max-age=31536000
|
||||
13
src/image-worker/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../missing-types.d.ts" />
|
||||
56
src/image-worker/mozjpegEncode/index.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import mozjpeg_enc, { MozJPEGModule } from 'codecs/mozjpeg_enc/mozjpeg_enc';
|
||||
import wasmUrl from 'url:codecs/mozjpeg_enc/mozjpeg_enc.wasm';
|
||||
import { initEmscriptenModule } from '../util';
|
||||
|
||||
export const enum MozJpegColorSpace {
|
||||
GRAYSCALE = 1,
|
||||
RGB,
|
||||
YCbCr,
|
||||
}
|
||||
|
||||
export interface EncodeOptions {
|
||||
quality: number;
|
||||
baseline: boolean;
|
||||
arithmetic: boolean;
|
||||
progressive: boolean;
|
||||
optimize_coding: boolean;
|
||||
smoothing: number;
|
||||
color_space: MozJpegColorSpace;
|
||||
quant_table: number;
|
||||
trellis_multipass: boolean;
|
||||
trellis_opt_zero: boolean;
|
||||
trellis_opt_table: boolean;
|
||||
trellis_loops: number;
|
||||
auto_subsample: boolean;
|
||||
chroma_subsample: number;
|
||||
separate_chroma_quality: boolean;
|
||||
chroma_quality: number;
|
||||
}
|
||||
|
||||
let emscriptenModule: Promise<MozJPEGModule>;
|
||||
|
||||
export default async function encode(
|
||||
data: ImageData,
|
||||
options: EncodeOptions,
|
||||
): Promise<ArrayBuffer> {
|
||||
if (!emscriptenModule) {
|
||||
emscriptenModule = initEmscriptenModule(mozjpeg_enc, wasmUrl);
|
||||
}
|
||||
|
||||
const module = await emscriptenModule;
|
||||
const resultView = module.encode(data.data, data.width, data.height, options);
|
||||
// wasm can’t run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
|
||||
return resultView.buffer as ArrayBuffer;
|
||||
}
|
||||
52
src/image-worker/quantize/index.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import imagequant, { QuantizerModule } from 'codecs/imagequant/imagequant';
|
||||
import wasmUrl from 'url:codecs/imagequant/imagequant.wasm';
|
||||
import { initEmscriptenModule } from '../util';
|
||||
|
||||
export interface QuantizeOptions {
|
||||
zx: number;
|
||||
maxNumColors: number;
|
||||
dither: number;
|
||||
}
|
||||
|
||||
export const defaultOptions: QuantizeOptions = {
|
||||
zx: 0,
|
||||
maxNumColors: 256,
|
||||
dither: 1.0,
|
||||
};
|
||||
|
||||
let emscriptenModule: Promise<QuantizerModule>;
|
||||
|
||||
export default async function process(
|
||||
data: ImageData,
|
||||
opts: QuantizeOptions,
|
||||
): Promise<ImageData> {
|
||||
if (!emscriptenModule) {
|
||||
emscriptenModule = initEmscriptenModule(imagequant, wasmUrl);
|
||||
}
|
||||
|
||||
const module = await emscriptenModule;
|
||||
|
||||
const result = opts.zx
|
||||
? module.zx_quantize(data.data, data.width, data.height, opts.dither)
|
||||
: module.quantize(
|
||||
data.data,
|
||||
data.width,
|
||||
data.height,
|
||||
opts.maxNumColors,
|
||||
opts.dither,
|
||||
);
|
||||
|
||||
return new ImageData(result, data.width, data.height);
|
||||
}
|
||||
7
src/image-worker/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../generic-tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["webworker", "esnext"]
|
||||
},
|
||||
"references": []
|
||||
}
|
||||
39
src/image-worker/util.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export function initEmscriptenModule<T extends EmscriptenWasm.Module>(
|
||||
moduleFactory: EmscriptenWasm.ModuleFactory<T>,
|
||||
wasmUrl: string,
|
||||
): Promise<T> {
|
||||
return moduleFactory({
|
||||
// Just to be safe, don't automatically invoke any wasm functions
|
||||
noInitialRun: true,
|
||||
locateFile: () => wasmUrl,
|
||||
});
|
||||
}
|
||||
|
||||
interface ClampOpts {
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
export function clamp(
|
||||
num: number,
|
||||
{ min = Number.MIN_VALUE, max = Number.MAX_VALUE }: ClampOpts,
|
||||
): number {
|
||||
return Math.min(Math.max(num, min), max);
|
||||
}
|
||||
|
||||
export function timed<T>(name: string, func: () => Promise<T>) {
|
||||
console.time(name);
|
||||
return func().finally(() => console.timeEnd(name));
|
||||
}
|
||||
26
src/image-worker/webpDecode/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import webpDecoder, { WebPModule } from 'codecs/webp/dec/webp_dec';
|
||||
import wasmUrl from 'url:codecs/webp/dec/webp_dec.wasm';
|
||||
import { initEmscriptenModule } from '../util';
|
||||
|
||||
let emscriptenModule: Promise<WebPModule>;
|
||||
|
||||
export default async function decode(data: ArrayBuffer): Promise<ImageData> {
|
||||
if (!emscriptenModule) {
|
||||
emscriptenModule = initEmscriptenModule(webpDecoder, wasmUrl);
|
||||
}
|
||||
|
||||
const module = await emscriptenModule;
|
||||
return module.decode(data);
|
||||
}
|
||||
61
src/image-worker/webpEncode/index.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import webpEncoder, { WebPModule } from 'codecs/webp/enc/webp_enc';
|
||||
import wasmUrl from 'url:codecs/webp/enc/webp_enc.wasm';
|
||||
import { initEmscriptenModule } from '../util';
|
||||
|
||||
export interface EncodeOptions {
|
||||
quality: number;
|
||||
target_size: number;
|
||||
target_PSNR: number;
|
||||
method: number;
|
||||
sns_strength: number;
|
||||
filter_strength: number;
|
||||
filter_sharpness: number;
|
||||
filter_type: number;
|
||||
partitions: number;
|
||||
segments: number;
|
||||
pass: number;
|
||||
show_compressed: number;
|
||||
preprocessing: number;
|
||||
autofilter: number;
|
||||
partition_limit: number;
|
||||
alpha_compression: number;
|
||||
alpha_filtering: number;
|
||||
alpha_quality: number;
|
||||
lossless: number;
|
||||
exact: number;
|
||||
image_hint: number;
|
||||
emulate_jpeg_size: number;
|
||||
thread_level: number;
|
||||
low_memory: number;
|
||||
near_lossless: number;
|
||||
use_delta_palette: number;
|
||||
use_sharp_yuv: number;
|
||||
}
|
||||
|
||||
let emscriptenModule: Promise<WebPModule>;
|
||||
|
||||
export default async function encode(
|
||||
data: ImageData,
|
||||
options: EncodeOptions,
|
||||
): Promise<ArrayBuffer> {
|
||||
if (!emscriptenModule) {
|
||||
emscriptenModule = initEmscriptenModule(webpEncoder, wasmUrl);
|
||||
}
|
||||
|
||||
const module = await emscriptenModule;
|
||||
const resultView = module.encode(data.data, data.width, data.height, options);
|
||||
// wasm can’t run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
|
||||
return resultView.buffer as ArrayBuffer;
|
||||
}
|
||||
3
src/static-build/components/base/all.css
Normal file
@@ -0,0 +1,3 @@
|
||||
html {
|
||||
background: green;
|
||||
}
|
||||
44
src/static-build/components/base/index.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { h, FunctionalComponent, RenderableProps } from 'preact';
|
||||
import styles from 'css-bundle:./all.css';
|
||||
import clientBundleURL, { imports } from 'client-bundle:client/index.tsx';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const BasePage: FunctionalComponent<Props> = ({
|
||||
children,
|
||||
title,
|
||||
}: RenderableProps<Props>) => {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{title ? `${title} - ` : ''}Squoosh</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, minimum-scale=1.0"
|
||||
></meta>
|
||||
<link rel="stylesheet" href={styles} />
|
||||
<script src={clientBundleURL} defer />
|
||||
{imports.map((v) => (
|
||||
<link rel="preload" as="script" href={v} />
|
||||
))}
|
||||
</head>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasePage;
|
||||
25
src/static-build/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { h } from 'preact';
|
||||
|
||||
import { renderPage, writeFiles } from './utils';
|
||||
import IndexPage from './pages/index';
|
||||
|
||||
interface Output {
|
||||
[outputPath: string]: string;
|
||||
}
|
||||
const toOutput: Output = {
|
||||
'index.html': renderPage(<IndexPage />),
|
||||
};
|
||||
|
||||
writeFiles(toOutput);
|
||||
25
src/static-build/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../missing-types.d.ts" />
|
||||
|
||||
declare module 'client-bundle:*' {
|
||||
const url: string;
|
||||
export default url;
|
||||
export const imports: string[];
|
||||
}
|
||||
|
||||
declare module 'css-bundle:*' {
|
||||
const url: string;
|
||||
export default url;
|
||||
export const inline: string;
|
||||
}
|
||||
21
src/static-build/pages/index/index.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { h, FunctionalComponent } from 'preact';
|
||||
import BasePage from 'static-build/components/base';
|
||||
|
||||
const IndexPage: FunctionalComponent<{}> = () => (
|
||||
<BasePage>
|
||||
<h1>Hi</h1>
|
||||
</BasePage>
|
||||
);
|
||||
export default IndexPage;
|
||||
8
src/static-build/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../generic-tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["esnext", "dom"],
|
||||
"types": ["node"]
|
||||
},
|
||||
"references": []
|
||||
}
|
||||
46
src/static-build/utils.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { promises as fsp } from 'fs';
|
||||
import { join as joinPath } from 'path';
|
||||
|
||||
import render from 'preact-render-to-string';
|
||||
import { VNode } from 'preact';
|
||||
|
||||
export function renderPage(vnode: VNode) {
|
||||
return '<!DOCTYPE html>' + render(vnode);
|
||||
}
|
||||
|
||||
interface OutputMap {
|
||||
[path: string]: string;
|
||||
}
|
||||
|
||||
export function writeFiles(toOutput: OutputMap) {
|
||||
Promise.all(
|
||||
Object.entries(toOutput).map(async ([path, content]) => {
|
||||
const pathParts = ['.tmp', 'build', 'static', ...path.split('/')];
|
||||
await fsp.mkdir(joinPath(...pathParts.slice(0, -1)), { recursive: true });
|
||||
const fullPath = joinPath(...pathParts);
|
||||
try {
|
||||
await fsp.writeFile(fullPath, content, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to write ' + fullPath);
|
||||
throw err;
|
||||
}
|
||||
}),
|
||||
).catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
@@ -1,7 +1,10 @@
|
||||
import { canvasEncodeTest } from '../generic/util';
|
||||
|
||||
export interface EncodeOptions { }
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
export interface EncodeOptions {}
|
||||
export interface EncoderState {
|
||||
type: typeof type;
|
||||
options: EncodeOptions;
|
||||
}
|
||||
|
||||
export const type = 'browser-bmp';
|
||||
export const label = 'Browser BMP';
|
||||
@@ -1,7 +1,10 @@
|
||||
import { canvasEncodeTest } from '../generic/util';
|
||||
|
||||
export interface EncodeOptions {}
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
export interface EncoderState {
|
||||
type: typeof type;
|
||||
options: EncodeOptions;
|
||||
}
|
||||
|
||||
export const type = 'browser-gif';
|
||||
export const label = 'Browser GIF';
|
||||
@@ -1,7 +1,10 @@
|
||||
import { canvasEncodeTest } from '../generic/util';
|
||||
|
||||
export interface EncodeOptions { }
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
export interface EncodeOptions {}
|
||||
export interface EncoderState {
|
||||
type: typeof type;
|
||||
options: EncodeOptions;
|
||||
}
|
||||
|
||||
export const type = 'browser-jp2';
|
||||
export const label = 'Browser JPEG 2000';
|
||||
@@ -1,5 +1,10 @@
|
||||
export interface EncodeOptions { quality: number; }
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
export interface EncodeOptions {
|
||||
quality: number;
|
||||
}
|
||||
export interface EncoderState {
|
||||
type: typeof type;
|
||||
options: EncodeOptions;
|
||||
}
|
||||
|
||||
export const type = 'browser-jpeg';
|
||||
export const label = 'Browser JPEG';
|
||||
@@ -1,7 +1,10 @@
|
||||
import { canvasEncodeTest } from '../generic/util';
|
||||
|
||||
export interface EncodeOptions { }
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
export interface EncodeOptions {}
|
||||
export interface EncoderState {
|
||||
type: typeof type;
|
||||
options: EncodeOptions;
|
||||
}
|
||||
|
||||
export const type = 'browser-pdf';
|
||||
export const label = 'Browser PDF';
|
||||
@@ -1,5 +1,8 @@
|
||||
export interface EncodeOptions {}
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
export interface EncoderState {
|
||||
type: typeof type;
|
||||
options: EncodeOptions;
|
||||
}
|
||||
|
||||
export const type = 'browser-png';
|
||||
export const label = 'Browser PNG';
|
||||
@@ -1,7 +1,10 @@
|
||||
import { canvasEncodeTest } from '../generic/util';
|
||||
|
||||
export interface EncodeOptions { }
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
export interface EncodeOptions {}
|
||||
export interface EncoderState {
|
||||
type: typeof type;
|
||||
options: EncodeOptions;
|
||||
}
|
||||
|
||||
export const type = 'browser-tiff';
|
||||
export const label = 'Browser TIFF';
|
||||
@@ -1,7 +1,12 @@
|
||||
import { canvasEncodeTest } from '../generic/util';
|
||||
|
||||
export interface EncodeOptions { quality: number; }
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
export interface EncodeOptions {
|
||||
quality: number;
|
||||
}
|
||||
export interface EncoderState {
|
||||
type: typeof type;
|
||||
options: EncodeOptions;
|
||||
}
|
||||
|
||||
export const type = 'browser-webp';
|
||||
export const label = 'Browser WebP';
|
||||
@@ -1,7 +1,10 @@
|
||||
import { builtinDecode, sniffMimeType, canDecodeImageType } from '../lib/util';
|
||||
import Processor from './processor';
|
||||
|
||||
export async function decodeImage(blob: Blob, processor: Processor): Promise<ImageData> {
|
||||
export async function decodeImage(
|
||||
blob: Blob,
|
||||
processor: Processor,
|
||||
): Promise<ImageData> {
|
||||
const mimeType = await sniffMimeType(blob);
|
||||
const canDecode = await canDecodeImageType(mimeType);
|
||||
|
||||
@@ -17,34 +17,34 @@ export interface EncoderSupportMap {
|
||||
}
|
||||
|
||||
export type EncoderState =
|
||||
identity.EncoderState |
|
||||
oxiPNG.EncoderState |
|
||||
mozJPEG.EncoderState |
|
||||
webP.EncoderState |
|
||||
avif.EncoderState |
|
||||
browserPNG.EncoderState |
|
||||
browserJPEG.EncoderState |
|
||||
browserWebP.EncoderState |
|
||||
browserGIF.EncoderState |
|
||||
browserTIFF.EncoderState |
|
||||
browserJP2.EncoderState |
|
||||
browserBMP.EncoderState |
|
||||
browserPDF.EncoderState;
|
||||
| identity.EncoderState
|
||||
| oxiPNG.EncoderState
|
||||
| mozJPEG.EncoderState
|
||||
| webP.EncoderState
|
||||
| avif.EncoderState
|
||||
| browserPNG.EncoderState
|
||||
| browserJPEG.EncoderState
|
||||
| browserWebP.EncoderState
|
||||
| browserGIF.EncoderState
|
||||
| browserTIFF.EncoderState
|
||||
| browserJP2.EncoderState
|
||||
| browserBMP.EncoderState
|
||||
| browserPDF.EncoderState;
|
||||
|
||||
export type EncoderOptions =
|
||||
identity.EncodeOptions |
|
||||
oxiPNG.EncodeOptions |
|
||||
mozJPEG.EncodeOptions |
|
||||
webP.EncodeOptions |
|
||||
avif.EncodeOptions |
|
||||
browserPNG.EncodeOptions |
|
||||
browserJPEG.EncodeOptions |
|
||||
browserWebP.EncodeOptions |
|
||||
browserGIF.EncodeOptions |
|
||||
browserTIFF.EncodeOptions |
|
||||
browserJP2.EncodeOptions |
|
||||
browserBMP.EncodeOptions |
|
||||
browserPDF.EncodeOptions;
|
||||
| identity.EncodeOptions
|
||||
| oxiPNG.EncodeOptions
|
||||
| mozJPEG.EncodeOptions
|
||||
| webP.EncodeOptions
|
||||
| avif.EncodeOptions
|
||||
| browserPNG.EncodeOptions
|
||||
| browserJPEG.EncodeOptions
|
||||
| browserWebP.EncodeOptions
|
||||
| browserGIF.EncodeOptions
|
||||
| browserTIFF.EncodeOptions
|
||||
| browserJP2.EncodeOptions
|
||||
| browserBMP.EncodeOptions
|
||||
| browserPDF.EncodeOptions;
|
||||
|
||||
export type EncoderType = keyof typeof encoderMap;
|
||||
|
||||
@@ -72,11 +72,14 @@ export const encoders = Array.from(Object.values(encoderMap));
|
||||
export const encodersSupported = Promise.resolve().then(async () => {
|
||||
const encodersSupported: EncoderSupportMap = {};
|
||||
|
||||
await Promise.all(encoders.map(async (encoder) => {
|
||||
// If the encoder provides a featureTest, call it, otherwise assume supported.
|
||||
const isSupported = !('featureTest' in encoder) || await encoder.featureTest();
|
||||
encodersSupported[encoder.type] = isSupported;
|
||||
}));
|
||||
await Promise.all(
|
||||
encoders.map(async (encoder) => {
|
||||
// If the encoder provides a featureTest, call it, otherwise assume supported.
|
||||
const isSupported =
|
||||
!('featureTest' in encoder) || (await encoder.featureTest());
|
||||
encodersSupported[encoder.type] = isSupported;
|
||||
}),
|
||||
);
|
||||
|
||||
return encodersSupported;
|
||||
});
|
||||
@@ -8,8 +8,8 @@ interface EncodeOptions {
|
||||
}
|
||||
|
||||
type Props = {
|
||||
options: EncodeOptions,
|
||||
onChange(newOptions: EncodeOptions): void,
|
||||
options: EncodeOptions;
|
||||
onChange(newOptions: EncodeOptions): void;
|
||||
};
|
||||
|
||||
interface QualityOptionArg {
|
||||
@@ -19,11 +19,7 @@ interface QualityOptionArg {
|
||||
}
|
||||
|
||||
export default function qualityOption(opts: QualityOptionArg = {}) {
|
||||
const {
|
||||
min = 0,
|
||||
max = 100,
|
||||
step = 1,
|
||||
} = opts;
|
||||
const { min = 0, max = 100, step = 1 } = opts;
|
||||
|
||||
class QualityOptions extends Component<Props, {}> {
|
||||
@bind
|
||||