Browse Source

Working on demo

Arjun Barrett 4 years ago
parent
commit
ba8147a1aa
17 changed files with 5858 additions and 235 deletions
  1. 4 1
      .gitignore
  2. 3 0
      .parcelrc
  3. 1 1
      README.md
  4. 18 1
      package.json
  5. 5 5
      scripts/buildUMD.ts
  6. 14 0
      src/demo/App.tsx
  7. 45 0
      src/demo/augment.d.ts
  8. 127 0
      src/demo/components/file-picker/index.tsx
  9. 9 0
      src/demo/index.css
  10. 13 0
      src/demo/index.html
  11. 11 0
      src/demo/index.tsx
  12. 41 0
      src/demo/sw.ts
  13. 134 0
      src/demo/util/workers.ts
  14. 4 2
      src/index.ts
  15. 3 3
      test/util.ts
  16. 8 0
      tsconfig.demo.json
  17. 5418 222
      yarn.lock

+ 4 - 1
.gitignore

@@ -5,4 +5,7 @@ esm/
 # Rust version - available when ready
 rs/
 .DS_STORE
-umd/
+umd/
+# for demo
+.parcel-cache
+dist/

+ 3 - 0
.parcelrc

@@ -0,0 +1,3 @@
+{
+  "extends": ["@parcel/config-default", "parcel-config-precache-manifest"]
+}

+ 1 - 1
README.md

@@ -4,7 +4,7 @@ High performance (de)compression in an 8kB package
 ## Why fflate?
 `fflate` (short for fast flate) is the **fastest, smallest, and most versatile** pure JavaScript compression and decompression library in existence, handily beating [`pako`](https://npmjs.com/package/pako), [`tiny-inflate`](https://npmjs.com/package/tiny-inflate), and [`UZIP.js`](https://github.com/photopea/UZIP.js) in performance benchmarks while being multiple times more lightweight. Its compression ratios are often better than even the original Zlib C library. It includes support for DEFLATE, GZIP, and Zlib data. Data compressed by `fflate` can be decompressed by other tools, and vice versa.
 
-In addition to the base decompression and compression APIs, `fflate` supports high-speed ZIP compression and decompression for an extra 3 kB. In fact, the compressor, in synchronous mode, compresses both more quickly and with a higher compression ratio than most compression software (even Info-ZIP, a C program), and in asynchronous mode it can utilize multiple threads to achieve over 3x the performance of any other utility.
+In addition to the base decompression and compression APIs, `fflate` supports high-speed ZIP file archiving for an extra 3 kB. In fact, the compressor, in synchronous mode, compresses both more quickly and with a higher compression ratio than most compression software (even Info-ZIP, a C program), and in asynchronous mode it can utilize multiple threads to achieve over 3x the performance of any other utility.
 
 |                             | `pako` | `tiny-inflate`         | `UZIP.js`             | `fflate`                       |
 |-----------------------------|--------|------------------------|-----------------------|--------------------------------|

+ 18 - 1
package.json

@@ -11,6 +11,15 @@
     "./lib/node-worker.js": "./lib/worker.js",
     "./esm/node-worker.js": "./esm/worker.js"
   },
+  "targets": {
+    "main": false,
+    "module": false,
+    "browser": false,
+    "types": false,
+    "demo": {
+      "distDir": "demo"
+    }
+  },
   "sideEffects": false,
   "repository": "https://github.com/101arrowz/fflate",
   "author": "Arjun Barrett",
@@ -33,11 +42,12 @@
     "non-blocking"
   ],
   "scripts": {
-    "build": "yarn build:lib && yarn build:docs && yarn build:rewrite",
+    "build": "yarn build:lib && yarn build:docs && yarn build:rewrite && yarn build:demo",
     "script": "node -r ts-node/register scripts/$SC.ts",
     "build:lib": "tsc && tsc --project tsconfig.esm.json && yarn build:umd",
     "build:umd": "SC=buildUMD yarn script",
     "build:rewrite": "SC=rewriteBuilds yarn script",
+    "build:demo": "parcel build src/demo/index.html --dist-dir demo",
     "build:docs": "typedoc --mode library --plugin typedoc-plugin-markdown --hideProjectName --hideBreadcrumbs --readme none --disableSources --excludePrivate --excludeProtected --out docs/ src/index.ts",
     "test": "TS_NODE_PROJECT=test/tsconfig.json uvu -b -r ts-node/register test",
     "prepack": "yarn build && yarn test"
@@ -49,6 +59,8 @@
     "@types/react-dom": "^16.9.9",
     "jszip": "^3.5.0",
     "pako": "*",
+    "parcel": "^2.0.0-nightly.440",
+    "parcel-config-precache-manifest": "^0.0.3",
     "preact": "^10.5.5",
     "react": "^17.0.1",
     "react-dom": "^17.0.1",
@@ -60,5 +72,10 @@
     "typescript": "^4.0.2",
     "uvu": "^0.3.3",
     "uzip": "*"
+  },
+  "alias": {
+    "react": "preact/compat",
+    "react-dom": "preact/compat",
+    "react-dom/test-utils": "preact/test-utils"
   }
 }

+ 5 - 5
scripts/buildUMD.ts

@@ -21,21 +21,21 @@ const opts: MinifyOptions = {
 };
 
 minify(src, opts).then(async out => {
-  const wkrOut = (await minify(worker, opts)).code.replace(
+  const wkrOut = (await minify(worker, opts)).code!.replace(
     /exports.__esModule=!0;/,
     ''
   ).replace(/exports\./g, '_f.');
-  const nodeWkrOut = (await minify(nodeWorker, opts)).code.replace(
+  const nodeWkrOut = (await minify(nodeWorker, opts)).code!.replace(
     /exports.__esModule=!0;/,
     ''
   ).replace(/exports\./g, '_f.').replace(
     /require/, // intentionally done just once
     "eval('require')"
   );
-  const res = "!function(f){typeof module!='undefined'&&typeof exports=='object'?module.exports=f():typeof define!='undefined'&&define.amd?define(['fflate',f]):(typeof self!='undefined'?self:this).fflate=f()}(function(){var _e = {};" +
-    out.code.replace(/require\("\.\/node-worker"\)/,
+  const res = "!function(f){typeof module!='undefined'&&typeof exports=='object'?module.exports=f():typeof define!='undefined'&&define.amd?define(['fflate',f]):(typeof self!='undefined'?self:this).fflate=f()}(function(){var _e={};" +
+    out.code!.replace(/exports\.(.*)=void 0;/, '').replace(/exports\./g, '_e.').replace(/require\("\.\/node-worker"\)/,
     "(typeof module!='undefined'&&typeof exports=='object'?function(_f){" + nodeWkrOut + 'return _f}:function(_f){' + wkrOut + 'return _f})({})'
-  ).replace(/exports\.(.*)=void 0;/, '').replace(/exports\./g, '_e.') + 'return _e})';
+  ) + 'return _e})';
   if (!existsSync(p('umd'))) mkdirSync(p('umd'));
   writeFileSync(p('umd', 'index.js'), res);
 });

+ 14 - 0
src/demo/App.tsx

@@ -0,0 +1,14 @@
+import React, { FC } from 'react';
+import FilePicker from './components/file-picker';
+
+const App: FC = () => {
+  return (
+    <div>
+      <FilePicker onFiles={f => {
+        console.log(f);
+      }} onError={console.log} onDrag={() => {}}>Hi</FilePicker>
+    </div>
+  );
+}
+
+export default App;

+ 45 - 0
src/demo/augment.d.ts

@@ -0,0 +1,45 @@
+declare module 'uzip' {
+  namespace UZIP {
+    function deflateRaw(buf: Uint8Array, opts?: { level: number }): Uint8Array;
+    function inflateRaw(buf: Uint8Array, out?: Uint8Array): Uint8Array;
+    function deflate(buf: Uint8Array, opts?: { level: number }): Uint8Array;
+    function inflate(buf: Uint8Array, out?: Uint8Array): Uint8Array;
+    function encode(files: Record<string, Uint8Array>, noCmpr?: boolean): Uint8Array;
+    function parse(buf: Uint8Array): Record<string, Uint8Array>;
+  }
+  export = UZIP;
+}
+
+interface DataTransferItem {
+  webkitGetAsEntry(): FileSystemEntry;
+}
+
+interface BaseFileSystemEntry {
+  fullPath: string;
+  name: string;
+  isFile: boolean;
+  isDirectory: boolean;
+}
+
+interface FileSystemFileEntry extends BaseFileSystemEntry {
+  isFile: true;
+  isDirectory: false
+  file(onSuccess: (file: File) => void, onError: (err: Error) => void): void;
+}
+
+type FileSystemEntry = FileSystemFileEntry | FileSystemDirectoryEntry;
+
+
+interface FileSystemDirectoryReader {
+  readEntries(onSuccess: (entries: FileSystemEntry[]) => void, onError: (err: Error) => void): void;
+}
+
+interface FileSystemDirectoryEntry extends BaseFileSystemEntry {
+  isFile: false;
+  isDirectory: true;
+  createReader(): FileSystemDirectoryReader;
+}
+
+interface File {
+  webkitRelativePath: string;
+}

+ 127 - 0
src/demo/components/file-picker/index.tsx

@@ -0,0 +1,127 @@
+import React, { FC, HTMLAttributes, InputHTMLAttributes, useEffect, useRef } from 'react';
+
+const supportsInputDirs = 'webkitdirectory' in HTMLInputElement.prototype;
+const supportsRelativePath = 'webkitRelativePath' in File.prototype;
+const supportsDirs = typeof DataTransferItem != 'undefined' && 'webkitGetAsEntry' in DataTransferItem.prototype;
+
+const readRecurse = (dir: FileSystemDirectoryEntry, onComplete: (files: File[]) => void, onError: (err: Error) => void) => {
+  let files: File[] = [];
+  let total = 0;
+  let errored = false;
+  let reachedEnd = false;
+  const onErr = (err: Error) => {
+    if (!errored) {
+      errored = true;
+      onError(err);
+    }
+  };
+  const onDone = (f: File[]) => {
+    files = files.concat(f);
+    if (!--total && reachedEnd) onComplete(files);
+  };
+  const reader = dir.createReader();
+  const onRead = (entries: FileSystemEntry[]) => {
+    if (!entries.length && !errored) {
+      if (!total) onComplete(files);
+      else reachedEnd = true;
+    } else reader.readEntries(onRead, onError);
+    for (const entry of entries) {
+      ++total;
+      if (entry.isFile) entry.file(f => onDone([
+        new File([f], entry.fullPath.slice(1), f)
+      ]), onErr);
+      else readRecurse(entry, onDone, onErr);
+    }
+  };
+  reader.readEntries(onRead, onError);
+}
+
+const FilePicker: FC<{
+  onFiles(files: File[]): void;
+  onDrag(on: boolean): void;
+  onError(err: string | Error): void;
+} & HTMLAttributes<HTMLDivElement>
+> = ({ onFiles, onDrag, onError, style, ...props }) => {
+  const inputRef = useRef<HTMLInputElement>(null);
+  const dirInputRef = useRef<HTMLInputElement>(null);
+  useEffect(() => {
+    // only init'd when support dirs
+    if (dirInputRef.current) {
+      dirInputRef.current.setAttribute('webkitdirectory', '');
+    }
+  }, []);
+  const rootProps: HTMLAttributes<HTMLDivElement> = {
+    onDrop(ev) {
+      ev.preventDefault();
+      const tf = ev.dataTransfer;
+      if (!tf.files.length) onError('Please drop some files in');
+      else {
+        if (supportsDirs) {
+          let outFiles: File[] = [];
+          let lft = tf.items.length;
+          let errored = false;
+          const onErr = (err: Error) => {
+            if (!errored) {
+              errored = true;
+              onError(err);
+            }
+          }
+          const onDone = (f: File[]) => {
+            outFiles = outFiles.concat(f);
+            if (!--lft && !errored) onFiles(outFiles);
+          };
+          for (let i = 0; i < tf.items.length; ++i) {
+            const entry = tf.items[i].webkitGetAsEntry();
+            if (entry.isFile) entry.file(f => onDone([f]), onErr);
+            else readRecurse(entry, onDone, onErr);
+          }
+        } else onFiles(Array.prototype.slice.call(tf.files));
+      }
+    },
+    onDragEnter() {
+      onDrag(true);
+    },
+    onDragOver(ev) {
+      ev.preventDefault();
+    },
+    onDragExit() {
+      onDrag(false);
+    },
+    style: {
+      display: 'flex',
+      flexDirection: 'row',
+      flex: 1,
+      ...style
+    }
+  };
+  const inputProps: InputHTMLAttributes<HTMLInputElement> = {
+    onInput(ev) {
+      const t = ev.currentTarget, files = t.files!;
+      if (supportsRelativePath) {
+        const outFiles: File[] = Array(files.length);
+        for (let i = 0; i < files.length; ++i) {
+          const file = files[i];
+          outFiles[i] = new File([file], file.webkitRelativePath, file);
+        }
+        onFiles(outFiles);
+      } else onFiles(Array.prototype.slice.call(files));
+      t.value = '';
+    },
+    style: { display: 'none' },
+    multiple: true
+  };
+  return (
+    <div {...props} {...rootProps}>
+      <input type="file" ref={inputRef} {...inputProps} />
+      <div onClick={() => inputRef.current!.click()}>Files</div>
+      {supportsInputDirs && 
+        <>
+          <input type="file" ref={dirInputRef} {...inputProps} />
+          <div onClick={() => dirInputRef.current!.click()}>Folders</div>
+        </>
+      }
+    </div>
+  );
+}
+
+export default FilePicker;

+ 9 - 0
src/demo/index.css

@@ -0,0 +1,9 @@
+html, body {
+  margin: 0;
+  padding: 0;
+}
+
+#app {
+  height: 100vh;
+  width: 100vw;
+}

+ 13 - 0
src/demo/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Document</title>
+  <link rel="stylesheet" href="index.css">
+</head>
+<body>
+  <div id="app"></div>
+  <script src="index.tsx"></script>
+</body>
+</html>

+ 11 - 0
src/demo/index.tsx

@@ -0,0 +1,11 @@
+import React from 'react';
+import App from './App';
+import { render } from 'react-dom';
+
+if (process.env.NODE_ENV == 'production') {
+  if ('serviceWorker' in navigator) {
+    navigator.serviceWorker.register('sw.ts');
+  }
+}
+
+render(<App />, document.getElementById('app'));

+ 41 - 0
src/demo/sw.ts

@@ -0,0 +1,41 @@
+/// <reference lib="webworker" />
+
+const sw = self as unknown as ServiceWorkerGlobalScope & {
+  __precacheManifest: ({ url: string, revision: string })[];
+};
+
+const precacheVersion = sw.__precacheManifest
+  .map(p => p.revision)
+  .join('');
+const precacheFiles = sw.__precacheManifest.map(p => p.url);
+
+const ch = () => caches.open(precacheVersion);
+ 
+sw.addEventListener('install', ev => {
+  // Do not finish installing until every file in the app has been cached
+  ev.waitUntil(
+    ch().then(
+      cache => cache.addAll(precacheFiles)
+    )
+  );
+});
+ 
+sw.addEventListener('activate', ev => {
+  ev.waitUntil(
+    caches.keys().then(keys => Promise.all(
+      keys.filter(k => k !== precacheVersion).map(
+        k => caches.delete(k)
+      )
+    )).then(() => sw.clients.claim())
+  );
+});
+
+sw.addEventListener('fetch', ev => {
+  ev.respondWith(
+    caches.match(ev.request).then(resp => resp || ch().then(c =>
+      fetch(ev.request).then(res => c.put(ev.request, res.clone()).then(
+        () => res
+      ))
+    ))
+  )
+});

+ 134 - 0
src/demo/util/workers.ts

@@ -0,0 +1,134 @@
+/// <reference lib="webworker" />
+import pako from 'pako';
+import * as UZIP from 'uzip';
+import JSZip from 'jszip';
+
+const dcmp = ['inflate', 'gunzip', 'unzlib'];
+
+const concat = (chunks: Uint8Array[]) => {
+  const out = new Uint8Array(
+    chunks.reduce((a, v) => v.length + a, 0)
+  );
+  let loc = 0;
+  for (const chunk of chunks) {
+    out.set(chunk, loc);
+    loc += chunk.length;
+  }
+  return out;
+}
+
+// CRC32 table
+const crct = new Uint32Array(256);
+for (let i = 0; i < 256; ++i) {
+  let c = i, k = 9;
+  while (--k) c = ((c & 1) && 0xEDB88320) ^ (c >>> 1);
+  crct[i] = c;
+}
+
+// CRC32
+const crc = (d: Uint8Array) => {
+  let c = 0xFFFFFFFF;
+  for (let i = 0; i < d.length; ++i) c = crct[(c & 255) ^ d[i]] ^ (c >>> 8);
+  return c ^ 0xFFFFFFFF;
+}
+
+const uzGzip = (d: Uint8Array) => {
+  const raw = UZIP.deflateRaw(d);
+  const head = new Uint8Array([31, 139, 8, 0, 0, 0, 0, 0, 0, 0]);
+  const c = crc(d);
+  const l = raw.length;
+  const tail = new Uint8Array([
+    c & 255, (c >>> 8) & 255, (c >>> 16) & 255, (c >>> 32) & 255,
+    l & 255, (l >>> 8) & 255, (l >>> 16) & 255, (l >>> 32) & 255,
+  ]);
+  return concat([head, raw, tail]);
+}
+
+onmessage = (ev: MessageEvent<[string, string]>) => {
+  const [lib, type] = ev.data;
+  if (lib == 'pako') {
+    if (type == 'zip') {
+      const zip = new JSZip();
+      onmessage = (ev: MessageEvent<null | [string, Uint8Array]>) => {
+        if (ev.data) {
+          zip.file(ev.data[0], ev.data[1]);
+        } else zip.generateAsync({
+          type: 'uint8array',
+          compressionOptions: { level: 6 }
+        }).then(buf => {
+          postMessage(buf, [buf.buffer]);
+        })
+      };
+    } else if (type == 'unzip') {
+      onmessage = (ev: MessageEvent<Uint8Array>) => {
+        JSZip.loadAsync(ev.data).then(zip => {
+          const out: Record<string, Uint8Array> = {};
+          const bufs: Promise<ArrayBuffer>[] = [];
+          for (const k in zip.files) {
+            const file = zip.files[k];
+            bufs.push(file.async('uint8array').then(v => {
+              out[file.name] = v;
+              return v.buffer;
+            }));
+          }
+          Promise.all(bufs).then(res => {
+            postMessage(out, res);
+          });
+        })
+      }
+    } else {
+      const strm = dcmp.indexOf(type) == -1
+      ? new pako.Deflate(type == 'gzip' ? {
+          gzip: true
+        } : {
+          raw: type == 'inflate'
+        }
+      ) : new pako.Inflate({
+        raw: type == 'deflate'
+      });
+      strm.onData = (chunk: Uint8Array) => {
+        postMessage(chunk, [chunk.buffer]);
+      };
+      onmessage = (ev: MessageEvent<[Uint8Array, boolean]>) => {
+        strm.push(ev.data[0], ev.data[1]);
+      };
+    }
+  } else if (lib == 'uzip') {
+    if (type == 'zip') {
+      const zip: Record<string, Uint8Array> = {};
+      onmessage = (ev: MessageEvent<null | [string, Uint8Array]>) => {
+        if (ev.data) {
+          zip[ev.data[0]] = ev.data[1];
+        } else {
+          const buf = UZIP.encode(zip);
+          postMessage(buf, [buf.buffer]);
+        }
+      };
+    } else if (type == 'unzip') {
+      onmessage = (ev: MessageEvent<Uint8Array>) => {
+        postMessage(UZIP.parse(ev.data));
+      }
+    } else {
+      const chunks: Uint8Array[] = [];
+      onmessage = (ev: MessageEvent<[Uint8Array, boolean]>) => {
+        chunks.push(ev.data[0]);
+        if (ev.data[1]) {
+          const out = concat(chunks);
+          const buf = type == 'inflate'
+            ? UZIP.inflateRaw(out)
+            : type == 'deflate'
+              ? UZIP.deflateRaw(out)
+              : type == 'zlib'
+                ? UZIP.deflate(out)
+                : type == 'unzlib'
+                  ? UZIP.inflate(out)
+                  : type == 'gzip'
+                    ? uzGzip(out)
+                    // we can pray that there's no special header
+                    : UZIP.inflateRaw(out.subarray(10, -8));
+          postMessage(buf, [buf.buffer]);
+        }
+      }
+    }
+  }
+}

+ 4 - 2
src/index.ts

@@ -67,7 +67,7 @@ const hMap = ((cd: Uint8Array, mb: number, r: 0 | 1) => {
   const s = cd.length;
   // index
   let i = 0;
-  // u8 "map": index -> # of codes with bit length = index
+  // u16 "map": index -> # of codes with bit length = index
   const l = new u16(mb);
   // length of cd must be 288 (total # of codes)
   for (; i < s; ++i) ++l[cd[i] - 1];
@@ -738,7 +738,7 @@ export interface DeflateOptions {
 export interface GzipOptions extends DeflateOptions {
   /**
    * When the file was last modified. Defaults to the current time.
-   * Set this to 0 to avoid specifying a modification date entirely.
+   * If you're using GZIP, set this to 0 to avoid revealing a modification date entirely.
    */
   mtime?: Date | string | number;
   /**
@@ -2060,6 +2060,8 @@ type ZipDat = AsyncZipDat & {
   o: number;
 }
 
+// TODO: Support streams as ZIP input
+
 /**
  * Asynchronously creates a ZIP file
  * @param data The directory structure for the ZIP archive

+ 3 - 3
test/util.ts

@@ -8,9 +8,9 @@ import { Worker } from 'worker_threads';
 const testFilesRaw = {
   basic: Buffer.from('Hello world!'),
   text: 'https://www.gutenberg.org/files/2701/old/moby10b.txt',
-  smallImage: 'https://www.hlevkin.com/TestImages/new/Rainier.bmp',
-  image: 'https://www.hlevkin.com/TestImages/new/Maltese.bmp',
-  largeImage: 'https://www.hlevkin.com/TestImages/new/Sunrise.bmp'
+  smallImage: 'https://hlevkin.com/hlevkin/TestImages/new/Rainier.bmp',
+  image: 'https://www.hlevkin.com/hlevkin/TestImages/new/Maltese.bmp',
+  largeImage: 'https://www.hlevkin.com/hlevkin/TestImages/new/Sunrise.bmp'
 };
 
 export type TestFile = keyof typeof testFilesRaw;

+ 8 - 0
tsconfig.demo.json

@@ -0,0 +1,8 @@
+{
+  "compilerOptions": {
+    "noEmit": true,
+    "target": "ESNext",
+    "moduleResolution": "node"
+  },
+  "include": ["src/demo/**/*.ts", "src/demo/**/*.tsx", "src/demo/augment.d.ts"]
+}

File diff suppressed because it is too large
+ 5418 - 222
yarn.lock


Some files were not shown because too many files changed in this diff