mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-28 06:33:41 +03:00
fix(Email Trigger (IMAP) Node): Handle attachments correctly (#9410)
This commit is contained in:
parent
bf549301df
commit
68a6c81729
@ -92,7 +92,8 @@
|
|||||||
"pyodide@0.23.4": "patches/pyodide@0.23.4.patch",
|
"pyodide@0.23.4": "patches/pyodide@0.23.4.patch",
|
||||||
"@types/express-serve-static-core@4.17.43": "patches/@types__express-serve-static-core@4.17.43.patch",
|
"@types/express-serve-static-core@4.17.43": "patches/@types__express-serve-static-core@4.17.43.patch",
|
||||||
"@types/ws@8.5.4": "patches/@types__ws@8.5.4.patch",
|
"@types/ws@8.5.4": "patches/@types__ws@8.5.4.patch",
|
||||||
"vite-plugin-checker@0.6.4": "patches/vite-plugin-checker@0.6.4.patch"
|
"vite-plugin-checker@0.6.4": "patches/vite-plugin-checker@0.6.4.patch",
|
||||||
|
"@types/uuencode@0.0.3": "patches/@types__uuencode@0.0.3.patch"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
"lint": "eslint . --quiet",
|
"lint": "eslint . --quiet",
|
||||||
"lintfix": "eslint . --fix",
|
"lintfix": "eslint . --fix",
|
||||||
"watch": "tsc -p tsconfig.build.json --watch",
|
"watch": "tsc -p tsconfig.build.json --watch",
|
||||||
"test": "echo \"Error: no test created yet\""
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "src/index.ts",
|
"module": "src/index.ts",
|
||||||
|
@ -2,13 +2,10 @@
|
|||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import type Imap from 'imap';
|
import type Imap from 'imap';
|
||||||
import { type ImapMessage } from 'imap';
|
import { type ImapMessage } from 'imap';
|
||||||
import * as qp from 'quoted-printable';
|
|
||||||
import * as iconvlite from 'iconv-lite';
|
|
||||||
import * as utf8 from 'utf8';
|
|
||||||
import * as uuencode from 'uuencode';
|
|
||||||
|
|
||||||
import { getMessage } from './helpers/getMessage';
|
import { getMessage } from './helpers/getMessage';
|
||||||
import type { Message, MessagePart } from './types';
|
import type { Message, MessagePart } from './types';
|
||||||
|
import { PartData } from './PartData';
|
||||||
|
|
||||||
const IMAP_EVENTS = ['alert', 'mail', 'expunge', 'uidvalidity', 'update', 'close', 'end'] as const;
|
const IMAP_EVENTS = ['alert', 'mail', 'expunge', 'uidvalidity', 'update', 'close', 'end'] as const;
|
||||||
|
|
||||||
@ -124,7 +121,7 @@ export class ImapSimple extends EventEmitter {
|
|||||||
/** The message part to be downloaded, from the `message.attributes.struct` Array */
|
/** The message part to be downloaded, from the `message.attributes.struct` Array */
|
||||||
part: MessagePart,
|
part: MessagePart,
|
||||||
) {
|
) {
|
||||||
return await new Promise<string>((resolve, reject) => {
|
return await new Promise<PartData>((resolve, reject) => {
|
||||||
const fetch = this.imap.fetch(message.attributes.uid, {
|
const fetch = this.imap.fetch(message.attributes.uid, {
|
||||||
bodies: [part.partID],
|
bodies: [part.partID],
|
||||||
struct: true,
|
struct: true,
|
||||||
@ -138,43 +135,8 @@ export class ImapSimple extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = result.parts[0].body as string;
|
const data = result.parts[0].body as string;
|
||||||
|
|
||||||
const encoding = part.encoding.toUpperCase();
|
const encoding = part.encoding.toUpperCase();
|
||||||
|
resolve(PartData.fromData(data, encoding));
|
||||||
if (encoding === 'BASE64') {
|
|
||||||
resolve(Buffer.from(data, 'base64').toString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (encoding === 'QUOTED-PRINTABLE') {
|
|
||||||
if (part.params?.charset?.toUpperCase() === 'UTF-8') {
|
|
||||||
resolve(Buffer.from(utf8.decode(qp.decode(data))).toString());
|
|
||||||
} else {
|
|
||||||
resolve(Buffer.from(qp.decode(data)).toString());
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (encoding === '7BIT') {
|
|
||||||
resolve(Buffer.from(data).toString('ascii'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (encoding === '8BIT' || encoding === 'BINARY') {
|
|
||||||
const charset = part.params?.charset ?? 'utf-8';
|
|
||||||
resolve(iconvlite.decode(Buffer.from(data), charset));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (encoding === 'UUENCODE') {
|
|
||||||
const parts = data.toString().split('\n'); // remove newline characters
|
|
||||||
const merged = parts.splice(1, parts.length - 4).join(''); // remove excess lines and join lines with empty string
|
|
||||||
resolve(uuencode.decode(merged));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if it gets here, the encoding is not currently supported
|
|
||||||
reject(new Error('Unknown encoding ' + part.encoding));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchOnError = (error: Error) => {
|
const fetchOnError = (error: Error) => {
|
||||||
|
84
packages/@n8n/imap/src/PartData.ts
Normal file
84
packages/@n8n/imap/src/PartData.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||||
|
import * as qp from 'quoted-printable';
|
||||||
|
import * as iconvlite from 'iconv-lite';
|
||||||
|
import * as utf8 from 'utf8';
|
||||||
|
import * as uuencode from 'uuencode';
|
||||||
|
|
||||||
|
export abstract class PartData {
|
||||||
|
constructor(readonly buffer: Buffer) {}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return this.buffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromData(data: string, encoding: string, charset?: string): PartData {
|
||||||
|
if (encoding === 'BASE64') {
|
||||||
|
return new Base64PartData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoding === 'QUOTED-PRINTABLE') {
|
||||||
|
return new QuotedPrintablePartData(data, charset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoding === '7BIT') {
|
||||||
|
return new SevenBitPartData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoding === '8BIT' || encoding === 'BINARY') {
|
||||||
|
return new BinaryPartData(data, charset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoding === 'UUENCODE') {
|
||||||
|
return new UuencodedPartData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it gets here, the encoding is not currently supported
|
||||||
|
throw new Error('Unknown encoding ' + encoding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Base64PartData extends PartData {
|
||||||
|
constructor(data: string) {
|
||||||
|
super(Buffer.from(data, 'base64'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class QuotedPrintablePartData extends PartData {
|
||||||
|
constructor(data: string, charset?: string) {
|
||||||
|
const decoded =
|
||||||
|
charset?.toUpperCase() === 'UTF-8' ? utf8.decode(qp.decode(data)) : qp.decode(data);
|
||||||
|
super(Buffer.from(decoded));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SevenBitPartData extends PartData {
|
||||||
|
constructor(data: string) {
|
||||||
|
super(Buffer.from(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return this.buffer.toString('ascii');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BinaryPartData extends PartData {
|
||||||
|
constructor(
|
||||||
|
data: string,
|
||||||
|
readonly charset: string = 'utf-8',
|
||||||
|
) {
|
||||||
|
super(Buffer.from(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return iconvlite.decode(this.buffer, this.charset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UuencodedPartData extends PartData {
|
||||||
|
constructor(data: string) {
|
||||||
|
const parts = data.split('\n'); // remove newline characters
|
||||||
|
const merged = parts.splice(1, parts.length - 4).join(''); // remove excess lines and join lines with empty string
|
||||||
|
const decoded = uuencode.decode(merged);
|
||||||
|
super(decoded);
|
||||||
|
}
|
||||||
|
}
|
88
packages/@n8n/imap/test/PartData.test.ts
Normal file
88
packages/@n8n/imap/test/PartData.test.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import {
|
||||||
|
PartData,
|
||||||
|
Base64PartData,
|
||||||
|
QuotedPrintablePartData,
|
||||||
|
SevenBitPartData,
|
||||||
|
BinaryPartData,
|
||||||
|
UuencodedPartData,
|
||||||
|
} from '../src/PartData';
|
||||||
|
|
||||||
|
describe('PartData', () => {
|
||||||
|
describe('fromData', () => {
|
||||||
|
it('should return an instance of Base64PartData when encoding is BASE64', () => {
|
||||||
|
const result = PartData.fromData('data', 'BASE64');
|
||||||
|
expect(result).toBeInstanceOf(Base64PartData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an instance of QuotedPrintablePartData when encoding is QUOTED-PRINTABLE', () => {
|
||||||
|
const result = PartData.fromData('data', 'QUOTED-PRINTABLE');
|
||||||
|
expect(result).toBeInstanceOf(QuotedPrintablePartData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an instance of SevenBitPartData when encoding is 7BIT', () => {
|
||||||
|
const result = PartData.fromData('data', '7BIT');
|
||||||
|
expect(result).toBeInstanceOf(SevenBitPartData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an instance of BinaryPartData when encoding is 8BIT or BINARY', () => {
|
||||||
|
let result = PartData.fromData('data', '8BIT');
|
||||||
|
expect(result).toBeInstanceOf(BinaryPartData);
|
||||||
|
result = PartData.fromData('data', 'BINARY');
|
||||||
|
expect(result).toBeInstanceOf(BinaryPartData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an instance of UuencodedPartData when encoding is UUENCODE', () => {
|
||||||
|
const result = PartData.fromData('data', 'UUENCODE');
|
||||||
|
expect(result).toBeInstanceOf(UuencodedPartData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error when encoding is not supported', () => {
|
||||||
|
expect(() => PartData.fromData('data', 'UNSUPPORTED')).toThrow(
|
||||||
|
'Unknown encoding UNSUPPORTED',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Base64PartData', () => {
|
||||||
|
it('should correctly decode base64 data', () => {
|
||||||
|
const data = Buffer.from('Hello, world!', 'utf-8').toString('base64');
|
||||||
|
const partData = new Base64PartData(data);
|
||||||
|
expect(partData.toString()).toBe('Hello, world!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('QuotedPrintablePartData', () => {
|
||||||
|
it('should correctly decode quoted-printable data', () => {
|
||||||
|
const data = '=48=65=6C=6C=6F=2C=20=77=6F=72=6C=64=21'; // 'Hello, world!' in quoted-printable
|
||||||
|
const partData = new QuotedPrintablePartData(data);
|
||||||
|
expect(partData.toString()).toBe('Hello, world!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SevenBitPartData', () => {
|
||||||
|
it('should correctly decode 7bit data', () => {
|
||||||
|
const data = 'Hello, world!';
|
||||||
|
const partData = new SevenBitPartData(data);
|
||||||
|
expect(partData.toString()).toBe('Hello, world!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('BinaryPartData', () => {
|
||||||
|
it('should correctly decode binary data', () => {
|
||||||
|
const data = Buffer.from('Hello, world!', 'utf-8').toString();
|
||||||
|
const partData = new BinaryPartData(data);
|
||||||
|
expect(partData.toString()).toBe('Hello, world!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('UuencodedPartData', () => {
|
||||||
|
it('should correctly decode uuencoded data', () => {
|
||||||
|
const data = Buffer.from(
|
||||||
|
'YmVnaW4gNjQ0IGRhdGEKLTImNUw7JlxMKCc9TzxGUUQoMGBgCmAKZW5kCg==',
|
||||||
|
'base64',
|
||||||
|
).toString('binary');
|
||||||
|
const partData = new UuencodedPartData(data);
|
||||||
|
expect(partData.toString()).toBe('Hello, world!');
|
||||||
|
});
|
||||||
|
});
|
@ -285,7 +285,7 @@ export class EmailReadImapV1 implements INodeType {
|
|||||||
|
|
||||||
// Returns the email text
|
// Returns the email text
|
||||||
|
|
||||||
const getText = async (parts: any[], message: Message, subtype: string) => {
|
const getText = async (parts: any[], message: Message, subtype: string): Promise<string> => {
|
||||||
if (!message.attributes.struct) {
|
if (!message.attributes.struct) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@ -296,12 +296,14 @@ export class EmailReadImapV1 implements INodeType {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (textParts.length === 0) {
|
const part = textParts[0];
|
||||||
|
if (!part) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await connection.getPartData(message, textParts[0]);
|
const partData = await connection.getPartData(message, part);
|
||||||
|
return partData.toString();
|
||||||
} catch {
|
} catch {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@ -330,7 +332,7 @@ export class EmailReadImapV1 implements INodeType {
|
|||||||
.then(async (partData) => {
|
.then(async (partData) => {
|
||||||
// Return it in the format n8n expects
|
// Return it in the format n8n expects
|
||||||
return await this.helpers.prepareBinaryData(
|
return await this.helpers.prepareBinaryData(
|
||||||
Buffer.from(partData),
|
partData.buffer,
|
||||||
attachmentPart.disposition.params.filename as string,
|
attachmentPart.disposition.params.filename as string,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -298,7 +298,11 @@ export class EmailReadImapV2 implements INodeType {
|
|||||||
|
|
||||||
// Returns the email text
|
// Returns the email text
|
||||||
|
|
||||||
const getText = async (parts: MessagePart[], message: Message, subtype: string) => {
|
const getText = async (
|
||||||
|
parts: MessagePart[],
|
||||||
|
message: Message,
|
||||||
|
subtype: string,
|
||||||
|
): Promise<string> => {
|
||||||
if (!message.attributes.struct) {
|
if (!message.attributes.struct) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@ -309,12 +313,14 @@ export class EmailReadImapV2 implements INodeType {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (textParts.length === 0) {
|
const part = textParts[0];
|
||||||
|
if (!part) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await connection.getPartData(message, textParts[0]);
|
const partData = await connection.getPartData(message, part);
|
||||||
|
return partData.toString();
|
||||||
} catch {
|
} catch {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@ -355,7 +361,7 @@ export class EmailReadImapV2 implements INodeType {
|
|||||||
?.filename as string,
|
?.filename as string,
|
||||||
);
|
);
|
||||||
// Return it in the format n8n expects
|
// Return it in the format n8n expects
|
||||||
return await this.helpers.prepareBinaryData(Buffer.from(partData), fileName);
|
return await this.helpers.prepareBinaryData(partData.buffer, fileName);
|
||||||
});
|
});
|
||||||
|
|
||||||
attachmentPromises.push(attachmentPromise);
|
attachmentPromises.push(attachmentPromise);
|
||||||
|
10
patches/@types__uuencode@0.0.3.patch
Normal file
10
patches/@types__uuencode@0.0.3.patch
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
diff --git a/index.d.ts b/index.d.ts
|
||||||
|
index f8f89c567f394a538018bfdf11c28dc15e9c9fdc..f3d1cd426711f1f714744474604bd7e321073983 100644
|
||||||
|
--- a/index.d.ts
|
||||||
|
+++ b/index.d.ts
|
||||||
|
@@ -1,4 +1,4 @@
|
||||||
|
/// <reference types="node"/>
|
||||||
|
|
||||||
|
-export function decode(str: string | Buffer): string;
|
||||||
|
+export function decode(str: string | Buffer): Buffer;
|
||||||
|
export function encode(str: string | Buffer): string;
|
@ -25,6 +25,9 @@ patchedDependencies:
|
|||||||
'@types/express-serve-static-core@4.17.43':
|
'@types/express-serve-static-core@4.17.43':
|
||||||
hash: 5orrj4qleu2iko5t27vl44u4we
|
hash: 5orrj4qleu2iko5t27vl44u4we
|
||||||
path: patches/@types__express-serve-static-core@4.17.43.patch
|
path: patches/@types__express-serve-static-core@4.17.43.patch
|
||||||
|
'@types/uuencode@0.0.3':
|
||||||
|
hash: 3i7wecddkama6vhpu5o37g24u4
|
||||||
|
path: patches/@types__uuencode@0.0.3.patch
|
||||||
'@types/ws@8.5.4':
|
'@types/ws@8.5.4':
|
||||||
hash: nbzuqaoyqbrfwipijj5qriqqju
|
hash: nbzuqaoyqbrfwipijj5qriqqju
|
||||||
path: patches/@types__ws@8.5.4.patch
|
path: patches/@types__ws@8.5.4.patch
|
||||||
@ -227,7 +230,7 @@ importers:
|
|||||||
version: 3.0.3
|
version: 3.0.3
|
||||||
'@types/uuencode':
|
'@types/uuencode':
|
||||||
specifier: ^0.0.3
|
specifier: ^0.0.3
|
||||||
version: 0.0.3
|
version: 0.0.3(patch_hash=3i7wecddkama6vhpu5o37g24u4)
|
||||||
|
|
||||||
packages/@n8n/nodes-langchain:
|
packages/@n8n/nodes-langchain:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -9258,7 +9261,7 @@ packages:
|
|||||||
ts-dedent: 2.2.0
|
ts-dedent: 2.2.0
|
||||||
type-fest: 2.19.0
|
type-fest: 2.19.0
|
||||||
vue: 3.4.21(typescript@5.4.2)
|
vue: 3.4.21(typescript@5.4.2)
|
||||||
vue-component-type-helpers: 2.0.17
|
vue-component-type-helpers: 2.0.18
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
- supports-color
|
- supports-color
|
||||||
@ -10161,11 +10164,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-+lqLGxWZsEe4Z6OrzBI7Ym4SMUTaMS5yOrHZ0/IL0bpIye1Qbs4PpobJL2mLDbftUXlPFZR7fu6d1yM+bHLX1w==}
|
resolution: {integrity: sha512-+lqLGxWZsEe4Z6OrzBI7Ym4SMUTaMS5yOrHZ0/IL0bpIye1Qbs4PpobJL2mLDbftUXlPFZR7fu6d1yM+bHLX1w==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/uuencode@0.0.3:
|
/@types/uuencode@0.0.3(patch_hash=3i7wecddkama6vhpu5o37g24u4):
|
||||||
resolution: {integrity: sha512-NaBWHPPQvcXqiSaMAGa2Ea/XaFcK/nHwGe2akwJBXRLkCNa2+izx/F1aKJrzFH+L68D88VLYIATTYP7B2k4zVA==}
|
resolution: {integrity: sha512-NaBWHPPQvcXqiSaMAGa2Ea/XaFcK/nHwGe2akwJBXRLkCNa2+izx/F1aKJrzFH+L68D88VLYIATTYP7B2k4zVA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 18.16.16
|
'@types/node': 18.16.16
|
||||||
dev: true
|
dev: true
|
||||||
|
patched: true
|
||||||
|
|
||||||
/@types/uuid@8.3.4:
|
/@types/uuid@8.3.4:
|
||||||
resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==}
|
resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==}
|
||||||
@ -24340,8 +24344,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-0vOfAtI67UjeO1G6UiX5Kd76CqaQ67wrRZiOe7UAb9Jm6GzlUr/fC7CV90XfwapJRjpCMaZFhv1V0ajWRmE9Dg==}
|
resolution: {integrity: sha512-0vOfAtI67UjeO1G6UiX5Kd76CqaQ67wrRZiOe7UAb9Jm6GzlUr/fC7CV90XfwapJRjpCMaZFhv1V0ajWRmE9Dg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/vue-component-type-helpers@2.0.17:
|
/vue-component-type-helpers@2.0.18:
|
||||||
resolution: {integrity: sha512-2car49m8ciqg/JjgMBkx7o/Fd2A7fHESxNqL/2vJYFLXm4VwYO4yH0rexOi4a35vwNgDyvt17B07Vj126l9rAQ==}
|
resolution: {integrity: sha512-zi1QaDBhSb3oeHJh55aTCrosFNKEQsOL9j3XCAjpF9dwxDUUtd85RkJVzO+YpJqy1LNoCWLU8gwuZ7HW2iDN/A==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/vue-demi@0.14.5(vue@3.4.21):
|
/vue-demi@0.14.5(vue@3.4.21):
|
||||||
|
Loading…
Reference in New Issue
Block a user