webpack代码之难读,可以说在我读过的源码里面也是前几的
究其原因,就是里面大量应用了 发布订阅模式 ,强依赖了Tapable,
导致 所有代码的执行流程支离破碎,经常读着读着,就会发现, “诶,这个函数是哪里来的?”
1. 编译准备工作
1.1 初始化 Compiler
webpack/lib/webpack.js#41
创建Compiler对象 -> 这个是横跨整个 打包周期的,拥有着文件读写的能力
compiler = new Compiler(options.context);
webpack/lib/webpack.js#43
将 node 读文件的能力 赋予Compiler
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
1.2 开启编译
webpack/lib/Compiler.js#247
开启编译
run(callback) {
....
this.hooks.beforeRun.callAsync(this, err => {
if (err) return finalCallback(err);
this.hooks.run.callAsync(this, err => {
if (err) return finalCallback(err);
this.readRecords(err => {
if (err) return finalCallback(err);
/** 注意这里的 onCompiled,在后文会提及 */
this.compile(onCompiled);
});
});
});
}
1.2 准备编译的工具
webpack/lib/Compiler.js#660
webpack/lib/Compiler.js#632
开启编译->
- 准备 编译的
params(normalModuleFactory模块工厂)- 创建
Compilation。 在一个 编译的过程中,会出现多个Compilation,如,监听到文件的改动,就会出现一个新的Compilation,它获取了 前文Compiler的读写能力- 触发
compilation&make钩子 ->- 让
SingleEntryPlugin监听 第3步的钩子执行- 让
JavascriptModulesPlugin监听 第3步的钩子执行
compile(callback) {
/** 准备打包的工具类 */
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
if (err) return callback(err);
this.hooks.compile.call(params);
/** 创建一个 Compilation */
const compilation = this.newCompilation(params);
/** 这里触发了 make 钩子 */
this.hooks.make.callAsync(compilation, err => {
if (err) return callback(err);
compilation.finish(err => {
if (err) return callback(err);
compilation.seal(err => {
if (err) return callback(err);
this.hooks.afterCompile.callAsync(compilation, err => {
if (err) return callback(err);
return callback(null, compilation);
});
});
});
});
});
}
newCompilationParams() {
const params = {
/** createNormalModuleFactory 创建 模块打包 工厂 */
/** 会在后文中的 SingleEntryPlugin 注入 Compilation */
normalModuleFactory: this.createNormalModuleFactory(),
contextModuleFactory: this.createContextModuleFactory(),
compilationDependencies: new Set()
};
return params;
}
createCompilation() {
return new Compilation(this);
}
newCompilation(params) {
const compilation = this.createCompilation();
compilation.fileTimestamps = this.fileTimestamps;
compilation.contextTimestamps = this.contextTimestamps;
compilation.name = this.name;
compilation.records = this.records;
compilation.compilationDependencies = params.compilationDependencies;
this.hooks.thisCompilation.call(compilation, params);
/** 这里触发了 SingleEntryPlugin 的钩子 */
this.hooks.compilation.call(compilation, params);
return compilation;
}
webpack/lib/SingleEntryPlugin.js#29
为上文提到的Compilation挂载 模块工厂的能力 和 依赖处理的容器
这个在后文中的 处理 依赖 极为有用
调用Compilation的addEntry,真正地开始编译流程
apply(compiler) {
compiler.hooks.compilation.tap(
"SingleEntryPlugin",
(compilation, { normalModuleFactory }) => {
compilation.dependencyFactories.set(
SingleEntryDependency,
/** 让 compilation 具有了 模块工厂的能力 */
normalModuleFactory
);
}
);
compiler.hooks.make.tapAsync(
"SingleEntryPlugin",
(compilation, callback) => {
/** 入口文件 */
// chunkname
/** 根目录 */
const { entry, name, context } = this;
/** 依赖关系处理 容器 */
const dep = SingleEntryPlugin.createDependency(entry, name);
/** 调用了 addEntry 函数,开始了 编译的过程 */
compilation.addEntry(context, dep, name, callback);
}
);
}
webpack/lib/JavascriptModulesPlugin.js#14
挂载了一堆的 hook,作用就是 处理 javascript 为 AST 语法树,这里的依赖是acorn
apply(compiler) {
compiler.hooks.compilation.tap(
"JavascriptModulesPlugin",
(compilation, { normalModuleFactory }) => {
normalModuleFactory.hooks.createParser
.for("javascript/auto")
.tap("JavascriptModulesPlugin", options => {
return new Parser(options, "auto");
});
normalModuleFactory.hooks.createParser
.for("javascript/dynamic")
.tap("JavascriptModulesPlugin", options => {
return new Parser(options, "script");
});
normalModuleFactory.hooks.createParser
.for("javascript/esm")
.tap("JavascriptModulesPlugin", options => {
return new Parser(options, "module");
});
normalModuleFactory.hooks.createGenerator
.for("javascript/auto")
.tap("JavascriptModulesPlugin", () => {
return new JavascriptGenerator();
});
normalModuleFactory.hooks.createGenerator
.for("javascript/dynamic")
.tap("JavascriptModulesPlugin", () => {
return new JavascriptGenerator();
});
normalModuleFactory.hooks.createGenerator
.for("javascript/esm")
.tap("JavascriptModulesPlugin", () => {
return new JavascriptGenerator();
});
...
}
2. 开始打包
2.1 开启打包流程
webpack/lib/Compilation.js#1143
在 上文中提到的SingleEntryPlugin的make钩子 调用了Compilation的addEntry
调用了 内部函数_addModuleChain开启了打包的流程
addEntry(context, entry, name, callback) {
this.hooks.addEntry.call(entry, name);
const slot = {
name: name,
// TODO webpack 5 remove `request`
request: null,
module: null
};
...
this._addModuleChain(
context,
entry,
module => {
this.entries.push(module);
},
(err, module) => {
if (err) {
this.hooks.failedEntry.call(entry, name, err);
return callback(err);
}
if (module) {
slot.module = module;
} else {
const idx = this._preparedEntrypoints.indexOf(slot);
if (idx >= 0) {
this._preparedEntrypoints.splice(idx, 1);
}
}
this.hooks.succeedEntry.call(entry, name, module);
return callback(null, module);
}
);
}
2.2 _addModuleChain 打包流程
webpack/lib/Compilation.js#1033
开启了一个 并发打包的过程
但是,还是但是,这里并不是一个 安安稳稳就执行下去的过程,
- 先调用了
normalModuleFactory的create- 等
create执行完之后,再调用 接下来的一堆 回调函数- 回调函数中 才开始
buildModule来 编译模块buildModule又有回调函数afterBuild,用来 加载依赖
_addModuleChain(context, dependency, onModule, callback) {
const start = this.profile && Date.now();
const currentProfile = this.profile && {};
...
// 并发打包
this.semaphore.acquire(() => {
moduleFactory.create(
{
contextInfo: {
issuer: "",
compiler: this.compiler.name
},
context: context,
dependencies: [dependency]
},
(err, module) => {
if (err) {
this.semaphore.release();
return errorAndCallback(new EntryModuleNotFoundError(err));
}
let afterFactory;
if (currentProfile) {
afterFactory = Date.now();
currentProfile.factory = afterFactory - start;
}
// 将模块内容加到缓存中
const addModuleResult = this.addModule(module);
module = addModuleResult.module;
onModule(module);
dependency.module = module;
module.addReason(null, dependency);
const afterBuild = () => {
if (addModuleResult.dependencies) {
/** 加载依赖 */
this.processModuleDependencies(module, err => {
if (err) return callback(err);
callback(null, module);
});
} else {
return callback(null, module);
}
};
if (addModuleResult.issuer) {
if (currentProfile) {
module.profile = currentProfile;
}
}
if (addModuleResult.build) {
// 开始编译模块
this.buildModule(module, false, null, null, err => {
if (err) {
this.semaphore.release();
return errorAndCallback(err);
}
if (currentProfile) {
const afterBuilding = Date.now();
currentProfile.building = afterBuilding - afterFactory;
}
this.semaphore.release();
afterBuild();
});
} else {
this.semaphore.release();
this.waitForBuildingFinished(module, afterBuild);
}
}
);
});
}
2.3 准备某个模块的打包原料
webpack/lib/NormalModuleFactory.js#373
承接 上一个 函数中的create调用,先是触发了beforeResolve钩子,又触发了factory钩子
注意的是,factory会去处理loader,所以是 将一个一个模块交给了 loader 处理一遍之后,再放入缓存中
然后 执行 上文中_addModuleChain插入 的callback,将createdModule返回
create(data, callback) {
const dependencies = data.dependencies;
const cacheEntry = dependencyCache.get(dependencies[0]);
if (cacheEntry) return callback(null, cacheEntry);
const context = data.context || this.context;
const resolveOptions = data.resolveOptions || EMPTY_RESOLVE_OPTIONS;
const request = dependencies[0].request;
const contextInfo = data.contextInfo || {};
this.hooks.beforeResolve.callAsync(
{
contextInfo,
resolveOptions,
context,
request,
dependencies
},
(err, result) => {
if (err) return callback(err);
// Ignored
if (!result) return callback();
/** !! 触发了 factory 钩子 */
const factory = this.hooks.factory.call(null);
// Ignored
if (!factory) return callback();
/** 将模块丢给 loader 处理 */
factory(result, (err, module) => {
if (err) return callback(err);
if (module && this.cachePredicate(module)) {
for (const d of dependencies) {
/** 将 模块放入 依赖缓存中 */
dependencyCache.set(d, module);
}
}
/** 这里的 callback 就是 上文中 _addModuleChain 调用 create 传入的回调 */
/** 调用链路太长,防止忘记再提醒一次 */
callback(null, module);
});
}
);
}
webpack/lib/NormalModuleFactory.js#124
上文中的factory钩子 就是在NormalModuleFactory的constructor注册的,省略的一堆的定义之后,如下文
将处理好的结果放到NormalModule中
这里是做好了build的前置工作,将文件内容、loader、parser、options 都封装完毕,并进行缓存
this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => {
/** 处理loader */
let resolver = this.hooks.resolver.call(null); //
// Ignored loader 被处理完毕
if (!resolver) return callback();
resolver(result, (err, data) => {
if (err) return callback(err);
// Ignored
if (!data) return callback();
// direct module
if (typeof data.source === "function") return callback(null, data);
this.hooks.afterResolve.callAsync(data, (err, result) => {
if (err) return callback(err);
// Ignored
if (!result) return callback();
let createdModule = this.hooks.createModule.call(result);
if (!createdModule) {
if (!result.request) {
return callback(new Error("Empty dependency (no request)"));
}
/** 将处理完成之后的结果放在 NormalModule 中 */
createdModule = new NormalModule(result);
}
createdModule = this.hooks.module.call(createdModule, result);
return callback(null, createdModule);
});
});
this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
const contextInfo = data.contextInfo;
const context = data.context;
const request = data.request;
const loaderResolver = this.getResolver("loader");
const normalResolver = this.getResolver("normal", data.resolveOptions);
asyncLib.parallel(
[
callback =>
this.resolveRequestArray(
contextInfo,
context,
elements,
loaderResolver,
callback
),
callback => {
...
}
],
(err, results) => {
...
asyncLib.parallel(
[
...
],
(err, results) => {
...
process.nextTick(() => {
const type = settings.type;
const resolveOptions = settings.resolve;
callback(null, {
context: context,
request: loaders
.map(loaderToIdent)
.concat([resource])
.join("!"),
dependencies: data.dependencies,
userRequest,
rawRequest: request,
loaders,
resource,
matchResource,
resourceResolveData,
settings,
type,
/** 就是上文中的 parser,解析 AST 语法树的 */
parser: this.getParser(type, settings.parser),
generator: this.getGenerator(type, settings.generator),
resolveOptions
});
});
}
);
}
);
});
}
2.4 正式编译
接下来就是进入
_addModuleChain的buildModule正式开始编译了 webpack/lib/Compilation.js#1112
这个函数其实在上文中写过,但是这里再写一次
this.buildModule(module, false, null, null, err => {
if (err) {
this.semaphore.release();
return errorAndCallback(err);
}
if (currentProfile) {
const afterBuilding = Date.now();
currentProfile.building = afterBuilding - afterFactory;
}
this.semaphore.release();
afterBuild();
});
webpack/lib/Compilation.js#723
- 调用了
module.build,- 触发了
succeedModule回调(用来标注当前的进度的)
buildModule(module, optional, origin, dependencies, thisCallback) {
let callbackList = this._buildingModules.get(module);
if (callbackList) {
callbackList.push(thisCallback);
return;
}
this._buildingModules.set(module, (callbackList = [thisCallback]));
...
this.hooks.buildModule.call(module);
module.build(
this.options,
this,
this.resolverFactory.get("normal", module.resolveOptions),
this.inputFileSystem,
error => {
...
const originalMap = module.dependencies.reduce((map, v, i) => {
map.set(v, i);
return map;
}, new Map());
module.dependencies.sort((a, b) => {
const cmp = compareLocations(a.loc, b.loc);
if (cmp) return cmp;
return originalMap.get(a) - originalMap.get(b);
});
if (error) {
this.hooks.failedModule.call(module, error);
return callback(error);
}
this.hooks.succeedModule.call(module);
return callback();
}
);
}
2.5 真的编译doBuild + 准备好 ast、source 等数据
webpack/lib/NormalModule.js#427
- 初始化了一堆数据,如
_ast, _source- 调用了
doBuild。。。,顾名思义,真的 在编译了- 然后 调用 上文中提及的 parse 解析 AST 语法树
build(options, compilation, resolver, fs, callback) { // 真家伙开始编译了
this.buildTimestamp = Date.now();
this.built = true;
this._source = null; // 源码
this._sourceSize = null;
this._ast = null; // AST
this._buildHash = "";
this.error = null;
this.errors.length = 0;
this.warnings.length = 0;
this.buildMeta = {};
this.buildInfo = {
cacheable: false,
fileDependencies: new Set(),
contextDependencies: new Set(),
assets: undefined,
assetsInfo: undefined
};
/** !!!!调用了 doBuild*/
return this.doBuild(options, compilation, resolver, fs, err => {
this._cachedSources.clear();
// if we have an error mark module as failed and exit
if (err) {
this.markModuleAsErrored(err);
this._initBuildHash(compilation);
return callback();
}
// check if this module should !not! be parsed.
// if so, exit here;
const noParseRule = options.module && options.module.noParse;
if (this.shouldPreventParsing(noParseRule, this.request)) {
this._initBuildHash(compilation);
return callback();
}
...
try {
const result = this.parser.parse(
/** 当前的 AST 语法树 */
this._ast || this._source.source(),
{
current: this,
module: this,
compilation: compilation,
options: options
},
(err, result) => {
if (err) {
handleParseError(err);
} else {
handleParseResult(result);
}
}
);
if (result !== undefined) {
// parse is sync
handleParseResult(result);
}
} catch (e) {
handleParseError(e);
}
});
}
2.6 运行 loader 并调用 callback
webpack/lib/NormalModule.js#287
- 使用了
loader-runner依赖 来运行当前的 准备的module- 然后给 上文中定义的各个 属性赋值
- 调用
callback
doBuild(options, compilation, resolver, fs, callback) {
const loaderContext = this.createLoaderContext(
resolver,
options,
compilation,
fs
);
/** 先处理loader -> loader-runner */
runLoaders(
{
resource: this.resource,
loaders: this.loaders,
context: loaderContext,
readResource: fs.readFile.bind(fs)
},
(err, result) => {
if (result) {
this.buildInfo.cacheable = result.cacheable;
/** 记录当前的依赖信息 */
this.buildInfo.fileDependencies = new Set(result.fileDependencies);
this.buildInfo.contextDependencies = new Set(
result.contextDependencies
);
}
...
const resourceBuffer = result.resourceBuffer;
const source = result.result[0];
const sourceMap = result.result.length >= 1 ? result.result[1] : null;
const extraInfo = result.result.length >= 2 ? result.result[2] : null;
if (!Buffer.isBuffer(source) && typeof source !== "string") {
const currentLoader = this.getCurrentLoader(loaderContext, 0);
const err = new Error(
`Final loader (${
currentLoader
? compilation.runtimeTemplate.requestShortener.shorten(
currentLoader.loader
)
: "unknown"
}) didn't return a Buffer or String`
);
const error = new ModuleBuildError(this, err);
return callback(error);
}
this._source = this.createSource(
this.binary ? asBuffer(source) : asString(source),
resourceBuffer,
sourceMap
);
this._sourceSize = null;
this._ast =
typeof extraInfo === "object" &&
extraInfo !== null &&
extraInfo.webpackAST !== undefined
? extraInfo.webpackAST
: null;
return callback();
}
);
}
2.7 梳理 callback 以及 onCompiled
我们可以简单的看一下,上一段
doBuild中的callback来源
webpack/lib/NormalModule.js#287
webpack/lib/NormalModule.js#445
webpack/lib/NormalModule.js#427
webpack/lib/Compilation.js#739 (这个 build 的回调函数底下还有一个 callback )
webpack/lib/Compilation.js#731
webpack/lib/Compilation.js#723 -> 里的thisCallback
webpack/lib/Compilation.js#1112 -> 里的afterBuild\ webpack/lib/Compilation.js#1097
webpack/lib/Compilation.js#1033
webpack/lib/Compilation.js#1165
webpack/lib/Compilation.js#1143
webpack/lib/SingleEntryPlugin.js#49
webpack/lib/Compiler.js#669
webpack/lib/Compiler.js#321
OKK,可以看到最后会调用onCompiled
- 触发了
shouldEmit钩子,如果返回false,就停止执行- 调用了
emitAssets函数,将 文件写入- 触发
done钩子
const onCompiled = (err, compilation) => {
if (err) return finalCallback(err);
if (this.hooks.shouldEmit.call(compilation) === false) {
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.hooks.done.callAsync(stats, err => {
if (err) return finalCallback(err);
return finalCallback(null, stats);
});
return;
}
this.emitAssets(compilation, err => {
if (err) return finalCallback(err);
if (compilation.hooks.needAdditionalPass.call()) {
compilation.needAdditionalPass = true;
/** 当前文件的 Stats */
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.hooks.done.callAsync(stats, err => {
if (err) return finalCallback(err);
this.hooks.additionalPass.callAsync(err => {
if (err) return finalCallback(err);
this.compile(onCompiled);
});
});
return;
}
this.emitRecords(err => {
if (err) return finalCallback(err);
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.hooks.done.callAsync(stats, err => {
if (err) return finalCallback(err);
return finalCallback(null, stats);
});
});
});
};
2.8 加载依赖
webpack/lib/Compilation.js#1112 -> 里的
afterBuild\ 回过头看上文这么多的文件查找,这里还有一个 加载依赖的过程
如果当前模块存在 依赖时,进行加载,不执行 onCompiledprocessModuleDependencies中,就会获取 上文SingleEntryPlugin提到的normalModuleFactoryaddModuleDependencies又会循环调用processModuleDependencies,就不再展开
const afterBuild = () => {
if (recursive && addModuleResult.dependencies) {
/** 加载依赖*/
this.processModuleDependencies(dependentModule, callback);
} else {
return callback();
}
};
processModuleDependencies(module, callback) {
const dependencies = new Map();
const addDependency = dep => {
const resourceIdent = dep.getResourceIdentifier();
if (resourceIdent) {
/** 这里的 factory 来自于 SignalFactory */
const factory = this.dependencyFactories.get(dep.constructor);
if (factory === undefined) {
throw new Error(
`No module factory available for dependency type: ${dep.constructor.name}`
);
}
let innerMap = dependencies.get(factory);
if (innerMap === undefined) {
dependencies.set(factory, (innerMap = new Map()));
}
let list = innerMap.get(resourceIdent);
if (list === undefined) innerMap.set(resourceIdent, (list = []));
list.push(dep);
}
};
...
this.addModuleDependencies(
module,
sortedDependencies,
this.bail,
null,
true,
callback
);
}
2.9 开始处理 chunk
webpack/lib/Compiler.js#669
又是和上文差不多,在onCompiled前的操作,也就是make钩子执行完成之后,的回调函数
调用了compilation.seal开始处理 chunk
this.hooks.make.callAsync(compilation, err => {
if (err) return callback(err);
compilation.finish(err => {
if (err) return callback(err);
/** 开始处理 chunk */
compilation.seal(err => {
if (err) return callback(err);
this.hooks.afterCompile.callAsync(compilation, err => {
if (err) return callback(err);
return callback(null, compilation);
});
});
});
});
2.10 生成代码内容
webpack/lib/Compilation.js#1398 开始了生成代码内容 + 一堆的 缓存判断,内容比较多,但是可以注意有注释的部分
if (this.hooks.shouldGenerateChunkAssets.call() !== false) {
this.hooks.beforeChunkAssets.call();
/** 生成代码内容*/
this.createChunkAssets();
}
createChunkAssets() {
const outputOptions = this.outputOptions;
const cachedSourceMap = new Map();
/** @type {Map<string, {hash: string, source: Source, chunk: Chunk}>} */
const alreadyWrittenFiles = new Map();
for (let i = 0; i < this.chunks.length; i++) {
const chunk = this.chunks[i];
chunk.files = [];
let source;
let file;
let filenameTemplate;
try {
const template = chunk.hasRuntime()
? this.mainTemplate
: this.chunkTemplate;
const manifest = template.getRenderManifest({
chunk,
hash: this.hash,
fullHash: this.fullHash,
outputOptions,
moduleTemplates: this.moduleTemplates,
dependencyTemplates: this.dependencyTemplates
}); // [{ render(), filenameTemplate, pathOptions, identifier, hash }]
for (const fileManifest of manifest) {
const cacheName = fileManifest.identifier;
const usedHash = fileManifest.hash;
filenameTemplate = fileManifest.filenameTemplate;
const pathAndInfo = this.getPathWithInfo(
filenameTemplate,
fileManifest.pathOptions
);
file = pathAndInfo.path;
const assetInfo = pathAndInfo.info;
// check if the same filename was already written by another chunk
const alreadyWritten = alreadyWrittenFiles.get(file);
...
if (
this.cache &&
this.cache[cacheName] &&
this.cache[cacheName].hash === usedHash
) {
source = this.cache[cacheName].source;
} else {
/** 实际的文件内容 */
source = fileManifest.render();
// Ensure that source is a cached source to avoid additional cost because of repeated access
if (!(source instanceof CachedSource)) {
const cacheEntry = cachedSourceMap.get(source);
if (cacheEntry) {
source = cacheEntry;
} else {
const cachedSource = new CachedSource(source);
cachedSourceMap.set(source, cachedSource);
source = cachedSource;
}
}
if (this.cache) {
this.cache[cacheName] = {
hash: usedHash,
source
};
}
}
/** 缓存要输出的文件 */
this.emitAsset(file, source, assetInfo);
chunk.files.push(file);
this.hooks.chunkAsset.call(chunk, file);
alreadyWrittenFiles.set(file, {
hash: usedHash,
source,
chunk
});
}
}
...
}
}
2.11 写入代码内容
webpack/lib/Compiler.js#353
看了前面那么多准备,终于可以写文件了
emitAssets(compilation, callback) {
let outputPath;
const emitFiles = err => {
if (err) return callback(err);
asyncLib.forEachLimit(
compilation.getAssets(),
15,
({ name: file, source }, callback) => {
let targetFile = file;
const queryStringIdx = targetFile.indexOf("?");
if (queryStringIdx >= 0) {
targetFile = targetFile.substr(0, queryStringIdx);
}
const writeOut = err => {
const targetPath = this.outputFileSystem.join(
outputPath,
targetFile
);
// TODO webpack 5 remove futureEmitAssets option and make it on by default
if (this.options.output.futureEmitAssets) {
...
/** 写入文件 */
this.outputFileSystem.writeFile(targetPath, content, ...);
}
...
};
if (targetFile.match(/\/|\\/)) {
const dir = path.dirname(targetFile);
this.outputFileSystem.mkdirp(
this.outputFileSystem.join(outputPath, dir),
writeOut
);
} else {
writeOut();
}
},
。。。
);
};
/** 执行 文件写入,这里也是钩子中 最后的 获取文件内容 的机会 */
this.hooks.emit.callAsync(compilation, err => {
if (err) return callback(err);
outputPath = compilation.getPath(this.outputPath);
this.outputFileSystem.mkdirp(outputPath, emitFiles);
});
}