Buckets:
| /* | |
| MIT License http://www.opensource.org/licenses/mit-license.php | |
| Author Tobias Koppers @sokra | |
| */ | |
| ; | |
| const { | |
| JAVASCRIPT_MODULE_TYPE_AUTO, | |
| JAVASCRIPT_MODULE_TYPE_ESM, | |
| JAVASCRIPT_MODULE_TYPE_DYNAMIC | |
| } = require("./ModuleTypeConstants"); | |
| const RuntimeGlobals = require("./RuntimeGlobals"); | |
| const WebpackError = require("./WebpackError"); | |
| const ConstDependency = require("./dependencies/ConstDependency"); | |
| const BasicEvaluatedExpression = require("./javascript/BasicEvaluatedExpression"); | |
| const { | |
| evaluateToString, | |
| toConstantDependency | |
| } = require("./javascript/JavascriptParserHelpers"); | |
| const createHash = require("./util/createHash"); | |
| /** @typedef {import("estree").Expression} Expression */ | |
| /** @typedef {import("./Compilation").ValueCacheVersion} ValueCacheVersion */ | |
| /** @typedef {import("./Compiler")} Compiler */ | |
| /** @typedef {import("./Module").BuildInfo} BuildInfo */ | |
| /** @typedef {import("./NormalModule")} NormalModule */ | |
| /** @typedef {import("./RuntimeTemplate")} RuntimeTemplate */ | |
| /** @typedef {import("./javascript/JavascriptParser")} JavascriptParser */ | |
| /** @typedef {import("./javascript/JavascriptParser").DestructuringAssignmentProperty} DestructuringAssignmentProperty */ | |
| /** @typedef {import("./javascript/JavascriptParser").Range} Range */ | |
| /** @typedef {import("./logging/Logger").Logger} Logger */ | |
| /** @typedef {import("./util/createHash").Algorithm} Algorithm */ | |
| /** @typedef {null|undefined|RegExp|Function|string|number|boolean|bigint|undefined} CodeValuePrimitive */ | |
| /** @typedef {RecursiveArrayOrRecord<CodeValuePrimitive|RuntimeValue>} CodeValue */ | |
| /** | |
| * @typedef {object} RuntimeValueOptions | |
| * @property {string[]=} fileDependencies | |
| * @property {string[]=} contextDependencies | |
| * @property {string[]=} missingDependencies | |
| * @property {string[]=} buildDependencies | |
| * @property {string|function(): string=} version | |
| */ | |
| /** @typedef {function({ module: NormalModule, key: string, readonly version: ValueCacheVersion }): CodeValuePrimitive} GeneratorFn */ | |
| class RuntimeValue { | |
| /** | |
| * @param {GeneratorFn} fn generator function | |
| * @param {true | string[] | RuntimeValueOptions=} options options | |
| */ | |
| constructor(fn, options) { | |
| this.fn = fn; | |
| if (Array.isArray(options)) { | |
| options = { | |
| fileDependencies: options | |
| }; | |
| } | |
| this.options = options || {}; | |
| } | |
| get fileDependencies() { | |
| return this.options === true ? true : this.options.fileDependencies; | |
| } | |
| /** | |
| * @param {JavascriptParser} parser the parser | |
| * @param {Map<string, ValueCacheVersion>} valueCacheVersions valueCacheVersions | |
| * @param {string} key the defined key | |
| * @returns {CodeValuePrimitive} code | |
| */ | |
| exec(parser, valueCacheVersions, key) { | |
| const buildInfo = /** @type {BuildInfo} */ (parser.state.module.buildInfo); | |
| if (this.options === true) { | |
| buildInfo.cacheable = false; | |
| } else { | |
| if (this.options.fileDependencies) { | |
| for (const dep of this.options.fileDependencies) { | |
| /** @type {NonNullable<BuildInfo["fileDependencies"]>} */ | |
| (buildInfo.fileDependencies).add(dep); | |
| } | |
| } | |
| if (this.options.contextDependencies) { | |
| for (const dep of this.options.contextDependencies) { | |
| /** @type {NonNullable<BuildInfo["contextDependencies"]>} */ | |
| (buildInfo.contextDependencies).add(dep); | |
| } | |
| } | |
| if (this.options.missingDependencies) { | |
| for (const dep of this.options.missingDependencies) { | |
| /** @type {NonNullable<BuildInfo["missingDependencies"]>} */ | |
| (buildInfo.missingDependencies).add(dep); | |
| } | |
| } | |
| if (this.options.buildDependencies) { | |
| for (const dep of this.options.buildDependencies) { | |
| /** @type {NonNullable<BuildInfo["buildDependencies"]>} */ | |
| (buildInfo.buildDependencies).add(dep); | |
| } | |
| } | |
| } | |
| return this.fn({ | |
| module: parser.state.module, | |
| key, | |
| get version() { | |
| return valueCacheVersions.get(VALUE_DEP_PREFIX + key); | |
| } | |
| }); | |
| } | |
| getCacheVersion() { | |
| return this.options === true | |
| ? undefined | |
| : (typeof this.options.version === "function" | |
| ? this.options.version() | |
| : this.options.version) || "unset"; | |
| } | |
| } | |
| /** | |
| * @param {Set<DestructuringAssignmentProperty> | undefined} properties properties | |
| * @returns {Set<string> | undefined} used keys | |
| */ | |
| function getObjKeys(properties) { | |
| if (!properties) return; | |
| return new Set([...properties].map(p => p.id)); | |
| } | |
| /** @typedef {Set<string> | null} ObjKeys */ | |
| /** @typedef {boolean | undefined | null} AsiSafe */ | |
| /** | |
| * @param {any[]|{[k: string]: any}} obj obj | |
| * @param {JavascriptParser} parser Parser | |
| * @param {Map<string, ValueCacheVersion>} valueCacheVersions valueCacheVersions | |
| * @param {string} key the defined key | |
| * @param {RuntimeTemplate} runtimeTemplate the runtime template | |
| * @param {Logger} logger the logger object | |
| * @param {AsiSafe=} asiSafe asi safe (undefined: unknown, null: unneeded) | |
| * @param {ObjKeys=} objKeys used keys | |
| * @returns {string} code converted to string that evaluates | |
| */ | |
| const stringifyObj = ( | |
| obj, | |
| parser, | |
| valueCacheVersions, | |
| key, | |
| runtimeTemplate, | |
| logger, | |
| asiSafe, | |
| objKeys | |
| ) => { | |
| let code; | |
| const arr = Array.isArray(obj); | |
| if (arr) { | |
| code = `[${ | |
| /** @type {any[]} */ (obj) | |
| .map(code => | |
| toCode( | |
| code, | |
| parser, | |
| valueCacheVersions, | |
| key, | |
| runtimeTemplate, | |
| logger, | |
| null | |
| ) | |
| ) | |
| .join(",") | |
| }]`; | |
| } else { | |
| let keys = Object.keys(obj); | |
| if (objKeys) { | |
| keys = objKeys.size === 0 ? [] : keys.filter(k => objKeys.has(k)); | |
| } | |
| code = `{${keys | |
| .map(key => { | |
| const code = /** @type {{[k: string]: any}} */ (obj)[key]; | |
| return `${JSON.stringify(key)}:${toCode( | |
| code, | |
| parser, | |
| valueCacheVersions, | |
| key, | |
| runtimeTemplate, | |
| logger, | |
| null | |
| )}`; | |
| }) | |
| .join(",")}}`; | |
| } | |
| switch (asiSafe) { | |
| case null: | |
| return code; | |
| case true: | |
| return arr ? code : `(${code})`; | |
| case false: | |
| return arr ? `;${code}` : `;(${code})`; | |
| default: | |
| return `/*#__PURE__*/Object(${code})`; | |
| } | |
| }; | |
| /** | |
| * Convert code to a string that evaluates | |
| * @param {CodeValue} code Code to evaluate | |
| * @param {JavascriptParser} parser Parser | |
| * @param {Map<string, ValueCacheVersion>} valueCacheVersions valueCacheVersions | |
| * @param {string} key the defined key | |
| * @param {RuntimeTemplate} runtimeTemplate the runtime template | |
| * @param {Logger} logger the logger object | |
| * @param {boolean | undefined | null=} asiSafe asi safe (undefined: unknown, null: unneeded) | |
| * @param {ObjKeys=} objKeys used keys | |
| * @returns {string} code converted to string that evaluates | |
| */ | |
| const toCode = ( | |
| code, | |
| parser, | |
| valueCacheVersions, | |
| key, | |
| runtimeTemplate, | |
| logger, | |
| asiSafe, | |
| objKeys | |
| ) => { | |
| const transformToCode = () => { | |
| if (code === null) { | |
| return "null"; | |
| } | |
| if (code === undefined) { | |
| return "undefined"; | |
| } | |
| if (Object.is(code, -0)) { | |
| return "-0"; | |
| } | |
| if (code instanceof RuntimeValue) { | |
| return toCode( | |
| code.exec(parser, valueCacheVersions, key), | |
| parser, | |
| valueCacheVersions, | |
| key, | |
| runtimeTemplate, | |
| logger, | |
| asiSafe | |
| ); | |
| } | |
| if (code instanceof RegExp && code.toString) { | |
| return code.toString(); | |
| } | |
| if (typeof code === "function" && code.toString) { | |
| return `(${code.toString()})`; | |
| } | |
| if (typeof code === "object") { | |
| return stringifyObj( | |
| code, | |
| parser, | |
| valueCacheVersions, | |
| key, | |
| runtimeTemplate, | |
| logger, | |
| asiSafe, | |
| objKeys | |
| ); | |
| } | |
| if (typeof code === "bigint") { | |
| return runtimeTemplate.supportsBigIntLiteral() | |
| ? `${code}n` | |
| : `BigInt("${code}")`; | |
| } | |
| return `${code}`; | |
| }; | |
| const strCode = transformToCode(); | |
| logger.debug(`Replaced "${key}" with "${strCode}"`); | |
| return strCode; | |
| }; | |
| /** | |
| * @param {CodeValue} code code | |
| * @returns {string | undefined} result | |
| */ | |
| const toCacheVersion = code => { | |
| if (code === null) { | |
| return "null"; | |
| } | |
| if (code === undefined) { | |
| return "undefined"; | |
| } | |
| if (Object.is(code, -0)) { | |
| return "-0"; | |
| } | |
| if (code instanceof RuntimeValue) { | |
| return code.getCacheVersion(); | |
| } | |
| if (code instanceof RegExp && code.toString) { | |
| return code.toString(); | |
| } | |
| if (typeof code === "function" && code.toString) { | |
| return `(${code.toString()})`; | |
| } | |
| if (typeof code === "object") { | |
| const items = Object.keys(code).map(key => ({ | |
| key, | |
| value: toCacheVersion(/** @type {Record<string, any>} */ (code)[key]) | |
| })); | |
| if (items.some(({ value }) => value === undefined)) return; | |
| return `{${items.map(({ key, value }) => `${key}: ${value}`).join(", ")}}`; | |
| } | |
| if (typeof code === "bigint") { | |
| return `${code}n`; | |
| } | |
| return `${code}`; | |
| }; | |
| const PLUGIN_NAME = "DefinePlugin"; | |
| const VALUE_DEP_PREFIX = `webpack/${PLUGIN_NAME} `; | |
| const VALUE_DEP_MAIN = `webpack/${PLUGIN_NAME}_hash`; | |
| const TYPEOF_OPERATOR_REGEXP = /^typeof\s+/; | |
| const WEBPACK_REQUIRE_FUNCTION_REGEXP = new RegExp( | |
| `${RuntimeGlobals.require}\\s*(!?\\.)` | |
| ); | |
| const WEBPACK_REQUIRE_IDENTIFIER_REGEXP = new RegExp(RuntimeGlobals.require); | |
| class DefinePlugin { | |
| /** | |
| * Create a new define plugin | |
| * @param {Record<string, CodeValue>} definitions A map of global object definitions | |
| */ | |
| constructor(definitions) { | |
| this.definitions = definitions; | |
| } | |
| /** | |
| * @param {GeneratorFn} fn generator function | |
| * @param {true | string[] | RuntimeValueOptions=} options options | |
| * @returns {RuntimeValue} runtime value | |
| */ | |
| static runtimeValue(fn, options) { | |
| return new RuntimeValue(fn, options); | |
| } | |
| /** | |
| * Apply the plugin | |
| * @param {Compiler} compiler the compiler instance | |
| * @returns {void} | |
| */ | |
| apply(compiler) { | |
| const definitions = this.definitions; | |
| compiler.hooks.compilation.tap( | |
| PLUGIN_NAME, | |
| (compilation, { normalModuleFactory }) => { | |
| const logger = compilation.getLogger("webpack.DefinePlugin"); | |
| compilation.dependencyTemplates.set( | |
| ConstDependency, | |
| new ConstDependency.Template() | |
| ); | |
| const { runtimeTemplate } = compilation; | |
| const mainHash = createHash( | |
| /** @type {Algorithm} */ | |
| (compilation.outputOptions.hashFunction) | |
| ); | |
| mainHash.update( | |
| /** @type {string} */ | |
| (compilation.valueCacheVersions.get(VALUE_DEP_MAIN)) || "" | |
| ); | |
| /** | |
| * Handler | |
| * @param {JavascriptParser} parser Parser | |
| * @returns {void} | |
| */ | |
| const handler = parser => { | |
| const mainValue = compilation.valueCacheVersions.get(VALUE_DEP_MAIN); | |
| parser.hooks.program.tap(PLUGIN_NAME, () => { | |
| const buildInfo = /** @type {BuildInfo} */ ( | |
| parser.state.module.buildInfo | |
| ); | |
| if (!buildInfo.valueDependencies) | |
| buildInfo.valueDependencies = new Map(); | |
| buildInfo.valueDependencies.set(VALUE_DEP_MAIN, mainValue); | |
| }); | |
| /** | |
| * @param {string} key key | |
| */ | |
| const addValueDependency = key => { | |
| const buildInfo = | |
| /** @type {BuildInfo} */ | |
| (parser.state.module.buildInfo); | |
| /** @type {NonNullable<BuildInfo["valueDependencies"]>} */ | |
| (buildInfo.valueDependencies).set( | |
| VALUE_DEP_PREFIX + key, | |
| compilation.valueCacheVersions.get(VALUE_DEP_PREFIX + key) | |
| ); | |
| }; | |
| /** | |
| * @template {Function} T | |
| * @param {string} key key | |
| * @param {T} fn fn | |
| * @returns {function(TODO): TODO} result | |
| */ | |
| const withValueDependency = | |
| (key, fn) => | |
| (...args) => { | |
| addValueDependency(key); | |
| return fn(...args); | |
| }; | |
| /** | |
| * Walk definitions | |
| * @param {Record<string, CodeValue>} definitions Definitions map | |
| * @param {string} prefix Prefix string | |
| * @returns {void} | |
| */ | |
| const walkDefinitions = (definitions, prefix) => { | |
| for (const key of Object.keys(definitions)) { | |
| const code = definitions[key]; | |
| if ( | |
| code && | |
| typeof code === "object" && | |
| !(code instanceof RuntimeValue) && | |
| !(code instanceof RegExp) | |
| ) { | |
| walkDefinitions( | |
| /** @type {Record<string, CodeValue>} */ (code), | |
| `${prefix + key}.` | |
| ); | |
| applyObjectDefine(prefix + key, code); | |
| continue; | |
| } | |
| applyDefineKey(prefix, key); | |
| applyDefine(prefix + key, code); | |
| } | |
| }; | |
| /** | |
| * Apply define key | |
| * @param {string} prefix Prefix | |
| * @param {string} key Key | |
| * @returns {void} | |
| */ | |
| const applyDefineKey = (prefix, key) => { | |
| const splittedKey = key.split("."); | |
| for (const [i, _] of splittedKey.slice(1).entries()) { | |
| const fullKey = prefix + splittedKey.slice(0, i + 1).join("."); | |
| parser.hooks.canRename.for(fullKey).tap(PLUGIN_NAME, () => { | |
| addValueDependency(key); | |
| return true; | |
| }); | |
| } | |
| }; | |
| /** | |
| * Apply Code | |
| * @param {string} key Key | |
| * @param {CodeValue} code Code | |
| * @returns {void} | |
| */ | |
| const applyDefine = (key, code) => { | |
| const originalKey = key; | |
| const isTypeof = TYPEOF_OPERATOR_REGEXP.test(key); | |
| if (isTypeof) key = key.replace(TYPEOF_OPERATOR_REGEXP, ""); | |
| let recurse = false; | |
| let recurseTypeof = false; | |
| if (!isTypeof) { | |
| parser.hooks.canRename.for(key).tap(PLUGIN_NAME, () => { | |
| addValueDependency(originalKey); | |
| return true; | |
| }); | |
| parser.hooks.evaluateIdentifier | |
| .for(key) | |
| .tap(PLUGIN_NAME, expr => { | |
| /** | |
| * this is needed in case there is a recursion in the DefinePlugin | |
| * to prevent an endless recursion | |
| * e.g.: new DefinePlugin({ | |
| * "a": "b", | |
| * "b": "a" | |
| * }); | |
| */ | |
| if (recurse) return; | |
| addValueDependency(originalKey); | |
| recurse = true; | |
| const res = parser.evaluate( | |
| toCode( | |
| code, | |
| parser, | |
| compilation.valueCacheVersions, | |
| key, | |
| runtimeTemplate, | |
| logger, | |
| null | |
| ) | |
| ); | |
| recurse = false; | |
| res.setRange(/** @type {Range} */ (expr.range)); | |
| return res; | |
| }); | |
| parser.hooks.expression.for(key).tap(PLUGIN_NAME, expr => { | |
| addValueDependency(originalKey); | |
| let strCode = toCode( | |
| code, | |
| parser, | |
| compilation.valueCacheVersions, | |
| originalKey, | |
| runtimeTemplate, | |
| logger, | |
| !parser.isAsiPosition(/** @type {Range} */ (expr.range)[0]), | |
| null | |
| ); | |
| if (parser.scope.inShorthand) { | |
| strCode = `${parser.scope.inShorthand}:${strCode}`; | |
| } | |
| if (WEBPACK_REQUIRE_FUNCTION_REGEXP.test(strCode)) { | |
| return toConstantDependency(parser, strCode, [ | |
| RuntimeGlobals.require | |
| ])(expr); | |
| } else if (WEBPACK_REQUIRE_IDENTIFIER_REGEXP.test(strCode)) { | |
| return toConstantDependency(parser, strCode, [ | |
| RuntimeGlobals.requireScope | |
| ])(expr); | |
| } | |
| return toConstantDependency(parser, strCode)(expr); | |
| }); | |
| } | |
| parser.hooks.evaluateTypeof.for(key).tap(PLUGIN_NAME, expr => { | |
| /** | |
| * this is needed in case there is a recursion in the DefinePlugin | |
| * to prevent an endless recursion | |
| * e.g.: new DefinePlugin({ | |
| * "typeof a": "typeof b", | |
| * "typeof b": "typeof a" | |
| * }); | |
| */ | |
| if (recurseTypeof) return; | |
| recurseTypeof = true; | |
| addValueDependency(originalKey); | |
| const codeCode = toCode( | |
| code, | |
| parser, | |
| compilation.valueCacheVersions, | |
| originalKey, | |
| runtimeTemplate, | |
| logger, | |
| null | |
| ); | |
| const typeofCode = isTypeof ? codeCode : `typeof (${codeCode})`; | |
| const res = parser.evaluate(typeofCode); | |
| recurseTypeof = false; | |
| res.setRange(/** @type {Range} */ (expr.range)); | |
| return res; | |
| }); | |
| parser.hooks.typeof.for(key).tap(PLUGIN_NAME, expr => { | |
| addValueDependency(originalKey); | |
| const codeCode = toCode( | |
| code, | |
| parser, | |
| compilation.valueCacheVersions, | |
| originalKey, | |
| runtimeTemplate, | |
| logger, | |
| null | |
| ); | |
| const typeofCode = isTypeof ? codeCode : `typeof (${codeCode})`; | |
| const res = parser.evaluate(typeofCode); | |
| if (!res.isString()) return; | |
| return toConstantDependency( | |
| parser, | |
| JSON.stringify(res.string) | |
| ).bind(parser)(expr); | |
| }); | |
| }; | |
| /** | |
| * Apply Object | |
| * @param {string} key Key | |
| * @param {object} obj Object | |
| * @returns {void} | |
| */ | |
| const applyObjectDefine = (key, obj) => { | |
| parser.hooks.canRename.for(key).tap(PLUGIN_NAME, () => { | |
| addValueDependency(key); | |
| return true; | |
| }); | |
| parser.hooks.evaluateIdentifier.for(key).tap(PLUGIN_NAME, expr => { | |
| addValueDependency(key); | |
| return new BasicEvaluatedExpression() | |
| .setTruthy() | |
| .setSideEffects(false) | |
| .setRange(/** @type {Range} */ (expr.range)); | |
| }); | |
| parser.hooks.evaluateTypeof | |
| .for(key) | |
| .tap( | |
| PLUGIN_NAME, | |
| withValueDependency(key, evaluateToString("object")) | |
| ); | |
| parser.hooks.expression.for(key).tap(PLUGIN_NAME, expr => { | |
| addValueDependency(key); | |
| let strCode = stringifyObj( | |
| obj, | |
| parser, | |
| compilation.valueCacheVersions, | |
| key, | |
| runtimeTemplate, | |
| logger, | |
| !parser.isAsiPosition(/** @type {Range} */ (expr.range)[0]), | |
| getObjKeys(parser.destructuringAssignmentPropertiesFor(expr)) | |
| ); | |
| if (parser.scope.inShorthand) { | |
| strCode = `${parser.scope.inShorthand}:${strCode}`; | |
| } | |
| if (WEBPACK_REQUIRE_FUNCTION_REGEXP.test(strCode)) { | |
| return toConstantDependency(parser, strCode, [ | |
| RuntimeGlobals.require | |
| ])(expr); | |
| } else if (WEBPACK_REQUIRE_IDENTIFIER_REGEXP.test(strCode)) { | |
| return toConstantDependency(parser, strCode, [ | |
| RuntimeGlobals.requireScope | |
| ])(expr); | |
| } | |
| return toConstantDependency(parser, strCode)(expr); | |
| }); | |
| parser.hooks.typeof | |
| .for(key) | |
| .tap( | |
| PLUGIN_NAME, | |
| withValueDependency( | |
| key, | |
| toConstantDependency(parser, JSON.stringify("object")) | |
| ) | |
| ); | |
| }; | |
| walkDefinitions(definitions, ""); | |
| }; | |
| normalModuleFactory.hooks.parser | |
| .for(JAVASCRIPT_MODULE_TYPE_AUTO) | |
| .tap(PLUGIN_NAME, handler); | |
| normalModuleFactory.hooks.parser | |
| .for(JAVASCRIPT_MODULE_TYPE_DYNAMIC) | |
| .tap(PLUGIN_NAME, handler); | |
| normalModuleFactory.hooks.parser | |
| .for(JAVASCRIPT_MODULE_TYPE_ESM) | |
| .tap(PLUGIN_NAME, handler); | |
| /** | |
| * Walk definitions | |
| * @param {Record<string, CodeValue>} definitions Definitions map | |
| * @param {string} prefix Prefix string | |
| * @returns {void} | |
| */ | |
| const walkDefinitionsForValues = (definitions, prefix) => { | |
| for (const key of Object.keys(definitions)) { | |
| const code = definitions[key]; | |
| const version = toCacheVersion(code); | |
| const name = VALUE_DEP_PREFIX + prefix + key; | |
| mainHash.update(`|${prefix}${key}`); | |
| const oldVersion = compilation.valueCacheVersions.get(name); | |
| if (oldVersion === undefined) { | |
| compilation.valueCacheVersions.set(name, version); | |
| } else if (oldVersion !== version) { | |
| const warning = new WebpackError( | |
| `${PLUGIN_NAME}\nConflicting values for '${prefix + key}'` | |
| ); | |
| warning.details = `'${oldVersion}' !== '${version}'`; | |
| warning.hideStack = true; | |
| compilation.warnings.push(warning); | |
| } | |
| if ( | |
| code && | |
| typeof code === "object" && | |
| !(code instanceof RuntimeValue) && | |
| !(code instanceof RegExp) | |
| ) { | |
| walkDefinitionsForValues( | |
| /** @type {Record<string, CodeValue>} */ (code), | |
| `${prefix + key}.` | |
| ); | |
| } | |
| } | |
| }; | |
| walkDefinitionsForValues(definitions, ""); | |
| compilation.valueCacheVersions.set( | |
| VALUE_DEP_MAIN, | |
| /** @type {string} */ (mainHash.digest("hex").slice(0, 8)) | |
| ); | |
| } | |
| ); | |
| } | |
| } | |
| module.exports = DefinePlugin; | |
Xet Storage Details
- Size:
- 19.9 kB
- Xet hash:
- a90a1157a95623bf848f2cc49bbeb56bcaed8f7378f54a28806d8f4c1cc09a4c
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.