not finished
This commit is contained in:
1
packages/requirefs/src/index.ts
Normal file
1
packages/requirefs/src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./requirefs";
|
170
packages/requirefs/src/requirefs.ts
Normal file
170
packages/requirefs/src/requirefs.ts
Normal file
@ -0,0 +1,170 @@
|
||||
import * as JSZip from "jszip";
|
||||
import * as path from "path";
|
||||
import * as resolve from "resolve";
|
||||
import { Tar } from "./tarReader";
|
||||
const textDecoder = new (typeof TextDecoder === "undefined" ? require("text-encoding").TextDecoder : TextDecoder)();
|
||||
|
||||
export interface IFileReader {
|
||||
exists(path: string): boolean;
|
||||
read(path: string): Uint8Array;
|
||||
}
|
||||
|
||||
/**
|
||||
* RequireFS allows users to require from a file system.
|
||||
*/
|
||||
export class RequireFS {
|
||||
|
||||
private readonly reader: IFileReader;
|
||||
private readonly customModules: Map<string, { exports: object }>;
|
||||
private readonly requireCache: Map<string, { exports: object }>;
|
||||
private baseDir: string | undefined;
|
||||
|
||||
public constructor(reader: IFileReader) {
|
||||
this.reader = reader;
|
||||
this.customModules = new Map();
|
||||
this.requireCache = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a base-directory to nest from.
|
||||
*/
|
||||
public basedir(path: string): void {
|
||||
this.baseDir = path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide custom modules to the require instance.
|
||||
*/
|
||||
// tslint:disable-next-line:no-any
|
||||
public provide(module: string, value: any): void {
|
||||
if (this.customModules.has(module)) {
|
||||
throw new Error("custom module has already been registered with this name");
|
||||
}
|
||||
|
||||
this.customModules.set(module, value);
|
||||
}
|
||||
|
||||
public readFile(target: string, type?: "string"): string;
|
||||
public readFile(target: string, type?: "buffer"): Buffer;
|
||||
|
||||
/**
|
||||
* Read a file and returns its contents.
|
||||
*/
|
||||
public readFile(target: string, type?: "string" | "buffer"): string | Buffer {
|
||||
target = path.normalize(target);
|
||||
const read = this.reader.read(target);
|
||||
|
||||
return type === "string" ? textDecoder.decode(read) : Buffer.from(read);
|
||||
}
|
||||
|
||||
/**
|
||||
* Require a path from a file system.
|
||||
*/
|
||||
// tslint:disable-next-line:no-any
|
||||
public require(target: string): any {
|
||||
target = path.normalize(target);
|
||||
|
||||
return this.doRequire([target], `./${path.basename(target)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Do require for a caller. Needed for resolving relative paths.
|
||||
*/
|
||||
private doRequire(callers: string[], resolvePath: string): object {
|
||||
if (this.customModules.has(resolvePath)) {
|
||||
return this.customModules.get(resolvePath)!.exports;
|
||||
}
|
||||
|
||||
const caller = callers[callers.length - 1];
|
||||
const reader = this.reader;
|
||||
|
||||
const newRelative = this.realizePath(caller, resolvePath);
|
||||
if (this.requireCache.has(newRelative)) {
|
||||
return this.requireCache.get(newRelative)!.exports;
|
||||
}
|
||||
|
||||
const module = {
|
||||
exports: {},
|
||||
};
|
||||
this.requireCache.set(newRelative, module);
|
||||
|
||||
const content = textDecoder.decode(reader.read(newRelative));
|
||||
if (newRelative.endsWith(".json")) {
|
||||
module.exports = JSON.parse(content);
|
||||
} else {
|
||||
eval("'use strict'; " + content);
|
||||
}
|
||||
|
||||
return module.exports;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to find a module from a path
|
||||
*/
|
||||
private realizePath(caller: string, fullRelative: string): string {
|
||||
const stripPrefix = (path: string): string => {
|
||||
if (path.startsWith("/")) {
|
||||
path = path.substr(1);
|
||||
}
|
||||
if (path.endsWith("/")) {
|
||||
path = path.substr(0, path.length - 1);
|
||||
}
|
||||
|
||||
return path;
|
||||
};
|
||||
const callerDirname = path.dirname(caller);
|
||||
const resolvedPath = resolve.sync(fullRelative, {
|
||||
basedir: this.baseDir ? callerDirname.startsWith(this.baseDir) ? callerDirname : path.join(this.baseDir, callerDirname) : callerDirname,
|
||||
extensions: [".js"],
|
||||
readFileSync: (file: string): string => {
|
||||
return this.readFile(stripPrefix(file));
|
||||
},
|
||||
isFile: (file: string): boolean => {
|
||||
return this.reader.exists(stripPrefix(file));
|
||||
},
|
||||
});
|
||||
|
||||
return stripPrefix(resolvedPath);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const fromTar = (content: Uint8Array): RequireFS => {
|
||||
const tar = Tar.fromUint8Array(content);
|
||||
|
||||
return new RequireFS({
|
||||
exists: (path: string): boolean => {
|
||||
return tar.files.has(path);
|
||||
},
|
||||
read: (path: string): Uint8Array => {
|
||||
const file = tar.files.get(path);
|
||||
if (!file) {
|
||||
throw new Error(`file "${path}" not found`);
|
||||
}
|
||||
|
||||
return file.read();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const fromZip = (content: Uint8Array): RequireFS => {
|
||||
const zip = new JSZip(content);
|
||||
|
||||
return new RequireFS({
|
||||
exists: (fsPath: string): boolean => {
|
||||
const file = zip.file(fsPath);
|
||||
|
||||
return typeof file !== "undefined" && file !== null;
|
||||
},
|
||||
read: (fsPath: string): Uint8Array => {
|
||||
const file = zip.file(fsPath);
|
||||
if (!file) {
|
||||
throw new Error(`file "${fsPath}" not found`);
|
||||
}
|
||||
|
||||
// TODO: Should refactor to allow a promise.
|
||||
// tslint:disable-next-line no-any
|
||||
return zip.file(fsPath).async("uint8array") as any;
|
||||
},
|
||||
});
|
||||
};
|
285
packages/requirefs/src/tarReader.ts
Normal file
285
packages/requirefs/src/tarReader.ts
Normal file
@ -0,0 +1,285 @@
|
||||
import * as path from "path";
|
||||
const textDecoder = new (typeof TextDecoder === "undefined" ? require("text-encoding").TextDecoder : TextDecoder)();
|
||||
|
||||
/**
|
||||
* Tar represents a tar archive.
|
||||
*/
|
||||
export class Tar {
|
||||
|
||||
/**
|
||||
* Return a tar object from a Uint8Array.
|
||||
*/
|
||||
public static fromUint8Array(array: Uint8Array): Tar {
|
||||
const reader = new Reader(array);
|
||||
|
||||
const tar = new Tar();
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const file = TarFile.fromReader(reader);
|
||||
if (file) {
|
||||
tar._files.set(path.normalize(file.name), file);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message === "EOF") {
|
||||
break;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
reader.unclamp();
|
||||
|
||||
return tar;
|
||||
}
|
||||
|
||||
private readonly _files: Map<string, TarFile>;
|
||||
|
||||
private constructor() {
|
||||
this._files = new Map();
|
||||
}
|
||||
|
||||
public get files(): ReadonlyMap<string, TarFile> {
|
||||
return this._files;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a tar files location within a reader
|
||||
*/
|
||||
export class TarFile {
|
||||
|
||||
/**
|
||||
* Locate a tar file from a reader.
|
||||
*/
|
||||
public static fromReader(reader: Reader): TarFile | undefined {
|
||||
const firstByte = reader.peek(1)[0];
|
||||
// If the first byte is nil, we know it isn't a filename
|
||||
if (firstByte === 0x00) {
|
||||
// The tar header is 512 bytes large. Its safe to skip here
|
||||
// because we know this block is not a header
|
||||
reader.skip(512);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let name = reader.readString(100);
|
||||
|
||||
reader.skip(8); // 100->108 mode
|
||||
reader.skip(8); // 108->116 uid
|
||||
reader.skip(8); // 116->124 gid
|
||||
|
||||
const rawSize = reader.read(12); // 124->136 size
|
||||
|
||||
reader.skip(12); // 136->148 mtime
|
||||
|
||||
if (reader.jump(345).readByte()) {
|
||||
name = reader.jump(345).readString(155) + "/" + name;
|
||||
}
|
||||
|
||||
const nums: number[] = [];
|
||||
rawSize.forEach((a) => nums.push(a));
|
||||
|
||||
const parseSize = (): number => {
|
||||
let offset = 0;
|
||||
// While 48 (ASCII value of 0), the byte is nil and considered padding.
|
||||
while (offset < rawSize.length && nums[offset] === 48) {
|
||||
offset++;
|
||||
}
|
||||
const clamp = (index: number, len: number, defaultValue: number): number => {
|
||||
if (typeof index !== "number") {
|
||||
return defaultValue;
|
||||
}
|
||||
// Coerce index to an integer.
|
||||
index = ~~index;
|
||||
if (index >= len) {
|
||||
return len;
|
||||
}
|
||||
if (index >= 0) {
|
||||
return index;
|
||||
}
|
||||
index += len;
|
||||
if (index >= 0) {
|
||||
return index;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Checks for the index of the POSIX file-size terminating char.
|
||||
// Falls back to GNU's tar format. If neither characters are found
|
||||
// the index will default to the end of the file size buffer.
|
||||
let i = nums.indexOf(32, offset);
|
||||
if (i === -1) {
|
||||
i = nums.indexOf(0, offset);
|
||||
if (i === -1) {
|
||||
i = rawSize.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
const end = clamp(i, rawSize.length, rawSize.length - 1);
|
||||
if (end === offset) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return parseInt(textDecoder.decode(rawSize.slice(offset, end)), 8);
|
||||
};
|
||||
|
||||
const size = parseSize();
|
||||
|
||||
const overflow = ((): number => {
|
||||
let newSize = size;
|
||||
newSize &= 511;
|
||||
|
||||
return newSize && 512 - newSize;
|
||||
})();
|
||||
|
||||
reader.jump(512);
|
||||
const offset = reader.offset;
|
||||
reader.skip(overflow + size);
|
||||
reader.clamp();
|
||||
|
||||
const tarFile = new TarFile(reader, {
|
||||
offset,
|
||||
name,
|
||||
size,
|
||||
});
|
||||
|
||||
return tarFile;
|
||||
}
|
||||
|
||||
public constructor(
|
||||
private readonly reader: Reader,
|
||||
private readonly data: {
|
||||
name: string;
|
||||
size: number;
|
||||
offset: number;
|
||||
},
|
||||
) { }
|
||||
|
||||
public get name(): string {
|
||||
return this.data.name;
|
||||
}
|
||||
|
||||
public get size(): number {
|
||||
return this.data.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the file type is a file.
|
||||
*/
|
||||
public isFile(): boolean {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the file as a string.
|
||||
*/
|
||||
public readAsString(): string {
|
||||
return textDecoder.decode(this.read());
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the file as Uint8Array.
|
||||
*/
|
||||
public read(): Uint8Array {
|
||||
return this.reader.jump(this.data.offset).read(this.data.size);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads within a Uint8Array.
|
||||
*/
|
||||
export class Reader {
|
||||
|
||||
private array: Uint8Array;
|
||||
private _offset: number;
|
||||
private lastClamp: number;
|
||||
|
||||
public constructor(array: Uint8Array) {
|
||||
this.array = array;
|
||||
this._offset = 0;
|
||||
this.lastClamp = 0;
|
||||
}
|
||||
|
||||
public get offset(): number {
|
||||
return this._offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip the specified amount of bytes.
|
||||
*/
|
||||
public skip(amount: number): boolean {
|
||||
if (this._offset + amount > this.array.length) {
|
||||
throw new Error("EOF");
|
||||
}
|
||||
this._offset += amount;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp the reader at a position.
|
||||
*/
|
||||
public clamp(): void {
|
||||
this.lastClamp = this._offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unclamp the reader.
|
||||
*/
|
||||
public unclamp(): void {
|
||||
this.lastClamp = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Jump to a specific offset.
|
||||
*/
|
||||
public jump(offset: number): Reader {
|
||||
this._offset = offset + this.lastClamp;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Peek the amount of bytes.
|
||||
*/
|
||||
public peek(amount: number): Uint8Array {
|
||||
return this.array.slice(this.offset, this.offset + amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a string.
|
||||
*/
|
||||
public readString(amount: number): string {
|
||||
// Replacing the 0s removes all nil bytes from the str
|
||||
return textDecoder.decode(this.read(amount)).replace(/\0/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a byte in the array.
|
||||
*/
|
||||
public readByte(): number {
|
||||
const data = this.array[this._offset];
|
||||
this._offset++;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the amount of bytes.
|
||||
*/
|
||||
public read(amount: number): Uint8Array {
|
||||
if (this._offset > this.array.length) {
|
||||
throw new Error("EOF");
|
||||
}
|
||||
|
||||
const data = this.array.slice(this._offset, this._offset + amount);
|
||||
this._offset += amount;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user