blog icon indicating copy to clipboard operation
blog copied to clipboard

pnpm 问题记录

Open xiaoxiaojx opened this issue 2 years ago • 22 comments

image

背景

2022 前端技术领域会有哪些新的变化? 话题中我曾回答到,越来越多的项目会开始使用 pnpm。

这是我正在推动的一件事,使用 pnpm 替换现在的 yarn 。无论是 csr 、ssr、monorepos 等类型项目都正在进行中,有近 10个项目已经迁移完成。 当时 yarn 的 pnp 特性出来的时候,观望过一阵子,没有大面积火起来,遂放弃 ... 现在是注意到 vite、modernjs 等使用了 pnpm,其设计理念与node_modules的目录结构也能让业务更加快速安全,所以决定开始全面使用 pnpm。

下面记录与分享一下最近使用 pnpm 遇到的问题与解决的过程~

✅ 已解决的问题

jest 单测运行失败

  • 问题描叙: 使用 pnpm 后,原有的 jest 单测失败了
  • 问题解决: jest 低版本不支持软链接, 升级 jest 大于等于 25.2.0 版本即可
  • 报错信息: ca958dffa2ef75ad06f068262b471a7ea952fc25
  • 问题分析:
    1. 根据报错的堆栈找到报错的包,发现其 packge.json dependencies 字段明确声明了依赖的 @xxx/fetch 的版本。但是从报错的信息看,实际运行测试时 import 的却是错误的版本了!
    2. 这里就需要思考一下 jest 是如何运行一个单测用例。如果是简单的 node xxx.test.js 运行一个单测那就不会有上面引用到错误版本依赖的问题,因为按照 node require 模块的规则是不会解析出错。
    3. 让我们回头看一个简单的 jest 单测用例,可以大胆推测一下每个 describe 或者是 it 语句就是一个单独的沙盒环境。如果简单的运行 node xxx.test.js 那就只会存在一个沙盒环境,所有的测试用例会共用一个上下文,这样明显不利于 jest 每个单测隔离的原则
    4. 所以我们初步判断 jest 会自己创建若干个沙盒环境去运行对应的测试代码。而用户的 src 待测试的代码在 jest 看来会通过 fs.readFileSync 去获取到内容字符串,然后不同类型文件经过 babelTransform 或者 tsTransform 得到 js 代码,最后通过 vm 或者 eval, new Function 这样去运行。
    5. 所以说代码中的 import require 等语句的路径是 jest 去静态分析补充完整的,低版本的 jest resolve 不支持软链接是完全有可能的,所以我们顺着 jest 的发版日志,找到最近支持软链接的版本问题解决。
    6. 同理 nextjs 等项目如果有问题也需要找到最近支持软链接的版本进行升级
// Copyright 2004-present Facebook. All Rights Reserved.

'use strict';

jest.useFakeTimers();

describe('timerGame', () => {
  beforeEach(() => {
    jest.spyOn(global, 'setTimeout');
  });
  it('waits 1 second before ending the game', () => {
    const timerGame = require('../timerGame');
    timerGame();

    expect(setTimeout).toBeCalledTimes(1);
    expect(setTimeout).toBeCalledWith(expect.any(Function), 1000);
  });

  it('calls the callback after 1 second via runAllTimers', () => {
    const timerGame = require('../timerGame');
    const callback = jest.fn();

    timerGame(callback);

    // At this point in time, the callback should not have been called yet
    expect(callback).not.toBeCalled();

    // Fast-forward until all timers have been executed
    jest.runAllTimers();

    // Now our callback should have been called!
    expect(callback).toBeCalled();
    expect(callback).toBeCalledTimes(1);
  });
});

node-gyp rebuild failures

  • 问题描叙: 使用 pnpm 后,项目中依赖的 Node.js C++ 插件 rebuild 失败
  • 问题解决: pnpm 低版本 bug,升级 pnpm 大于等于 6.23.1 版本即可,相关 issue issues/2135
  • 报错信息:
# pnpm i better-sqlite3
Packages: +11
+++++++++++
Resolving: total 11, reused 11, downloaded 0, done
node_modules/.pnpm/registry.npmjs.org/integer/2.1.0/node_modules/integer: Running install script, done in 2s
node_modules/.pnpm/registry.npmjs.org/better-sqlite3/5.4.3/node_modules/better-sqlite3: Running install script, failed in 393ms
.../5.4.3/node_modules/better-sqlite3 install$ node-gyp rebuild
│ gyp info it worked if it ends with ok
│ gyp info using [email protected]
│ gyp info using [email protected] | linux | x64
│ gyp info find Python using Python version 3.6.8 found at "/usr/bin/python3"
│ gyp info spawn /usr/bin/python3
│ gyp info spawn args [
│ gyp info spawn args   '/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp/gyp/gyp_main.py',
│ gyp info spawn args   'binding.gyp',
│ gyp info spawn args   '-f',
│ gyp info spawn args   'make',
│ gyp info spawn args   '-I',
│ gyp info spawn args   '/root/2/node_modules/.pnpm/registry.npmjs.org/better-sqlite3/5.4.3/node_modules/better-sqlite3/build/config.gypi',
│ gyp info spawn args   '-I',
│ gyp info spawn args   '/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp/addon.gypi',
│ gyp info spawn args   '-I',
│ gyp info spawn args   '/root/.cache/node-gyp/12.13.0/include/node/common.gypi',
│ gyp info spawn args   '-Dlibrary=shared_library',
│ gyp info spawn args   '-Dvisibility=default',
│ gyp info spawn args   '-Dnode_root_dir=/root/.cache/node-gyp/12.13.0',
│ gyp info spawn args   '-Dnode_gyp_dir=/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp',
│ gyp info spawn args   '-Dnode_lib_file=/root/.cache/node-gyp/12.13.0/<(target_arch)/node.lib',
│ gyp info spawn args   '-Dmodule_root_dir=/root/2/node_modules/.pnpm/registry.npmjs.org/better-sqlite3/5.4.3/node_modules/better-sqlite3',
│ gyp info spawn args   '-Dnode_engine=v8',
│ gyp info spawn args   '--depth=.',
│ gyp info spawn args   '--no-parallel',
│ gyp info spawn args   '--generator-output',
│ gyp info spawn args   'build',
│ gyp info spawn args   '-Goutput_dir=.'
│ gyp info spawn args ]
│ Traceback (most recent call last):
│   File "/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp/gyp/gyp_main.py", line 50, in <module>
│     sys.exit(gyp.script_main())
│   File "/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp/gyp/pylib/gyp/__init__.py", line 554, in script_main
│     return main(sys.argv[1:])
│   File "/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp/gyp/pylib/gyp/__init__.py", line 547, in main
│     return gyp_main(args)
│   File "/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp/gyp/pylib/gyp/__init__.py", line 532, in gyp_main
│     generator.GenerateOutput(flat_list, targets, data, params)
│   File "/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp/gyp/pylib/gyp/generator/make.py", line 2215, in GenerateOutput
│     part_of_all=qualified_target in needed_targets)
│   File "/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp/gyp/pylib/gyp/generator/make.py", line 794, in Write
│     extra_mac_bundle_resources, part_of_all)
│   File "/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp/gyp/pylib/gyp/generator/make.py", line 978, in WriteActions
│     part_of_all=part_of_all, command=name)
│   File "/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp/gyp/pylib/gyp/generator/make.py", line 1724, in WriteDoCmd
│     force = True)
│   File "/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp/gyp/pylib/gyp/generator/make.py", line 1779, in WriteMakeRule
│     cmddigest = hashlib.sha1(command if command else self.target).hexdigest()
│ TypeError: Unicode-objects must be encoded before hashing
│ gyp ERR! configure error
│ gyp ERR! stack Error: `gyp` failed with exit code: 1
│ gyp ERR! stack     at ChildProcess.onCpExit (/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp/lib/configure.js:351:16)
│ gyp ERR! stack     at ChildProcess.emit (events.js:210:5)
│ gyp ERR! stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:272:12)
│ gyp ERR! System Linux 4.15.0-33-generic
│ gyp ERR! command "/usr/bin/node" "/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp/bin/node-gyp.js" "rebuild"
│ gyp ERR! cwd /root/2/node_modules/.pnpm/registry.npmjs.org/better-sqlite3/5.4.3/node_modules/better-sqlite3
│ gyp ERR! node -v v12.13.0
│ gyp ERR! node-gyp -v v6.0.0
│ gyp ERR! not ok
└─ Failed in 393ms
 ERROR  Command failed with exit code 1.
  • 问题分析: 会不会是 c++ 版本的问题?而得到的信息为该镜像使用 yarn 却是好的! 最终换了同一个 Node.js 版本的镜像又好了,辗转反侧才在 pnpm 的 issue 中找到真正的原因,为 pnpm 低版本的 bug。

同一个版本的包有两份副本

  • 问题描述: 同一个版本的包在 .pnpm 目录下却有两份副本
  • 问题解决: 添加 .pnpmfile.cjs 文件,忽略 peerDependencies,使其对 peer 的处理与 yarn 保持一致
// .pnpmfile.cjs

function readPackage(pkg, context) {
  if (pkg.name && pkg.peerDependencies) {
    // https://pnpm.io/zh/how-peers-are-resolved
    pkg.peerDependencies = {}
  }

  return pkg
}

module.exports = {
  hooks: {
    readPackage,
  },
}

only-allow 命令影响到了业务项目

  • 问题描叙: 当你在一个公共包的项目中添加了 preinstall 的勾子,但是实际依赖该包的业务并未使用 pnpm,造成报错
  • 问题解决: only-allow 当作为依赖时不应该进行检查,暂时使用支持了该功能的 only-allow-test 包代替。对应的讨论见 : discussions/4131
{
    "scripts": {
        "preinstall": "npx only-allow pnpm"
    }
}

❌ 未解决的问题

pnpm add 与 pnpm i 命令不会去重

  • 问题描叙: 当使用 pnpm add 或者 pnpm i 升级某个包时,会存在某几个版本兼容的包没有进行合并,导致存在多个版本。如 sass: ^1.30.0 和 sass: '^1.44.0'没有被合并,但是使用 pnpm update 去升级这个包是会进行合并
  • 问题分析: 由于使用 yarn 的习惯不小心就会发生这种情况,所以希望支持 pnpm deduplicate 去重的命令,每次构建前强制运行一次,反馈后作者表示 pnpm add 命令会保持和 update 命令同样的行为。对应的讨论见 discussions/4143
  • 临时解决: 找到了一个 pnpm update / install / add 命令后都会运行的一个勾子,在其中写了一个自定义的校验函数,如果pkgName 存在 dependencies 或者 devDependencies 中,就只能使用 pnpm update 命令去更新升级
// .pnpmfile.cjs

const argv = process.argv.slice(2)

function readPackage(pkg, context) {
  // Override the manifest of [email protected] after downloading it from the registry
  if (pkg.name && pkg.peerDependencies) {
    // Replace [email protected] with [email protected]
    pkg.peerDependencies = {}
  }

  return pkg
}

function checkCommand() {
  const command = argv[0]
  const pkgJson = require('./package.json')
  const deps = Object.assign({}, pkgJson.dependencies || {}, pkgJson.devDependencies || {})

  if (['add', 'i', 'install'].some((name) => command === name) && typeof argv[1] === 'string') {
    const { name, version } = getNameAndVersion(argv[1])

    if (deps[name]) {
      throw new Error(
        `【Inspector 依赖检查】: 更新升级依赖请用 "pnpm update ${name}${
          version ? '@' + version : ''
        }" 命令 !!!`
      )
    }
  }
}

function getNameAndVersion(nameAndVersion) {
  let name = ''
  let version = ''
  let splitName = nameAndVersion.split('@')

  if (nameAndVersion.startsWith('@')) {
    // @xxxx/pkg@latest or @xxxx/pkg

    if (splitName.length === 3) {
      // @xxxx/pkg@latest
      name = `@` + splitName[1]
      version = splitName[splitName.length - 1]
    } else {
      // @xxxx/pkg
      name = `@` + splitName[1]
      version = ''
    }
  } else {
    // react@latest or react
    if (splitName.length === 2) {
      // @xxxx/pkg@latest
      name = splitName[0]
      version = splitName[1]
    } else {
      // @xxxx/pkg
      name = splitName[0]
      version = ''
    }
  }
  return {
    name,
    version: version || 'latest',
  }
}

module.exports = {
  hooks: {
    readPackage,
    afterAllResolved(lockfile) {
      checkCommand()
      return lockfile
    },
  },
}

cypress e2e 测试运行失败

  • 问题描叙: 使用 pnpm 后,原有的 cypress e2e 测试失败了
  • 问题分析: 经过 debug 发现 cypress 还不支持 pnpm, 于是提了一个 pr,cypress 处理跟进较慢,还未解决

xiaoxiaojx avatar Dec 19 '21 14:12 xiaoxiaojx

竟然看到类似的经历,现在挺好的,大家已经大规模接受软链接,当年 pnpm 出来的时候大家都吐槽为啥是软链接。 不过事情总是非常奇妙,可遇见的 2022 年,回归到 npm 最原始的文件目录格式,又会引发一轮浪潮。

fengmk2 avatar Dec 20 '21 02:12 fengmk2

不过事情总是非常奇妙,可遇见的 2022 年,回归到 npm 最原始的文件目录格式,又会引发一轮浪潮。

评论区惊现大佬,哈哈,竟然还有这么一段吐槽的故事。我注意到软链接还是从 lerna 开始,而 pnpm 的软硬链接其上阵的架构又让人学习了不少 ~

xiaoxiaojx avatar Dec 20 '21 15:12 xiaoxiaojx

竟然看到类似的经历,现在挺好的,大家已经大规模接受软链接,当年 pnpm 出来的时候大家都吐槽为啥是软链接。 不过事情总是非常奇妙,可遇见的 2022 年,回归到 npm 最原始的文件目录格式,又会引发一轮浪潮。

其实 npm 也在做软链模式了 https://github.com/npm/rfcs/pull/436

m1heng avatar Dec 22 '21 12:12 m1heng

竟然看到类似的经历,现在挺好的,大家已经大规模接受软链接,当年 pnpm 出来的时候大家都吐槽为啥是软链接。 不过事情总是非常奇妙,可遇见的 2022 年,回归到 npm 最原始的文件目录格式,又会引发一轮浪潮。

其实 npm 也在做软链模式了 npm/rfcs#436

哈哈,用户已经损失严重了 ~

xiaoxiaojx avatar Dec 22 '21 15:12 xiaoxiaojx

竟然看到类似的经历,现在挺好的,大家已经大规模接受软链接,当年 pnpm 出来的时候大家都吐槽为啥是软链接。 不过事情总是非常奇妙,可遇见的 2022 年,回归到 npm 最原始的文件目录格式,又会引发一轮浪潮。

其实 npm 也在做软链模式了 npm/rfcs#436

哈哈,用户已经损失严重了 ~

就目前来说, pnpm 的确比 npm 好用太多了,npm 正在被广大node.js开发者抛弃

zurmokeeper avatar Dec 23 '21 09:12 zurmokeeper

竟然看到类似的经历,现在挺好的,大家已经大规模接受软链接,当年 pnpm 出来的时候大家都吐槽为啥是软链接。 不过事情总是非常奇妙,可遇见的 2022 年,回归到 npm 最原始的文件目录格式,又会引发一轮浪潮。

其实 npm 也在做软链模式了 npm/rfcs#436

哈哈,用户已经损失严重了 ~

就目前来说, pnpm 的确比 npm 好用太多了,npm 正在被广大node.js开发者抛弃

谁好用,就用谁 🤔

xiaoxiaojx avatar Dec 23 '21 15:12 xiaoxiaojx

2022年应该会出现一个回归普通文件的模式,期待变化。

fengmk2 avatar Jan 01 '22 02:01 fengmk2

2022年应该会出现一个回归普通文件的模式,期待变化。

使用 yarn 的 pnp 需要用它的 resolver 才能从映射表找到正确的资源 使用 pnpm 后通过硬链接达到资源共享,只需支持软链接即可,降低了其他工具支持的成本

一时很难想到更优秀的方案了,期待出现 ~

xiaoxiaojx avatar Jan 04 '22 05:01 xiaoxiaojx

2022年应该会出现一个回归普通文件的模式,期待变化。

使用 yarn 的 pnp 需要用它的 resolver 才能从映射表找到正确的资源 使用 pnpm 后通过硬链接达到资源共享,只需支持软链接即可,降低了其他工具支持的成本

一时很难想到更优秀的方案了,期待出现 ~

明天的 SEEConf https://seeconf.antfin.com/ ,有一个分享会讲到 npm fs 的概念,可以围观。 image

fengmk2 avatar Jan 07 '22 16:01 fengmk2

16: 50 来看尽然就结束了 😂 , 遗憾错过,后面去找下 ppt 学习一下

xiaoxiaojx avatar Jan 08 '22 09:01 xiaoxiaojx

2022年应该会出现一个回归普通文件的模式,期待变化。

使用 yarn 的 pnp 需要用它的 resolver 才能从映射表找到正确的资源 使用 pnpm 后通过硬链接达到资源共享,只需支持软链接即可,降低了其他工具支持的成本 一时很难想到更优秀的方案了,期待出现 ~

明天的 SEEConf https://seeconf.antfin.com/ ,有一个分享会讲到 npm fs 的概念,可以围观。 image

有 PPT 的链接吗? 想看看

m1heng avatar Jan 09 '22 07:01 m1heng

应该就是这篇文章了 深入浅出 tnpm rapid 模式 - 如何比 pnpm 快 10 秒

过于优秀了 ~

xiaoxiaojx avatar Jan 11 '22 11:01 xiaoxiaojx

作者你好,用pnpm workspace搭的monorepo项目,或多或少的遇到依赖冲突的问题,不知道是否遇到过以及是否有相关的思路

详细描述: 假设我有一个app A,依赖了antd: 3.18.0,antd自身又依赖了moment: 2.29.0。 另外有一个app B,依赖了antd: 3.20.0,依赖了moment: 2.30.0

本来预期情况是各依赖各自的版本,岁月静好。 但是会出现app A在跑dev或者打包后,moment依赖会变成用的2.30.0,从而造成一些bug

目前我的解决方法是在app A中显式的装一下[email protected]版本的依赖。

但是没有搞清楚是为什么,以及是否有更好的解决方案。

Stoneleee avatar Oct 13 '22 09:10 Stoneleee

作者你好,用pnpm workspace搭的monorepo项目,或多或少的遇到依赖冲突的问题,不知道是否遇到过以及是否有相关的思路

详细描述: 假设我有一个app A,依赖了antd: 3.18.0,antd自身又依赖了moment: 2.29.0。 另外有一个app B,依赖了antd: 3.20.0,依赖了moment: 2.30.0

本来预期情况是各依赖各自的版本,岁月静好。 但是会出现app A在跑dev或者打包后,moment依赖会变成用的2.30.0,从而造成一些bug

目前我的解决方法是在app A中显式的装一下[email protected]版本的依赖。

但是没有搞清楚是为什么,以及是否有更好的解决方案。

app A 打包后的 antd 肯定是用了正确的 2.29.0 版本的 moment (除非 antd 没有写死版本), 至于 app A 中如果某个依赖用到了 moment, 但没声明版本这些包则可能用到 2.30.0 的 moment 吧

xiaoxiaojx avatar Oct 13 '22 11:10 xiaoxiaojx

作者你好,用pnpm workspace搭的monorepo项目,或多或少的遇到依赖冲突的问题,不知道是否遇到过以及是否有相关的思路 详细描述: 假设我有一个app A,依赖了antd: 3.18.0,antd自身又依赖了moment: 2.29.0。 另外有一个app B,依赖了antd: 3.20.0,依赖了moment: 2.30.0 本来预期情况是各依赖各自的版本,岁月静好。 但是会出现app A在跑dev或者打包后,moment依赖会变成用的2.30.0,从而造成一些bug 目前我的解决方法是在app A中显式的装一下[email protected]版本的依赖。 但是没有搞清楚是为什么,以及是否有更好的解决方案。

app A 打包后的 antd 肯定是用了正确的 2.29.0 版本的 moment (除非 antd 没有写死版本), 至于 app A 中如果某个依赖用到了 moment, 但没声明版本这些包则可能用到 2.30.0 的 moment 吧

上述情况算是举例吧,实际遇到的情况相当于 moment作为依赖的依赖,是在antd中写死了版本号的,所以按道理不应该出现版本依赖到错误版本的问题,但实际上出现了。

Stoneleee avatar Oct 14 '22 03:10 Stoneleee

作者你好,用pnpm workspace搭的monorepo项目,或多或少的遇到依赖冲突的问题,不知道是否遇到过以及是否有相关的思路 详细描述: 假设我有一个app A,依赖了antd: 3.18.0,antd自身又依赖了moment: 2.29.0。 另外有一个app B,依赖了antd: 3.20.0,依赖了moment: 2.30.0 本来预期情况是各依赖各自的版本,岁月静好。 但是会出现app A在跑dev或者打包后,moment依赖会变成用的2.30.0,从而造成一些bug 目前我的解决方法是在app A中显式的装一下[email protected]版本的依赖。 但是没有搞清楚是为什么,以及是否有更好的解决方案。

app A 打包后的 antd 肯定是用了正确的 2.29.0 版本的 moment (除非 antd 没有写死版本), 至于 app A 中如果某个依赖用到了 moment, 但没声明版本这些包则可能用到 2.30.0 的 moment 吧

上述情况算是举例吧,实际遇到的情况相当于 moment作为依赖的依赖,是在antd中写死了版本号的,所以按道理不应该出现版本依赖到错误版本的问题,但实际上出现了。

进入.pnpm的任意模块目录,在当前目录下执行

$ node
$ require.resolve('moment')

返回的结果应该都是两个版本中的一个,可能是这个原因导致了你的项目中antd解析到的moment的也只会是其中一个版本

wwh0219 avatar Oct 26 '22 01:10 wwh0219

@wwh0219 此时应该到 antd 目录下去运行 require.resolve('moment') , baseDir 为当前代码 import 的目录

xiaoxiaojx avatar Oct 26 '22 16:10 xiaoxiaojx

@xiaoxiaojx 但是antd里边引用moment的时候应该是没办法指定badedir的吧,根本原因还是在.pnpm目录下会有个node_modules目录,其中的moment指向了某个版本,所以在.pnpm/antd下require moment会默认引用到.pnpm/node_modules

wwh0219 avatar Oct 28 '22 11:10 wwh0219

pnpm add 与 pnpm i 命令不会去重 这个问题目前依然存在,怎么说呢,不算大问题,还是真遇到了还是挺头疼的,以前yarn用习惯了

Mrcxt avatar Dec 07 '22 13:12 Mrcxt

@Mrcxt 可以用这个库 ocavue/pnpm-deduplicate , 目前看来没什么问题

xiaoxiaojx avatar Dec 07 '22 13:12 xiaoxiaojx

作者你好,用pnpm workspace搭的monorepo项目,或多或少的遇到依赖冲突的问题,不知道是否遇到过以及是否有相关的思路

详细描述: 假设我有一个app A,依赖了antd: 3.18.0,antd自身又依赖了moment: 2.29.0。 另外有一个app B,依赖了antd: 3.20.0,依赖了moment: 2.30.0

本来预期情况是各依赖各自的版本,岁月静好。 但是会出现app A在跑dev或者打包后,moment依赖会变成用的2.30.0,从而造成一些bug

目前我的解决方法是在app A中显式的装一下[email protected]版本的依赖。

但是没有搞清楚是为什么,以及是否有更好的解决方案。

关于这个问题,这篇文章应该是说清楚了😂 https://blog.csdn.net/qq_21567385/article/details/121088506

Stoneleee avatar Feb 05 '23 02:02 Stoneleee

23年8月,pnpm版本:v8.7.0 “pnpm add 与 pnpm i 命令不会去重”,这个问题,现在执行pnpm update依然不会去重,代码引用不会自动变更,感觉是个bug了

Mrcxt avatar Sep 01 '23 03:09 Mrcxt