Compare commits

...

13 Commits
dev ... parcel2

53 changed files with 8638 additions and 470 deletions

39
.babelrc Normal file
View File

@ -0,0 +1,39 @@
{
"presets": [
[
"@babel/preset-typescript",
{
"jsxPragma": "h"
}
],
[
"@parcel/babel-preset-env",
{
"loose": true,
"bugfixes": true
}
]
],
"plugins": [
[
"@babel/plugin-transform-react-jsx",
{
"loose": true,
"pragma": "h",
"pragmaFrag": "Fragment"
}
],
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
],
[
"@babel/plugin-proposal-class-properties",
{
"loose": true
}
]
]
}

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ node_modules
*.scss.d.ts
*.css.d.ts
*.o
.parcel-cache

9
.parcelrc Normal file
View File

@ -0,0 +1,9 @@
{
"extends": "@parcel/config-default",
"transformers": {
"*.wasm": ["@parcel/transformer-raw"]
},
"packagers": {
"*.wasm": "@parcel/packager-raw"
}
}

15
.proxyrc.js Normal file
View File

@ -0,0 +1,15 @@
module.exports = function(app) {
// `app` is an Express instance
app.use(function(req, res, next) {
const sh = res.setHeader;
res.setHeader = function(key, value) {
// remove pointless/incorrect charset from binary responses:
if (/^content-type$/i.test(key)) {
const m = value && value.match(/^(image\/|application\/wasm); charset=.+$/);
if (m) value = m[1];
}
return sh.call(this, key, value);
}
next();
});
};

3
global.d.ts vendored
View File

@ -1,6 +1,3 @@
declare const __webpack_public_path__: string;
declare const PRERENDER: boolean;
declare interface NodeModule {
hot: any;
}

8776
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,8 +4,8 @@
"version": "1.11.4",
"license": "apache-2.0",
"scripts": {
"start": "webpack-dev-server --host 0.0.0.0 --hot",
"build": "webpack -p",
"start": "parcel serve src/index.html --no-autoinstall --host 0.0.0.0 --dist-dir build",
"build": "parcel build src/index.html --no-autoinstall --dist-dir build",
"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"
@ -15,7 +15,27 @@
"pre-commit": "npm run lint"
}
},
"browserslist": ">0.5%, not IE 11, not op_mini all",
"postcss": {
"modules": true,
"plugins": {
"postcss-modules": {
"localsConvention": "camelCaseOnly",
"generateScopedName": "[name]__[local]"
}
}
},
"//": "See https://github.com/GoogleChromeLabs/pointer-tracker/pull/10",
"alias": {
"pointer-tracker": "./node_modules/pointer-tracker/dist/PointerTracker.mjs"
},
"devDependencies": {
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-proposal-decorators": "^7.10.5",
"@babel/plugin-transform-react-jsx": "^7.10.4",
"@babel/preset-typescript": "^7.10.4",
"@parcel/babel-preset-env": "^2.0.0-alpha.3",
"@parcel/types": "^2.0.0-alpha.3",
"@types/node": "10.14.15",
"@types/pretty-bytes": "5.1.0",
"@types/webassembly-js-api": "0.0.3",
@ -47,13 +67,16 @@
"node-sass": "4.13.0",
"normalize-path": "^3.0.0",
"optimize-css-assets-webpack-plugin": "5.0.1",
"parcel": "^2.0.0-beta.1",
"pointer-tracker": "2.0.3",
"postcss-modules": "^3.2.0",
"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": "^1.26.10",
"sass-loader": "7.3.1",
"script-ext-html-webpack-plugin": "2.1.4",
"source-map-loader": "0.2.4",

View File

@ -1,6 +1,6 @@
import { builtinDecode, sniffMimeType, canDecodeImage } from '../lib/util';
import Processor from './processor';
import webpDataUrl from 'url-loader!./tiny.webp';
import webpDataUrl from 'url:./tiny.webp';
const webPSupported = canDecodeImage(webpDataUrl);

View File

@ -1,6 +1,6 @@
import { h, Component } from 'preact';
import { bind } from '../../lib/initial-util';
import * as style from '../../components/Options/style.scss';
import * as style from '../../components/Options/style.module.scss';
import Range from '../../components/range';
interface EncodeOptions {

View File

@ -2,7 +2,7 @@ import { h, Component } from 'preact';
import { bind } from '../../lib/initial-util';
import { inputFieldValueAsNumber, konami, preventDefault } from '../../lib/util';
import { QuantizeOptions } from './processor-meta';
import * as style from '../../components/Options/style.scss';
import * as style from '../../components/Options/style.module.scss';
import Expander from '../../components/expander';
import Select from '../../components/select';
import Range from '../../components/range';

View File

@ -2,7 +2,7 @@ import { h, Component } from 'preact';
import { bind } from '../../lib/initial-util';
import { inputFieldChecked, inputFieldValueAsNumber, preventDefault } from '../../lib/util';
import { EncodeOptions, MozJpegColorSpace } from './encoder-meta';
import * as style from '../../components/Options/style.scss';
import * as style from '../../components/Options/style.module.scss';
import Checkbox from '../../components/checkbox';
import Expander from '../../components/expander';
import Select from '../../components/select';

View File

@ -3,7 +3,7 @@ import { bind } from '../../lib/initial-util';
import { inputFieldValueAsNumber, preventDefault } from '../../lib/util';
import { EncodeOptions } from './encoder-meta';
import Range from '../../components/range';
import * as style from '../../components/Options/style.scss';
import * as style from '../../components/Options/style.module.scss';
type Props = {
options: EncodeOptions;

View File

@ -23,6 +23,19 @@ type ProcessorWorkerApi = import('./processor-worker').ProcessorWorkerApi;
/** How long the worker should be idle before terminating. */
const workerTimeout = 10000;
/**
* Decorator that manages the (re)starting of the worker and aborting existing jobs. Not all
* processing jobs require a worker (e.g. the main thread canvas encodes), use the needsWorker
* option to control this.
*/
function processingJob(options: ProcessingJobOptions = {}) {
return (target: Processor, propertyKey: string, descriptor: PropertyDescriptor): void => {
const processingFunc = descriptor.value;
descriptor.value = target.runProcessingJob.bind(target, options, processingFunc);
};
}
interface ProcessingJobOptions {
needsWorker?: boolean;
}
@ -41,52 +54,43 @@ export default class Processor {
/** setTimeout ID for killing the worker when idle. */
private _workerTimeoutId: number = 0;
/**
* Decorator that manages the (re)starting of the worker and aborting existing jobs. Not all
* processing jobs require a worker (e.g. the main thread canvas encodes), use the needsWorker
* option to control this.
*/
private static _processingJob(options: ProcessingJobOptions = {}) {
/** @private */
async runProcessingJob(options: ProcessingJobOptions, processingFunc: any, ...args: any[]) {
const { needsWorker = false } = options;
return (target: Processor, propertyKey: string, descriptor: PropertyDescriptor): void => {
const processingFunc = descriptor.value;
this._latestJobId += 1;
const jobId = this._latestJobId;
this.abortCurrent();
descriptor.value = async function (this: Processor, ...args: any[]) {
this._latestJobId += 1;
const jobId = this._latestJobId;
this.abortCurrent();
if (needsWorker) self.clearTimeout(this._workerTimeoutId);
if (needsWorker) self.clearTimeout(this._workerTimeoutId);
if (!this._worker && needsWorker) {
// worker-loader does magic here.
// @ts-ignore - Typescript doesn't know about the 2nd param to new Worker, and the
// definition can't be overwritten.
this._worker = new Worker(
'./processor-worker',
{ name: 'processor-worker' },
// { name: 'processor-worker', type: 'module' },
) as Worker;
// Need to do some TypeScript trickery to make the type match.
this._workerApi = proxy(this._worker) as any as ProcessorWorkerApi;
}
if (!this._worker && needsWorker) {
// worker-loader does magic here.
// @ts-ignore - Typescript doesn't know about the 2nd param to new Worker, and the
// definition can't be overwritten.
this._worker = new Worker(
'./processor-worker',
{ name: 'processor-worker', type: 'module' },
) as Worker;
// Need to do some TypeScript trickery to make the type match.
this._workerApi = proxy(this._worker) as any as ProcessorWorkerApi;
}
this._busy = true;
this._busy = true;
const returnVal = Promise.race([
processingFunc.call(this, ...args),
new Promise((_, reject) => { this._abortRejector = reject; }),
]);
const returnVal = Promise.race([
processingFunc.call(this, ...args),
new Promise((_, reject) => { this._abortRejector = reject; }),
]);
// Wait for the operation to settle.
await returnVal.catch(() => {});
// Wait for the operation to settle.
await returnVal.catch(() => {});
// If no other jobs are happening, cleanup.
if (jobId === this._latestJobId) this._jobCleanup();
// If no other jobs are happening, cleanup.
if (jobId === this._latestJobId) this._jobCleanup();
return returnVal;
};
};
return returnVal;
}
private _jobCleanup(): void {
@ -116,33 +120,33 @@ export default class Processor {
}
// Off main thread jobs:
@Processor._processingJob({ needsWorker: true })
@processingJob({ needsWorker: true })
imageQuant(data: ImageData, opts: QuantizeOptions): Promise<ImageData> {
return this._workerApi!.quantize(data, opts);
}
@Processor._processingJob({ needsWorker: true })
@processingJob({ needsWorker: true })
rotate(
data: ImageData, opts: import('./rotate/processor-meta').RotateOptions,
): Promise<ImageData> {
return this._workerApi!.rotate(data, opts);
}
@Processor._processingJob({ needsWorker: true })
@processingJob({ needsWorker: true })
workerResize(
data: ImageData, opts: import('./resize/processor-meta').WorkerResizeOptions,
): Promise<ImageData> {
return this._workerApi!.resize(data, opts);
}
@Processor._processingJob({ needsWorker: true })
@processingJob({ needsWorker: true })
mozjpegEncode(
data: ImageData, opts: MozJPEGEncoderOptions,
): Promise<ArrayBuffer> {
return this._workerApi!.mozjpegEncode(data, opts);
}
@Processor._processingJob({ needsWorker: true })
@processingJob({ needsWorker: true })
async oxiPngEncode(
data: ImageData, opts: OxiPNGEncoderOptions,
): Promise<ArrayBuffer> {
@ -152,12 +156,12 @@ export default class Processor {
return this._workerApi!.oxiPngEncode(pngBuffer, opts);
}
@Processor._processingJob({ needsWorker: true })
@processingJob({ needsWorker: true })
webpEncode(data: ImageData, opts: WebPEncoderOptions): Promise<ArrayBuffer> {
return this._workerApi!.webpEncode(data, opts);
}
@Processor._processingJob({ needsWorker: true })
@processingJob({ needsWorker: true })
async webpDecode(blob: Blob): Promise<ImageData> {
const data = await blobToArrayBuffer(blob);
return this._workerApi!.webpDecode(data);
@ -165,42 +169,42 @@ export default class Processor {
// Not-worker jobs:
@Processor._processingJob()
@processingJob()
browserBmpEncode(data: ImageData): Promise<Blob> {
return browserBMP.encode(data);
}
@Processor._processingJob()
@processingJob()
browserPngEncode(data: ImageData): Promise<Blob> {
return browserPNG.encode(data);
}
@Processor._processingJob()
@processingJob()
browserJpegEncode(data: ImageData, opts: BrowserJPEGOptions): Promise<Blob> {
return browserJPEG.encode(data, opts);
}
@Processor._processingJob()
@processingJob()
browserWebpEncode(data: ImageData, opts: BrowserWebpEncodeOptions): Promise<Blob> {
return browserWebP.encode(data, opts);
}
@Processor._processingJob()
@processingJob()
browserGifEncode(data: ImageData): Promise<Blob> {
return browserGIF.encode(data);
}
@Processor._processingJob()
@processingJob()
browserTiffEncode(data: ImageData): Promise<Blob> {
return browserTIFF.encode(data);
}
@Processor._processingJob()
@processingJob()
browserJp2Encode(data: ImageData): Promise<Blob> {
return browserJP2.encode(data);
}
@Processor._processingJob()
@processingJob()
browserPdfEncode(data: ImageData): Promise<Blob> {
return browserPDF.encode(data);
}

View File

@ -5,7 +5,7 @@ import {
inputFieldValueAsNumber, inputFieldValue, preventDefault, inputFieldChecked,
} from '../../lib/util';
import { ResizeOptions, isWorkerOptions } from './processor-meta';
import * as style from '../../components/Options/style.scss';
import * as style from '../../components/Options/style.module.scss';
import Checkbox from '../../components/checkbox';
import Expander from '../../components/expander';
import Select from '../../components/select';

View File

@ -2,7 +2,7 @@ import { h, Component } from 'preact';
import { bind } from '../../lib/initial-util';
import { inputFieldCheckedAsNumber, inputFieldValueAsNumber, preventDefault } from '../../lib/util';
import { EncodeOptions, WebPImageHint } from './encoder-meta';
import * as style from '../../components/Options/style.scss';
import * as style from '../../components/Options/style.module.scss';
import Checkbox from '../../components/checkbox';
import Expander from '../../components/expander';
import Select from '../../components/select';

View File

@ -1,7 +1,7 @@
import { h, Component } from 'preact';
import { bind, linkRef, Fileish } from '../../lib/initial-util';
import * as style from './style.scss';
import * as style from './style.module.scss';
import { FileDropEvent } from 'file-drop-element';
import 'file-drop-element';
import SnackBarElement, { SnackOptions } from '../../lib/SnackBar';
@ -12,11 +12,9 @@ import '../custom-els/LoadingSpinner';
const ROUTE_EDITOR = '/editor';
const compressPromise = import(
/* webpackChunkName: "main-app" */
'../compress');
const swBridgePromise = import(
/* webpackChunkName: "sw-bridge" */
'../../lib/sw-bridge');
function back() {
@ -47,8 +45,9 @@ export default class App extends Component<Props, State> {
compressPromise.then((module) => {
this.setState({ Compress: module.default });
}).catch(() => {
}).catch((e) => {
this.showSnack('Failed to load app');
throw e;
});
swBridgePromise.then(async ({ offliner, getSharedImage }) => {

View File

@ -24,7 +24,7 @@
right: 10px;
bottom: 10px;
border: 2px dashed #fff;
background-color:rgba(88, 116, 88, 0.2);
background-color: rgba(88, 116, 88, 0.2);
border-color: rgba(65, 129, 65, 0.5);
border-radius: 10px;
opacity: 0;

View File

@ -1,6 +1,6 @@
import { h, Component } from 'preact';
import * as style from './style.scss';
import * as style from './style.module.scss';
import { bind } from '../../lib/initial-util';
import { cleanSet, cleanMerge } from '../../lib/clean-modify';
import OxiPNGEncoderOptions from '../../codecs/oxipng/options';

View File

@ -1,5 +1,5 @@
import PointerTracker, { Pointer } from 'pointer-tracker';
import * as styles from './styles.css';
import * as styles from './styles.module.css';
const legacyClipCompatAttr = 'legacy-clip-compat';
const orientationAttr = 'orientation';

View File

@ -2,7 +2,7 @@ import { h, Component } from 'preact';
import PinchZoom, { ScaleToOpts } from './custom-els/PinchZoom';
import './custom-els/PinchZoom';
import './custom-els/TwoUp';
import * as style from './style.scss';
import * as style from './style.module.scss';
import { bind, linkRef } from '../../lib/initial-util';
import { shallowEqual, drawDataToCanvas } from '../../lib/util';
import {
@ -13,7 +13,7 @@ import {
ToggleBackgroundActiveIcon,
RotateIcon,
} from '../../lib/icons';
import { twoUpHandle } from './custom-els/TwoUp/styles.css';
import { twoUpHandle } from './custom-els/TwoUp/styles.module.css';
import { InputProcessorState } from '../../codecs/input-processors';
import { cleanSet } from '../../lib/clean-modify';
import { SourceImage } from '../compress';

View File

@ -1,5 +1,5 @@
import { h, Component } from 'preact';
import * as style from './style.scss';
import * as style from './style.module.scss';
import { UncheckedIcon, CheckedIcon } from '../../lib/icons';
interface Props extends JSX.HTMLAttributes {}

View File

@ -1,4 +1,4 @@
import * as style from './styles.css';
import * as style from './styles.module.css';
import { transitionHeight } from '../../../../lib/util';
interface CloseAllOptions {

View File

@ -2,7 +2,7 @@ import { h, Component } from 'preact';
import { bind, Fileish } from '../../lib/initial-util';
import { blobToImg, drawableToImageData, blobToText } from '../../lib/util';
import * as style from './style.scss';
import * as style from './style.module.scss';
import Output from '../Output';
import Options from '../Options';
import ResultCache from './result-cache';

View File

@ -1,4 +1,4 @@
import * as styles from './styles.css';
import * as styles from './styles.module.css';
/**
* A simple spinner. This custom element has no JS API. Just put it in the document, and it'll

View File

@ -1,5 +1,5 @@
import { h, Component, ComponentChild, ComponentChildren } from 'preact';
import * as style from './style.scss';
import * as style from './style.module.scss';
import { transitionHeight } from '../../lib/util';
interface Props {

View File

@ -3,15 +3,15 @@ import { h, Component } from 'preact';
import { bind, linkRef, Fileish } from '../../lib/initial-util';
import '../custom-els/LoadingSpinner';
import logo from './imgs/logo.svg';
import largePhoto from './imgs/demos/demo-large-photo.jpg';
import artwork from './imgs/demos/demo-artwork.jpg';
import deviceScreen from './imgs/demos/demo-device-screen.png';
import largePhotoIcon from './imgs/demos/icon-demo-large-photo.jpg';
import artworkIcon from './imgs/demos/icon-demo-artwork.jpg';
import deviceScreenIcon from './imgs/demos/icon-demo-device-screen.jpg';
import logoIcon from './imgs/demos/icon-demo-logo.png';
import * as style from './style.scss';
import logo from 'url:./imgs/logo.svg';
import largePhoto from 'url:./imgs/demos/demo-large-photo.jpg';
import artwork from 'url:./imgs/demos/demo-artwork.jpg';
import deviceScreen from 'url:./imgs/demos/demo-device-screen.png';
import largePhotoIcon from 'url:./imgs/demos/icon-demo-large-photo.jpg';
import artworkIcon from 'url:./imgs/demos/icon-demo-artwork.jpg';
import deviceScreenIcon from 'url:./imgs/demos/icon-demo-device-screen.jpg';
import logoIcon from 'url:./imgs/demos/icon-demo-logo.png';
import * as style from './style.module.scss';
import SnackBarElement from '../../lib/SnackBar';
const demos = [

View File

@ -1,5 +1,5 @@
import { h, Component } from 'preact';
import * as style from './style.scss';
import * as style from './style.module.scss';
import RangeInputElement from '../../custom-els/RangeInput';
import '../../custom-els/RangeInput';
import { linkRef, bind } from '../../lib/initial-util';

View File

@ -1,6 +1,6 @@
import { h, Component } from 'preact';
import * as prettyBytes from 'pretty-bytes';
import * as style from './style.scss';
import prettyBytes from 'pretty-bytes';
import * as style from './style.module.scss';
interface Props {
blob: Blob;

View File

@ -1,6 +1,6 @@
import { h, Component, ComponentChildren, ComponentChild } from 'preact';
import * as style from './style.scss';
import * as style from './style.module.scss';
import FileSize from './FileSize';
import { DownloadIcon, CopyAcrossIcon, CopyAcrossIconProps } from '../../lib/icons';
import '../custom-els/LoadingSpinner';

View File

@ -1,5 +1,5 @@
import { h, Component } from 'preact';
import * as style from './style.scss';
import * as style from './style.module.scss';
interface Props extends JSX.HTMLAttributes {
large?: boolean;

View File

@ -1,6 +1,6 @@
import PointerTracker from 'pointer-tracker';
import { bind } from '../../lib/initial-util';
import * as style from './styles.css';
import * as style from './styles.module.css';
const RETARGETED_EVENTS = ['focus', 'blur'];
const UPDATE_EVENTS = ['input', 'change'];

View File

@ -12,5 +12,6 @@
<link rel="manifest" href="/manifest.json">
</head>
<body>
<script src="./index.ts"></script>
</body>
</html>

View File

@ -1,18 +1,27 @@
declare module '@webcomponents/custom-elements';
// Patch Worker to ignore `importScripts("x.css")` generated by Parcel:
const W = self.Worker;
self.Worker = function (url: string | URL, options?: WorkerOptions) {
const code = `
importScripts=(function(){
return this.apply(self,[].slice.call(arguments).map(function(x){return !/\\.css$/i.test(x) && new URL(x,self.url).href}).filter(Boolean))
}).bind(importScripts);importScripts(self.url=${JSON.stringify(url)})
`.trim();
return new W(URL.createObjectURL(new Blob([code], { type: 'text/javascript' })), options);
} as any as (typeof Worker);
function init() {
require('./init-app.tsx');
}
if (!('customElements' in self)) {
import(
/* webpackChunkName: "wc-polyfill" */
'@webcomponents/custom-elements').then(init);
import('@webcomponents/custom-elements').then(init);
} else {
init();
}
if (typeof PRERENDER === 'undefined') {
if (typeof process.env.PRERENDER === 'undefined') {
// Determine the current display mode.
let displayMode = 'browser';
const mqStandAlone = '(display-mode: standalone)';

View File

@ -1,6 +1,6 @@
import { h, render } from 'preact';
import './lib/fix-pmc';
import './style';
import './lib/fix-pmc.mjs';
import './style/index.scss';
import App from './components/App';
// Find the outermost Element in our server-rendered HTML structure.
@ -14,12 +14,8 @@ if (process.env.NODE_ENV !== 'production') {
// Enable support for React DevTools and some helpful console warnings:
require('preact/debug');
// When an update to any module is received, re-import the app and trigger a full re-render:
module.hot.accept('./components/App', () => {
// The linter doesn't like the capital A in App. It is wrong.
// tslint:disable-next-line variable-name
import('./components/App').then(({ default: App }) => {
root = render(<App />, document.body, root);
});
});
// Full HMR may not be working due to https://github.com/parcel-bundler/parcel/issues/5016
if (module.hot) {
module.hot.accept();
}
}

View File

@ -1,4 +1,4 @@
import * as style from './styles.css';
import * as style from './styles.module.css';
export interface SnackOptions {
timeout?: number;

View File

@ -60,7 +60,9 @@ export function getSharedImage(): Promise<File> {
/** Set up the service worker and monitor changes */
export async function offliner(showSnack: SnackBarElement['showSnackbar']) {
// This needs to be a typeof because Webpack.
if (typeof PRERENDER === 'boolean') return;
if (process.env.PRERENDER) return;
if (!navigator.serviceWorker) return;
if (process.env.NODE_ENV === 'production') {
navigator.serviceWorker.register('../sw');

View File

@ -2,6 +2,13 @@ interface CanvasRenderingContext2D {
filter: string;
}
declare module '*.module.scss' {
const classNameMapping: Record<string, string> & {
default: Record<string, string>
};
export = classNameMapping;
}
// Handling file-loader imports:
declare module '*.png' {
const content: string;
@ -28,7 +35,12 @@ declare module '*.wasm' {
export default content;
}
declare module 'url-loader!*' {
declare module 'url:*' {
const value: string;
export default value;
}
declare module 'data-url:*' {
const value: string;
export default value;
}
@ -41,5 +53,5 @@ declare var ga: {
};
interface Navigator {
readonly standalone: boolean;
readonly standalone: boolean;
}

View File

@ -1,4 +1,4 @@
import webpDataUrl from 'url-loader!../codecs/tiny.webp';
import webpDataUrl from 'url:../codecs/tiny.webp';
// Give TypeScript the correct global.
declare var self: ServiceWorkerGlobalScope;

View File

@ -6,6 +6,7 @@
"module": "esnext",
"moduleResolution": "node",
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"noUnusedLocals": true,
"sourceMap": true,
"jsx": "react",