Implement posting threads (#6049)
* Implement posting a thread Co-authored-by: Dan Abramov <dan.abramov@gmail.com> * Fix native build * Remove dependency on web crypto API * Fix unrelated TS error (wtf) --------- Co-authored-by: Mary <148872143+mary-ext@users.noreply.github.com>
This commit is contained in:
@@ -6,6 +6,32 @@ cfg.resolver.sourceExts = process.env.RN_SRC_EXT
|
||||
? process.env.RN_SRC_EXT.split(',').concat(cfg.resolver.sourceExts)
|
||||
: cfg.resolver.sourceExts
|
||||
|
||||
if (cfg.resolver.resolveRequest) {
|
||||
throw Error('Update this override because it is conflicting now.')
|
||||
}
|
||||
cfg.resolver.resolveRequest = (context, moduleName, platform) => {
|
||||
// HACK: manually resolve a few packages that use `exports` in `package.json`.
|
||||
// A proper solution is to enable `unstable_enablePackageExports` but this needs careful testing.
|
||||
if (moduleName.startsWith('multiformats/hashes/hasher')) {
|
||||
return context.resolveRequest(
|
||||
context,
|
||||
'multiformats/dist/src/hashes/hasher',
|
||||
platform,
|
||||
)
|
||||
}
|
||||
if (moduleName.startsWith('multiformats/cid')) {
|
||||
return context.resolveRequest(
|
||||
context,
|
||||
'multiformats/dist/src/cid',
|
||||
platform,
|
||||
)
|
||||
}
|
||||
if (moduleName === '@ipld/dag-cbor') {
|
||||
return context.resolveRequest(context, '@ipld/dag-cbor/src', platform)
|
||||
}
|
||||
return context.resolveRequest(context, moduleName, platform)
|
||||
}
|
||||
|
||||
cfg.transformer.getTransformOptions = async () => ({
|
||||
transform: {
|
||||
experimentalImportSupport: true,
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||
"@fortawesome/react-native-fontawesome": "^0.3.2",
|
||||
"@haileyok/bluesky-video": "0.2.3",
|
||||
"@ipld/dag-cbor": "^9.2.0",
|
||||
"@lingui/react": "^4.5.0",
|
||||
"@mattermost/react-native-paste-input": "^0.7.1",
|
||||
"@miblanchard/react-native-slider": "^2.3.1",
|
||||
@@ -158,6 +159,7 @@
|
||||
"lodash.set": "^4.3.2",
|
||||
"lodash.shuffle": "^4.2.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"multiformats": "^13.1.0",
|
||||
"nanoid": "^5.0.5",
|
||||
"normalize-url": "^8.0.0",
|
||||
"patch-package": "^6.5.1",
|
||||
|
||||
@@ -128,7 +128,9 @@ async function loggedOutFetch({
|
||||
headers: {'Accept-Language': contentLangs, ...labelersHeader},
|
||||
},
|
||||
)
|
||||
let data = res.ok ? jsonStringToLex(await res.text()) : null
|
||||
let data = res.ok
|
||||
? (jsonStringToLex(await res.text()) as GetCustomFeed.OutputSchema)
|
||||
: null
|
||||
if (data?.feed?.length) {
|
||||
return {
|
||||
success: true,
|
||||
@@ -143,7 +145,9 @@ async function loggedOutFetch({
|
||||
}&limit=${limit}`,
|
||||
{method: 'GET', headers: {'Accept-Language': '', ...labelersHeader}},
|
||||
)
|
||||
data = res.ok ? jsonStringToLex(await res.text()) : null
|
||||
data = res.ok
|
||||
? (jsonStringToLex(await res.text()) as GetCustomFeed.OutputSchema)
|
||||
: null
|
||||
if (data?.feed?.length) {
|
||||
return {
|
||||
success: true,
|
||||
|
||||
+162
-65
@@ -5,7 +5,6 @@ import {
|
||||
AppBskyEmbedRecordWithMedia,
|
||||
AppBskyEmbedVideo,
|
||||
AppBskyFeedPost,
|
||||
AppBskyFeedPostgate,
|
||||
AtUri,
|
||||
BlobRef,
|
||||
BskyAgent,
|
||||
@@ -15,8 +14,12 @@ import {
|
||||
RichText,
|
||||
} from '@atproto/api'
|
||||
import {TID} from '@atproto/common-web'
|
||||
import * as dcbor from '@ipld/dag-cbor'
|
||||
import {t} from '@lingui/macro'
|
||||
import {QueryClient} from '@tanstack/react-query'
|
||||
import {sha256} from 'js-sha256'
|
||||
import {CID} from 'multiformats/cid'
|
||||
import * as Hasher from 'multiformats/hashes/hasher'
|
||||
|
||||
import {isNetworkError} from '#/lib/strings/errors'
|
||||
import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
|
||||
@@ -53,30 +56,15 @@ export async function post(
|
||||
opts: PostOpts,
|
||||
) {
|
||||
const thread = opts.thread
|
||||
const draft = thread.posts[0] // TODO: Support threads.
|
||||
|
||||
opts.onStateChange?.(t`Processing...`)
|
||||
// NB -- Do not await anything here to avoid waterfalls!
|
||||
// Instead, store Promises which will be unwrapped as they're needed.
|
||||
const rtPromise = resolveRT(agent, draft.richtext)
|
||||
const embedPromise = resolveEmbed(
|
||||
agent,
|
||||
queryClient,
|
||||
draft,
|
||||
opts.onStateChange,
|
||||
)
|
||||
let replyPromise
|
||||
if (opts.replyTo) {
|
||||
replyPromise = resolveReply(agent, opts.replyTo)
|
||||
}
|
||||
|
||||
// set labels
|
||||
let labels: ComAtprotoLabelDefs.SelfLabels | undefined
|
||||
if (draft.labels.length) {
|
||||
labels = {
|
||||
$type: 'com.atproto.label.defs#selfLabels',
|
||||
values: draft.labels.map(val => ({val})),
|
||||
}
|
||||
let replyPromise:
|
||||
| Promise<AppBskyFeedPost.Record['reply']>
|
||||
| AppBskyFeedPost.Record['reply']
|
||||
| undefined
|
||||
if (opts.replyTo) {
|
||||
// Not awaited to avoid waterfalls.
|
||||
replyPromise = resolveReply(agent, opts.replyTo)
|
||||
}
|
||||
|
||||
// add top 3 languages from user preferences if langs is provided
|
||||
@@ -85,20 +73,48 @@ export async function post(
|
||||
langs = opts.langs.slice(0, 3)
|
||||
}
|
||||
|
||||
const rkey = TID.nextStr()
|
||||
const uri = `at://${agent.assertDid}/app.bsky.feed.post/${rkey}`
|
||||
const date = new Date().toISOString()
|
||||
|
||||
const did = agent.assertDid
|
||||
const writes: ComAtprotoRepoApplyWrites.Create[] = []
|
||||
const uris: string[] = []
|
||||
|
||||
let now = new Date()
|
||||
let tid: TID | undefined
|
||||
|
||||
for (let i = 0; i < thread.posts.length; i++) {
|
||||
const draft = thread.posts[i]
|
||||
|
||||
// Not awaited to avoid waterfalls.
|
||||
const rtPromise = resolveRT(agent, draft.richtext)
|
||||
const embedPromise = resolveEmbed(
|
||||
agent,
|
||||
queryClient,
|
||||
draft,
|
||||
opts.onStateChange,
|
||||
)
|
||||
let labels: ComAtprotoLabelDefs.SelfLabels | undefined
|
||||
if (draft.labels.length) {
|
||||
labels = {
|
||||
$type: 'com.atproto.label.defs#selfLabels',
|
||||
values: draft.labels.map(val => ({val})),
|
||||
}
|
||||
}
|
||||
|
||||
// The sorting behavior for multiple posts sharing the same createdAt time is
|
||||
// undefined, so what we'll do here is increment the time by 1 for every post
|
||||
now.setMilliseconds(now.getMilliseconds() + 1)
|
||||
tid = TID.next(tid)
|
||||
const rkey = tid.toString()
|
||||
const uri = `at://${did}/app.bsky.feed.post/${rkey}`
|
||||
uris.push(uri)
|
||||
|
||||
// Create post record
|
||||
{
|
||||
const rt = await rtPromise
|
||||
const embed = await embedPromise
|
||||
const reply = await replyPromise
|
||||
const record: AppBskyFeedPost.Record = {
|
||||
// IMPORTANT: $type has to exist, CID is calculated with the `$type` field
|
||||
// present and will produce the wrong CID if you omit it.
|
||||
$type: 'app.bsky.feed.post',
|
||||
createdAt: date,
|
||||
createdAt: now.toISOString(),
|
||||
text: rt.text,
|
||||
facets: rt.facets,
|
||||
reply,
|
||||
@@ -106,49 +122,52 @@ export async function post(
|
||||
langs,
|
||||
labels,
|
||||
}
|
||||
|
||||
writes.push({
|
||||
$type: 'com.atproto.repo.applyWrites#create',
|
||||
collection: 'app.bsky.feed.post',
|
||||
rkey: rkey,
|
||||
value: record,
|
||||
})
|
||||
}
|
||||
|
||||
// Create threadgate record
|
||||
if (thread.threadgate.some(tg => tg.type !== 'everybody')) {
|
||||
const record = createThreadgateRecord({
|
||||
createdAt: date,
|
||||
post: uri,
|
||||
allow: threadgateAllowUISettingToAllowRecordValue(thread.threadgate),
|
||||
})
|
||||
|
||||
writes.push({
|
||||
$type: 'com.atproto.repo.applyWrites#create',
|
||||
collection: 'app.bsky.feed.threadgate',
|
||||
rkey: rkey,
|
||||
value: record,
|
||||
})
|
||||
}
|
||||
|
||||
// Create postgate record
|
||||
if (
|
||||
thread.postgate.embeddingRules?.length ||
|
||||
thread.postgate.detachedEmbeddingUris?.length
|
||||
) {
|
||||
const record: AppBskyFeedPostgate.Record = {
|
||||
...thread.postgate,
|
||||
$type: 'app.bsky.feed.postgate',
|
||||
createdAt: date,
|
||||
post: uri,
|
||||
if (i === 0 && thread.threadgate.some(tg => tg.type !== 'everybody')) {
|
||||
writes.push({
|
||||
$type: 'com.atproto.repo.applyWrites#create',
|
||||
collection: 'app.bsky.feed.threadgate',
|
||||
rkey: rkey,
|
||||
value: createThreadgateRecord({
|
||||
createdAt: now.toISOString(),
|
||||
post: uri,
|
||||
allow: threadgateAllowUISettingToAllowRecordValue(thread.threadgate),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
writes.push({
|
||||
$type: 'com.atproto.repo.applyWrites#create',
|
||||
collection: 'app.bsky.feed.postgate',
|
||||
rkey: rkey,
|
||||
value: record,
|
||||
})
|
||||
if (
|
||||
thread.postgate.embeddingRules?.length ||
|
||||
thread.postgate.detachedEmbeddingUris?.length
|
||||
) {
|
||||
writes.push({
|
||||
$type: 'com.atproto.repo.applyWrites#create',
|
||||
collection: 'app.bsky.feed.postgate',
|
||||
rkey: rkey,
|
||||
value: {
|
||||
...thread.postgate,
|
||||
$type: 'app.bsky.feed.postgate',
|
||||
createdAt: now.toISOString(),
|
||||
post: uri,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Prepare a ref to the current post for the next post in the thread.
|
||||
const ref = {
|
||||
cid: await computeCid(record),
|
||||
uri,
|
||||
}
|
||||
replyPromise = {
|
||||
root: reply?.root ?? ref,
|
||||
parent: ref,
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -170,7 +189,7 @@ export async function post(
|
||||
}
|
||||
}
|
||||
|
||||
return {uri}
|
||||
return {uris}
|
||||
}
|
||||
|
||||
async function resolveRT(agent: BskyAgent, richtext: RichText) {
|
||||
@@ -382,3 +401,81 @@ async function resolveRecord(
|
||||
}
|
||||
return resolvedLink.record
|
||||
}
|
||||
|
||||
// The built-in hashing functions from multiformats (`multiformats/hashes/sha2`)
|
||||
// are meant for Node.js, this is the cross-platform equivalent.
|
||||
const mf_sha256 = Hasher.from({
|
||||
name: 'sha2-256',
|
||||
code: 0x12,
|
||||
encode: input => {
|
||||
const digest = sha256.arrayBuffer(input)
|
||||
return new Uint8Array(digest)
|
||||
},
|
||||
})
|
||||
|
||||
async function computeCid(record: AppBskyFeedPost.Record): Promise<string> {
|
||||
// IMPORTANT: `prepareObject` prepares the record to be hashed by removing
|
||||
// fields with undefined value, and converting BlobRef instances to the
|
||||
// right IPLD representation.
|
||||
const prepared = prepareForHashing(record)
|
||||
// 1. Encode the record into DAG-CBOR format
|
||||
const encoded = dcbor.encode(prepared)
|
||||
// 2. Hash the record in SHA-256 (code 0x12)
|
||||
const digest = await mf_sha256.digest(encoded)
|
||||
// 3. Create a CIDv1, specifying DAG-CBOR as content (code 0x71)
|
||||
const cid = CID.createV1(0x71, digest)
|
||||
// 4. Get the Base32 representation of the CID (`b` prefix)
|
||||
return cid.toString()
|
||||
}
|
||||
|
||||
// Returns a transformed version of the object for use in DAG-CBOR.
|
||||
function prepareForHashing(v: any): any {
|
||||
// IMPORTANT: BlobRef#ipld() returns the correct object we need for hashing,
|
||||
// the API client will convert this for you but we're hashing in the client,
|
||||
// so we need it *now*.
|
||||
if (v instanceof BlobRef) {
|
||||
return v.ipld()
|
||||
}
|
||||
|
||||
// Walk through arrays
|
||||
if (Array.isArray(v)) {
|
||||
let pure = true
|
||||
const mapped = v.map(value => {
|
||||
if (value !== (value = prepareForHashing(value))) {
|
||||
pure = false
|
||||
}
|
||||
return value
|
||||
})
|
||||
return pure ? v : mapped
|
||||
}
|
||||
|
||||
// Walk through plain objects
|
||||
if (isPlainObject(v)) {
|
||||
const obj: any = {}
|
||||
let pure = true
|
||||
for (const key in v) {
|
||||
let value = v[key]
|
||||
// `value` is undefined
|
||||
if (value === undefined) {
|
||||
pure = false
|
||||
continue
|
||||
}
|
||||
// `prepareObject` returned a value that's different from what we had before
|
||||
if (value !== (value = prepareForHashing(value))) {
|
||||
pure = false
|
||||
}
|
||||
obj[key] = value
|
||||
}
|
||||
// Return as is if we haven't needed to tamper with anything
|
||||
return pure ? v : obj
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
function isPlainObject(v: any): boolean {
|
||||
if (typeof v !== 'object' || v === null) {
|
||||
return false
|
||||
}
|
||||
const proto = Object.getPrototypeOf(v)
|
||||
return proto === Object.prototype || proto === null
|
||||
}
|
||||
|
||||
@@ -366,7 +366,7 @@ export const ComposePost = ({
|
||||
onStateChange: setPublishingStage,
|
||||
langs: toPostLanguages(langPrefs.postLanguage),
|
||||
})
|
||||
).uri
|
||||
).uris[0]
|
||||
try {
|
||||
await whenAppViewReady(agent, postUri, res => {
|
||||
const postedThread = res.data.thread
|
||||
|
||||
@@ -4209,6 +4209,14 @@
|
||||
cborg "^1.6.0"
|
||||
multiformats "^9.5.4"
|
||||
|
||||
"@ipld/dag-cbor@^9.2.0":
|
||||
version "9.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@ipld/dag-cbor/-/dag-cbor-9.2.1.tgz#e61f413770bb0fb27ffafa9577049869272d2056"
|
||||
integrity sha512-nyY48yE7r3dnJVlxrdaimrbloh4RokQaNRdI//btfTkcTEZbpmSrbYcBQ4VKTf8ZxXAOUJy4VsRpkJo+y9RTnA==
|
||||
dependencies:
|
||||
cborg "^4.0.0"
|
||||
multiformats "^13.1.0"
|
||||
|
||||
"@isaacs/cliui@^8.0.2":
|
||||
version "8.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
|
||||
@@ -9024,6 +9032,11 @@ cborg@^1.6.0:
|
||||
resolved "https://registry.yarnpkg.com/cborg/-/cborg-1.10.2.tgz#83cd581b55b3574c816f82696307c7512db759a1"
|
||||
integrity sha512-b3tFPA9pUr2zCUiCfRd2+wok2/LBSNUMKOuRRok+WlvvAgEt/PlbgPTsZUcwCOs53IJvLgTp0eotwtosE6njug==
|
||||
|
||||
cborg@^4.0.0:
|
||||
version "4.2.6"
|
||||
resolved "https://registry.yarnpkg.com/cborg/-/cborg-4.2.6.tgz#7491c29986a87c647d6e2c232e64c82214ca660e"
|
||||
integrity sha512-77vo4KlSwfjCIXcyZUVei4l2gdjesSCeYSx4U/Upwix7pcWZq8uw21sVRpjwn7mjEi//ieJPTj1MRWDHmud1Rg==
|
||||
|
||||
chalk@5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385"
|
||||
@@ -15817,6 +15830,11 @@ multicast-dns@^7.2.5:
|
||||
dns-packet "^5.2.2"
|
||||
thunky "^1.0.2"
|
||||
|
||||
multiformats@^13.1.0:
|
||||
version "13.3.0"
|
||||
resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-13.3.0.tgz#1f5188bc7c4fe08ff829ae1c18dc33409042fb71"
|
||||
integrity sha512-CBiqvsufgmpo01VT5ze94O+uc+Pbf6f/sThlvWss0sBZmAOu6GQn5usrYV2sf2mr17FWYc0rO8c/CNe2T90QAA==
|
||||
|
||||
multiformats@^9.4.2, multiformats@^9.5.4, multiformats@^9.6.4, multiformats@^9.9.0:
|
||||
version "9.9.0"
|
||||
resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37"
|
||||
@@ -19645,7 +19663,16 @@ string-natural-compare@^3.0.1:
|
||||
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
||||
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -19754,7 +19781,7 @@ stringify-object@^3.3.0:
|
||||
is-obj "^1.0.1"
|
||||
is-regexp "^1.0.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@@ -19768,6 +19795,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.2.0:
|
||||
dependencies:
|
||||
ansi-regex "^4.1.0"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^7.0.1:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
|
||||
@@ -21483,7 +21517,7 @@ workbox-window@6.6.1:
|
||||
"@types/trusted-types" "^2.0.2"
|
||||
workbox-core "6.6.1"
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
@@ -21501,6 +21535,15 @@ wrap-ansi@^6.2.0:
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||
|
||||
Reference in New Issue
Block a user